diff --git a/.github/ct.yaml b/.github/ct.yaml index 531244f4..969d7b0d 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -4,7 +4,10 @@ chart-dirs: - helm chart-repos: - bitnami=https://charts.bitnami.com/bitnami + - elastic=https://helm.elastic.co + - grafana=https://grafana.github.io/helm-charts helm-extra-args: --timeout 600s check-version-increment: true debug: false -validate-maintainers: false \ No newline at end of file +validate-maintainers: false +helm-dependency-extra-args: "--skip-refresh" \ No newline at end of file diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index 1ca99744..20be854e 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -22,29 +22,41 @@ jobs: check-latest: true - name: Set up chart-testing - uses: helm/chart-testing-action@v2.3.1 + uses: helm/chart-testing-action@v2.6.1 - name: Run chart-testing (list-changed) id: list-changed run: | changed=$(ct list-changed --config .github/ct.yaml) if [[ -n "$changed" ]]; then - echo "::set-output name=changed::true" + echo "changed=true >> $GITHUB_OUTPUT" fi - name: Run chart-testing (lint) run: ct lint --config .github/ct.yaml - - # deploy-charts-to-kind: - # name: ${{ matrix.environments }} - gen3 data portal build + + # TODO: add back in when we have tests + # deploy-and-test-chart: + # name: Deploy and Test Chart # timeout-minutes: 20 # runs-on: ubuntu-latest - # needs: [get-changes-for-envs] - # if: ${{ needs.get-changes-for-envs.outputs.matrix != '[]' && needs.get-changes-for-envs.outputs.matrix != '' }} # steps: + + # - name: Checkout + # uses: actions/checkout@v2 + # with: + # fetch-depth: 0 + + # - name: Set up Helm + # uses: azure/setup-helm@v3 + + # - name: Set up chart-testing + # uses: helm/chart-testing-action@v2.6.1 + + # - name: Create kind cluster - # uses: helm/kind-action@v1.4.0 - # if: steps.list-changed.outputs.changed == 'true' + # uses: helm/kind-action@v1.8.0 + - # - name: Run chart-testing (install) - # run: ct install + # - name: Run chart install + testing + # run: ct install --charts ./helm/gen3 --config .github/ct.yaml diff --git a/.gitignore b/.gitignore index 66497221..df2d04fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ postgres.txt **/charts/ notes/ -Chart.lock \ No newline at end of file +Chart.lock +.DS_Store +_sample-*/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a8a8714..c0cac5fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,36 +1,39 @@ repos: - repo: git@github.com:Yelp/detect-secrets - rev: v0.13.1 + rev: v1.5.0 hooks: - id: detect-secrets args: ["--baseline", ".secrets.baseline"] + additional_dependencies: ["gibberish-detector"] + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.5.0 hooks: - id: no-commit-to-branch args: [--branch, develop, --branch, master, --pattern, release/.*] - + # - repo: https://github.com/gruntwork-io/pre-commit # rev: v0.1.17 # Get the latest from: https://github.com/gruntwork-io/pre-commit/releases # hooks: # - id: helmlint - - repo: local - hooks: - - id: helm-docs - args: [] - description: Uses 'helm-docs' to create documentation from the Helm chart's 'values.yaml' file, and inserts the result into a corresponding 'README.md' file. - entry: git-hook/helm-docs.sh - language: script - name: Helm Docs - require_serial: true - - - repo: local - hooks: - - id: helm-chart-bump - args: [] - description: Updates the .Chart.yaml with updates version if there are changes since master branch. This is to ensure we bump our charts for updates. - entry: git-hook/helm-bump.sh - language: script - name: Helm Docs - require_serial: true \ No newline at end of file + - repo: https://github.com/norwoodj/helm-docs + rev: "v1.14.2" + hooks: + # Use a pinned version of helm-docs in a container to generate consistent documentation. + - name: helm-docs + id: helm-docs-container + entry: jnorwood/helm-docs:v1.14.2 + args: + - "--chart-search-root=helm" + - "--skip-version-footer" + + - repo: local + hooks: + - id: helm-chart-bump + args: [] + description: Updates the .Chart.yaml with updates version if there are changes since master branch. This is to ensure we bump our charts for updates. + entry: git-hook/helm-bump.sh + language: script + name: Helm Docs + require_serial: true diff --git a/.secrets.baseline b/.secrets.baseline index 4bbd33c5..370b6e54 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,19 +1,18 @@ { - "exclude": { - "files": "^.secrets.baseline$", - "lines": null - }, - "generated_at": "2023-03-16T19:37:11Z", + "version": "1.5.0", "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, { "name": "AWSKeyDetector" }, { - "name": "ArtifactoryDetector" + "name": "AzureStorageKeyDetector" }, { - "base64_limit": 4.5, - "name": "Base64HighEntropyString" + "name": "Base64HighEntropyString", + "limit": 4.5 }, { "name": "BasicAuthDetector" @@ -22,8 +21,17 @@ "name": "CloudantDetector" }, { - "hex_limit": 3, - "name": "HexHighEntropyString" + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 }, { "name": "IbmCloudIamDetector" @@ -31,760 +39,144 @@ { "name": "IbmCosHmacDetector" }, + { + "name": "IPPublicDetector" + }, { "name": "JwtTokenDetector" }, { - "keyword_exclude": null, - "name": "KeywordDetector" + "name": "KeywordDetector", + "keyword_exclude": "" }, { "name": "MailchimpDetector" }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, { "name": "PrivateKeyDetector" }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, { "name": "SlackDetector" }, { "name": "SoftlayerDetector" }, + { + "name": "SquareOAuthDetector" + }, { "name": "StripeDetector" }, + { + "name": "TelegramBotTokenDetector" + }, { "name": "TwilioKeyDetector" } ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.gibberish.should_exclude_secret", + "limit": 3.7 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], "results": { - "README.md": [ - { - "hashed_secret": "ac0fedaac180de6bd70a97b711692a92dade479e", - "is_secret": false, - "is_verified": false, - "line_number": 83, - "type": "Secret Keyword" - }, - { - "hashed_secret": "64ab0c1d3edc1c8c166351207b840ac7b2a90523", - "is_secret": false, - "is_verified": false, - "line_number": 112, - "type": "Secret Keyword" - } - ], - "docs/databases.md": [ - { - "hashed_secret": "ac0fedaac180de6bd70a97b711692a92dade479e", - "is_secret": false, - "is_verified": false, - "line_number": 22, - "type": "Secret Keyword" - } - ], - "helm/arborist/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 38, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 40, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 59, - "type": "Secret Keyword" - } - ], - "helm/audit/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 46, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 76, - "type": "Secret Keyword" - } - ], - "helm/aws-es-proxy/README.md": [ - { - "hashed_secret": "7c150ec931dbb741d0bfd6c8f4ef914026c0b44b", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Secret Keyword" - } - ], - "helm/common/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 22, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 24, - "type": "Secret Keyword" - } - ], - "helm/common/templates/_postgres_secrets.tpl": [ - { - "hashed_secret": "07b87392697bbdd9d97f6cd887f901820a0150df", - "is_secret": false, - "is_verified": false, - "line_number": 34, - "type": "Secret Keyword" - }, - { - "hashed_secret": "e343239977fa87adac52528619fc6bf2e1a82ee7", - "is_secret": false, - "is_verified": false, - "line_number": 62, - "type": "Secret Keyword" - } - ], - "helm/dicom-server/README.md": [ - { - "hashed_secret": "b47233f6f28e9716c72d5eba0278edea3a24baad", - "is_secret": false, - "is_verified": false, - "line_number": 21, - "type": "Secret Keyword" - }, - { - "hashed_secret": "3f6d5580af2ddf647ca25346aa6ec9c434577d05", - "is_secret": false, - "is_verified": false, - "line_number": 36, - "type": "Secret Keyword" - } - ], - "helm/dicom-server/values.yaml": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 68, - "type": "Secret Keyword" - } - ], - "helm/fence/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 99, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 101, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 129, - "type": "Secret Keyword" - }, - { - "hashed_secret": "9d8fada0e01336e865c461bb3549084d206fe6da", - "is_secret": false, - "is_verified": false, - "line_number": 160, - "type": "Secret Keyword" - } - ], - "helm/fence/fence-secret/config_helper.py": [ - { - "hashed_secret": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", - "is_secret": false, - "is_verified": false, - "line_number": 66, - "type": "Basic Auth Credentials" - } - ], - "helm/fence/fence-secret/fence_settings.py": [ - { - "hashed_secret": "3ef0fb8a603abdc0b6caac44a23fdc6792f77ddf", - "is_secret": false, - "is_verified": false, - "line_number": 6, - "type": "Basic Auth Credentials" - }, - { - "hashed_secret": "b60d121b438a380c343d5ec3c2037564b82ffef3", - "is_secret": false, - "is_verified": false, - "line_number": 58, - "type": "Secret Keyword" - }, - { - "hashed_secret": "347cd9c53ff77d41a7b22aa56c7b4efaf54658e3", - "is_secret": false, - "is_verified": false, - "line_number": 80, - "type": "Basic Auth Credentials" - } - ], - "helm/fence/templates/fence-creds.yaml": [ - { - "hashed_secret": "c2dae5a3c7ce218639b38d8a0256f02fe81d439e", - "is_secret": false, - "is_verified": false, - "line_number": 11, - "type": "Secret Keyword" - }, - { - "hashed_secret": "1f5e25be9b575e9f5d39c82dfd1d9f4d73f1975c", - "is_secret": false, - "is_verified": false, - "line_number": 14, - "type": "Secret Keyword" - }, - { - "hashed_secret": "8db3b325254b6389ca194d829d2fc923dc0a945d", - "is_secret": false, - "is_verified": false, - "line_number": 15, - "type": "Secret Keyword" - } - ], - "helm/fence/values.yaml": [ - { - "hashed_secret": "5d07e1b80e448a213b392049888111e1779a52db", - "is_secret": false, - "is_verified": false, - "line_number": 1890, - "type": "Secret Keyword" - } - ], - "helm/gen3/README.md": [ - { - "hashed_secret": "4caa5dcab48a481e96f4352e45459c0ecd6f3cf7", - "is_secret": false, - "is_verified": false, - "line_number": 76, - "type": "Secret Keyword" - }, - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 89, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 91, - "type": "Secret Keyword" - } - ], - "helm/guppy/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 53, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 55, - "type": "Secret Keyword" - } - ], - "helm/hatchery/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 34, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 36, - "type": "Secret Keyword" - }, - { - "hashed_secret": "e94cc2a86b04ad4ddc98fcbf91ed236437939d47", - "is_secret": false, - "is_verified": false, - "line_number": 46, - "type": "Secret Keyword" - } - ], - "helm/hatchery/values.yaml": [ - { - "hashed_secret": "9b5925ea817163740dfb287a9894e8ab3aba2c18", - "is_secret": false, - "is_verified": false, - "line_number": 186, - "type": "Secret Keyword" - } - ], - "helm/indexd/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 40, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 61, - "type": "Secret Keyword" - }, - { - "hashed_secret": "cb87e7ebb6991e08dc8964923e04230d002b7f12", - "is_secret": false, - "is_verified": false, - "line_number": 90, - "type": "Secret Keyword" - } - ], - "helm/indexd/indexd-settings/local_settings.py": [ - { - "hashed_secret": "0a0d18c85e096611b5685b62bc60ec534d19bacc", - "is_secret": false, - "is_verified": false, - "line_number": 60, - "type": "Basic Auth Credentials" - } - ], - "helm/indexd/templates/indexd-secret.yaml": [ - { - "hashed_secret": "c2dae5a3c7ce218639b38d8a0256f02fe81d439e", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Secret Keyword" - } - ], - "helm/manifestservice/README.md": [ - { - "hashed_secret": "611f2e9064b518afdb23f201321f39029dd28917", - "is_secret": false, - "is_verified": false, - "line_number": 56, - "type": "Secret Keyword" - } - ], - "helm/manifestservice/templates/metadataservice-creds.yaml": [ - { - "hashed_secret": "d2e2ab0f407e4ee3cf2ab87d61c31b25a74085e5", - "is_secret": false, - "is_verified": false, - "line_number": 12, - "type": "Secret Keyword" - } - ], - "helm/metadata/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 52, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 54, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 74, - "type": "Secret Keyword" - } - ], - "helm/peregrine/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 44, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 62, - "type": "Secret Keyword" - }, - { - "hashed_secret": "7d4e263f1ae83868444f5327219830493a7d1486", - "is_secret": false, - "is_verified": false, - "line_number": 89, - "type": "Secret Keyword" - } - ], - "helm/peregrine/peregrine-secret/config_helper.py": [ - { - "hashed_secret": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", - "is_secret": false, - "is_verified": false, - "line_number": 66, - "type": "Basic Auth Credentials" - } - ], - "helm/peregrine/peregrine-secret/settings.py": [ - { - "hashed_secret": "347cd9c53ff77d41a7b22aa56c7b4efaf54658e3", - "is_secret": false, - "is_verified": false, - "line_number": 45, - "type": "Basic Auth Credentials" - } - ], - "helm/pidgin/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 46, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Secret Keyword" - }, - { - "hashed_secret": "abb751db44bcfd1bb9d4ad53e40138422abd739e", - "is_secret": false, - "is_verified": false, - "line_number": 61, - "type": "Secret Keyword" - } - ], "helm/portal/README.md": [ { + "type": "Base64 High Entropy String", + "filename": "helm/portal/README.md", "hashed_secret": "eb9739c6625f06b4ab73035223366dda6262ae77", - "is_secret": false, "is_verified": false, - "line_number": 26, - "type": "Base64 High Entropy String" + "line_number": 34, + "is_secret": false }, { + "type": "Base64 High Entropy String", + "filename": "helm/portal/README.md", "hashed_secret": "08eeb737b239bdb7362a875b90e22c10b8826b20", - "is_secret": false, - "is_verified": false, - "line_number": 30, - "type": "Base64 High Entropy String" - }, - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, "is_verified": false, - "line_number": 46, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Secret Keyword" + "line_number": 39, + "is_secret": false } ], "helm/portal/values.yaml": [ { + "type": "Base64 High Entropy String", + "filename": "helm/portal/values.yaml", "hashed_secret": "08eeb737b239bdb7362a875b90e22c10b8826b20", "is_verified": false, - "line_number": 460, - "type": "Base64 High Entropy String" + "line_number": 475, + "is_secret": false }, { + "type": "Base64 High Entropy String", + "filename": "helm/portal/values.yaml", "hashed_secret": "eb9739c6625f06b4ab73035223366dda6262ae77", "is_verified": false, - "line_number": 463, - "type": "Base64 High Entropy String" - } - ], - "helm/requestor/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 50, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 52, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 71, - "type": "Secret Keyword" - } - ], - "helm/revproxy/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 33, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 35, - "type": "Secret Keyword" - }, - { - "hashed_secret": "abb751db44bcfd1bb9d4ad53e40138422abd739e", - "is_secret": false, - "is_verified": false, - "line_number": 60, - "type": "Secret Keyword" + "line_number": 477, + "is_secret": false } ], "helm/revproxy/nginx/helpers.js": [ { + "type": "Base64 High Entropy String", + "filename": "helm/revproxy/nginx/helpers.js", "hashed_secret": "1d278d3c888d1a2fa7eed622bfc02927ce4049af", - "is_secret": false, "is_verified": false, "line_number": 10, - "type": "Base64 High Entropy String" - } - ], - "helm/sheepdog/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 51, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 53, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 69, - "type": "Secret Keyword" - }, - { - "hashed_secret": "c2c4e52c03a03ce3efeb21eb202d301018d4548e", - "is_secret": false, - "is_verified": false, - "line_number": 89, - "type": "Secret Keyword" - }, - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 96, - "type": "Secret Keyword" - }, - { - "hashed_secret": "fa4497447699cdb0a81c66a7f21af28a75170195", - "is_secret": false, - "is_verified": false, - "line_number": 98, - "type": "Secret Keyword" - } - ], - "helm/sheepdog/sheepdog-secret/config_helper.py": [ - { - "hashed_secret": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", - "is_secret": false, - "is_verified": false, - "line_number": 66, - "type": "Basic Auth Credentials" - } - ], - "helm/sheepdog/sheepdog-secret/wsgi.py": [ - { - "hashed_secret": "347cd9c53ff77d41a7b22aa56c7b4efaf54658e3", - "is_secret": false, - "is_verified": false, - "line_number": 38, - "type": "Basic Auth Credentials" - } - ], - "helm/sheepdog/values.yaml": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 229, - "type": "Secret Keyword" - } - ], - "helm/ssjdispatcher/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 44, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 46, - "type": "Secret Keyword" - }, - { - "hashed_secret": "0c86d58792b32e1d12af733a0614837ff9002014", - "is_secret": false, - "is_verified": false, - "line_number": 104, - "type": "Secret Keyword" - } - ], - "helm/ssjdispatcher/templates/ssjdispatcher-secret.yaml": [ - { - "hashed_secret": "d2e2ab0f407e4ee3cf2ab87d61c31b25a74085e5", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Secret Keyword" - } - ], - "helm/ssjdispatcher/values.yaml": [ - { - "hashed_secret": "13d9ed7e3d69f1b6330dff80bc4658931708eddc", - "is_secret": false, - "is_verified": false, - "line_number": 227, - "type": "Secret Keyword" - } - ], - "helm/wts/README.md": [ - { - "hashed_secret": "2546383b95bb44732e9be6a877fd476c0442fdab", - "is_secret": false, - "is_verified": false, - "line_number": 40, - "type": "Secret Keyword" - }, - { - "hashed_secret": "d84ce25b0f9bc2cc263006ae39453efb22cc2900", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Secret Keyword" - }, - { - "hashed_secret": "f09dd6e359833a12f48c4c4255d6e87a6e55cfe9", - "is_secret": false, - "is_verified": false, - "line_number": 69, - "type": "Secret Keyword" - } - ], - "sample-values/fence-config.yaml": [ - { - "hashed_secret": "5d07e1b80e448a213b392049888111e1779a52db", - "is_secret": false, - "is_verified": false, - "line_number": 560, - "type": "Secret Keyword" - } - ], - "sample-values/values_aws_dev.yaml": [ - { - "hashed_secret": "9b5925ea817163740dfb287a9894e8ab3aba2c18", - "is_secret": false, - "is_verified": false, - "line_number": 34, - "type": "Secret Keyword" - } - ], - "skaffold.yaml": [ - { - "hashed_secret": "b60d121b438a380c343d5ec3c2037564b82ffef3", - "is_secret": false, - "is_verified": false, - "line_number": 30, - "type": "Secret Keyword" + "is_secret": false } ] }, - "version": "0.13.1", - "word_list": { - "file": null, - "hash": null - } + "generated_at": "2024-12-05T20:32:52Z" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c81ac0d3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,134 @@ +# Contributing + +We welcome contributions to the gen3-helm repository! This document outlines the guidelines for contributing to this project. + +## Git and GitHub resources + +Before starting a new contribution, you need to be familiar with [Git](https://git-scm.com/) and [GitHub](https://github.com/) concepts like: ***commit, branch, push, pull, remote, fork, repository***, etc. There are plenty of resources online to learn Git and GitHub, for example: +- [Git Guide](https://github.com/git-guides/) +- [GitHub Quick start](https://docs.github.com/en/get-started/quickstart) +- [GitHub on YouTube](https://www.youtube.com/github) +- [Git and GitHub learning resources](https://docs.github.com/en/get-started/quickstart/git-and-github-learning-resources) +- [Collaborating with Pull Requests](https://docs.github.com/en/github/collaborating-with-pull-requests) +- [GitHub Documentation, guides and help topics](https://docs.github.com/en/github) +- And many more... + + +## Before You Begin + + +If you have an idea for a new feature or a bugfix, it is best to communicate with the University of Chicago Center for Translational Data Science (CTDS) developers early. The primary venue for this is the [GitHub issue tracker](https://github.com/uc-cdis/gen3-helm/issues). Browse through existing GitHub issues and if one seems related, comment on it. For more direct communication, CTDS developers are generally available via Slack. + + +## Reporting a New Issue + +If you have identified a potential new issue the first step is to ask the community whether this is something they are familiar with and for which they may already have a solution. The slack channel #gen3_helm_ext is the preferred forum for communication regarding helm. Please inquire in #gen3_community if you would like access ([request access here](https://docs.google.com/forms/d/e/1FAIpQLSczyhhOXeCK9FdVtpQpelOHYnRj1EAq1rwwnm9q6cPAe5a7ug/viewform)). + +If the community has no solution and no existing gen3-helm issue seems appropriate, a new issue can be opened using [this form](https://github.com/uc-cdis/gen3-helm/issues/new). Please be specific in your comment and include instructions on how to reproduce the issue. Please also make sure to add a short descriptive title. + +## How to Contribute + +All changes to the gen3-helm repository should be made through pull requests. + +1. Fork the [gen3-helm repository](https://github.com/uc-cdis/gen3-helm) on GitHub to make your changes. + +4. Run the relevant tests for the features added or bugs fixed by your pull request. + +5. Write a descriptive commit message. + +6. Commit and push your changes to your fork. + +7. Open a pull request with these changes. + +8. Your pull request will be reviewed by a project maintainer and merged if it is deemed appropriate. + +## Style Guidelines + +### Helm + +- `gen3-helm` follows [General Conventions](https://helm.sh/docs/chart_best_practices/) for helm charts. + +## Documentation + +Documentation is found in the ``docs/`` directory. + +The documentation source files are written in [Markdown](https://daringfireball.net/projects/markdown/syntax) format. + +Each chart has its own README.md that is automatically built with [helm-docs](https://github.com/norwoodj/helm-docs). This happens in the pre-commit so make sure to check in all the changed files. + +## Helm chart release strategy + +It is important to understand that when a branch is merged into the main branch, a GitHub action will generate a new helm chart release if the helm chart version in the chart.yaml file has been incremented. Consider the following example where a change to the Helm chart has been made and the contributor wants a new version to be released: + +The original Chart.yaml file: + + ```yaml + apiVersion: v2 + name: Sheepdog + description: A Helm chart for Kubernetes + type: application + version: 0.1.0 + ``` + +If a modification to the Helm chart is made (an update to the values.yaml file for instance) the version in Chart.yaml is incremented to `0.2.0`: + + ```yaml + apiVersion: v2 + name: Sheepdog + description: A Helm chart for Kubernetes + type: application + version: 0.2.0 # version updates to 0.2.0 + ``` + +Once the associated branch is merged into the main branch, the GitHub action packages and publishes an artifact, making it available for consumption. The release name is based off the 'name' field and the 'version' field in the Chart.yaml file. Given the example above, GitHub action will produce a release called `sheepdog-0.2.0`. + + +## Branch Naming Conventions + +Branches are named as `type/scope`, and commit messages are written as `type(scope): explanation`, where + +- `scope` identifies the thing that was added or modified, +- `explanation` is a brief description of the changes in imperative present tense (such as “add function to _”, not “added function”), +- and `type` is defined as: + ``` + type = "chore" | "docs" | "feat" | "fix" | "refactor" | "style" | "test" + ``` + +Some example branch names: + +- `refactor/db-calls` +- `test/user` +- `docs/deployment` + +Some example commit messages: + +- `fix(scope): remove admin scope from client` +- `feat(project_members): list all members given project` +- `docs(generation): fix generation script and update docs` + +## Pull Requests (PRs) + + +Before submitting a PR for review, try to make sure you’ve accomplished these things: + +The PR: +- contains a brief description of what it changes and/or adds +- passes status checks +- If there are changes to the charts, it bumps the chart versions + + +To merge the PR: + +If the branch now has conflicts with the master branch, follow these steps to update it: + +```bash +git checkout master +git pull origin master +git checkout $YOUR_BRANCH_NAME +git merge master +git commit +# The previous command should open an editor with the default merge commit +# message; simply save and exit +git push + +``` diff --git a/README.md b/README.md index aef945f3..bd0b0da2 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,7 @@ Helm charts for deploying [Gen3](https://gen3.org) on any kubernetes cluster. -# Deployment instructions -For a full set of configuration options see the [README.md for gen3](./helm/gen3/README.md) - -To see documentation around setting up gen3 developer environments see [gen3_developer_environments.md](./docs/gen3_developer_environments.md) +# Deploying gen3 with helm ## TL;DR ``` @@ -17,93 +14,43 @@ helm repo update helm upgrade --install gen3 gen3/gen3 -f ./values.yaml ``` -Use the following as a template for your `values.yaml` file for a minimum deployment of gen3 using these helm charts. +For more information on how to deploy Gen3 with helm, please see the [Gen3 Example Deployment Guide](https://docs.gen3.org/docs/Deployment/Example%20Deployment) +https://docs.gen3.org -```yaml -global: - hostname: example-commons.com - -fence: - FENCE_CONFIG: - OPENID_CONNECT: - google: - client_id: "insert.google.client_id.here" - client_secret: "insert.google.client_secret.here" -``` - -This is to have a gen3 deployment with google login. You may also use MOCK_AUTH using the following config. NB! This will bypass any login and is only recommended for testing environments +## Configuration +For a full set of configuration options see the [CONFIGURATION.md](./docs/CONFIGURATION.md) for a more in depth instructions on how to configure each service. -```yaml -global: - hostname: example-commons.com +There's also an auto-generated table of basic configuration options here: -fence: - FENCE_CONFIG: - # if true, will automatically login a user with username "test" - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - MOCK_AUTH: true -``` +[README.md for gen3 chart](./helm/gen3/README.md) (auto-generated documentation) or -## Selective deployments -All service helm charts are sub-charts of the gen3 chart (which acts as an umbrella chart) -To enable or disable a service you can add this pattern to your `values.yaml` +To see documentation around setting up gen3 developer environments see [our Example Deployment](https://docs.gen3.org/docs/Deployment/Example%20Deployment/). -```yaml -fence: - enabled: true -wts: - enabled: false -``` - - -## Prerequisites - -### Kubernetes cluster -Any kubernetes cluster _should_ work. We are testing with EKS, AKS, GKE and Rancher Desktop. - -It is suggested to use [Rancher Desktop](https://rancherdesktop.io/) as Kubernetes on your laptop, especially on M1 Mac's. You also get ingress and other benefits out of the box. +Use the following as a template for your `values.yaml` file for a minimum deployment of gen3 using these helm charts. -### Postgres -We need a postgres database. For development/CI clusters an instance of postgres is deployed and automatically configured for you. -For production environments please fill out these values and provide a master password for postgres -``` +```yaml global: - postgres: - db_create: true - master: - host: insert.postgres.hostname.here - username: postgres - password: - port: "5432" + hostname: example-commons.com + +fence: + FENCE_CONFIG: + # Any fence-config overrides here. ``` -### Login Options +## Gen3 Login Options Gen3 does not have any IDP, but can integrate with many. We will cover Google login here, but refer to the fence documentation for additional options. TL/DR: At minimum to have google logins working you need to set these settings in your `values.yaml` file ``` -global: - aws: - # If you're deploying to an EKS set this to true. This will annotate ingress/service accounts appropriately. - # In the future we will be adding support for GKE/AKS using same method. - enabled: true - aws_access_key_id: - aws_secret_access_key: - postgres: - master: - host: "rds.host.com" - username: "postgres" - password: "test" - port: "5432" fence: FENCE_CONFIG: OPENID_CONNECT: @@ -133,25 +80,6 @@ For `"Authorized redirect URIs"` add `https:///user/login/google/logi After configuration is complete, take note of the client ID that was created. You will need the client ID and client secret to complete the next steps. -# Production deployments -For production deployments you have to use an external postgres server and elasticsearch server. - -NOTE: Gen3 helm charts are currently not used in production by CTDS, but we are aiming to do that soon and will have additional documentation on that. - -# Local Development - -For local development you must be connected to a kubernetes cluster. As referenced above in the section `Kubernetes cluster` we recommend using [Rancher Desktop](https://rancherdesktop.io/) as Kubernetes on your local machine, especially on M1 Mac's. You also get ingress and other benefits out of the box. - -1. Clone the repository -2. Navigate to the `gen3-helm/helm/gen3` directory and run `helm dependency update` -3. Navigate to the back to the `gen3-helm` directory and create your values.yaml file. See the `TL;DR` section for a minimal example. -4. Run `helm upgrade --install gen3 ./helm/gen3 -f ./values.yaml` - -## Using Skaffold - -Skaffold is a tool for local development that can be used to automatically rebuild and redeploy your application when changes are detected. A minimal skaffold.yaml configuration file has been provided in the gen3-helm directory. Update the values of this file to match your needs. - -Follow the steps above, but instead of doing the helm upgrade --install step, use `skaffold dev` to start the development process. Skaffold will automatically build and deploy your application to your kubernetes cluster. # Troubleshooting diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 00000000..3323c0e1 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,610 @@ +# Gen3 Services + +# Ambassador + +## What Does it Do + +Ambassador is an envoy proxy. We use this service to direct traffic toward our workspaces, hatchery and jupyter containers. + +## How to Configure it + +For a full set of configuration see the [helm README.md for ambassador](../helm/ambassador/README.md) or read the [values.yaml](../helm/ambassador/values.yaml) directly + +Example configuration using gen3 umbrella chart: + +```yaml +ambassador: + # Whether or not to deploy the service or not + enabled: true + + # What image/ tag to pull + image: + repository: quay.io/datawire/ambassador + tag: "1.4.2" + pullPolicy: Always +``` + + +## Extra Information + +Ambassador is only necessary if there is a hatchery deployment, as this is used as an envoy proxy primarily for workspaces. This may change in the future. + +--- + +# aws-es-proxy + +## What Does it Do + +The aws-es-proxy is a proxy for hitting the elasticsearch service running in AWS. It is required for guppy, ETL and metadata, if you are running managed elasticsearch in AWS, as they leverage this pod to talk to elasticsearch. + +## How to Configure it + + +For a full set of configuration see the [helm README.md for aws-es-proxy](../helm/aws-es-proxy/README.md) or read the [values.yaml](../helm/aws-es-proxy/values.yaml) directly + + +Some important configuration items for `aws-es-proxy` in helm: + +```yaml +# -- AWS user to use to connect to ES +aws-es-proxy: + # Whether or not to deploy the service or not + enabled: true + + # What image/ tag to pull + image: + repository: + tag: + + # AWS secrets + secrets: + awsAccessKeyId: "" + awsSecretAccessKey: "" + + # Elasticsearch endpoint in AWS + esEndpoint: test.us-east-1.es.amazonaws.com +``` + + +## Extra Information + +This pod can also be used to make direct queries to elastic search. If you know you want to make a manaul query to elastic search. You can exec into the aws-es-proxy pod and run the following, filling in the appropriate endpoint you want to hit to query elasticsearch. + +```bash +curl http://localhost:9200/ +``` + +--- + +# Arborist + +## What Does it Do + +Arborist is the authorization service. It works with fence to assign authortizations to a user based on their authentication information. Information around user authorizations are set within a useryaml, or telemetry file for dbgap authorized users, and put into the arborist db during usersync. + +## How to configure it + +For a full set of configuration see the [helm README.md for arborist](../helm/arborist/README.md) or read the [values.yaml](../helm/arborist/values.yaml) directly + +Some configuration options include: +- postgres configuration +- image repo/ tag + + +```yaml +arborist: + # Whether or not to deploy the service or not + enabled: true + + # What image/ tag to pull + image: + tag: + repository: +``` + +## Extra Information + +[Find common arborist database queries here](https://github.com/uc-cdis/cdis-wiki/blob/master/dev/gen3-sql-queries.md#arborist-database). + +--- + +# Fence + +## What Does it Do + +Fence is a core service for a gen3 datacommons which handles authentication. It is necessary for a commons to run and will handle authentication on the /login endpoint as well as creating presigned url's in the presigned-url-fence pods. + +## How to Configure it + + +```yaml +fence: + # Whether or not to deploy the service or not + enabled: true + + # What image/ tag to pull + image: + tag: + repository: + + # FENCE_CONFIG + FENCE_CONFIG: + OPENID_CONNECT: + google: + client_id: "insert.google.client_id.here" + client_secret: "insert.google.client_secret.here" + + # -- (string) USER YAML. Passed in as a multiline string. + USER_YAML: | + + + +``` + +You need to ensure a proper working fence-config file. Fence is highly configurable and a lot of config is commons specific, but some important fields to configure are as follows. + +1. BASE_URL + * This should be (the url of the commons)/user. +2. DB + * This should contain the psql connection string, which should contain the correct database, user, password and hostname. +3. OPENID_CONNECT + * This is where different IdP's can be configured. To be able to leverage an IdP as a login option you need to add the client id's/secrets and any other necesary config to the predefined blocks. +4. ENABLED_IDENTITY_PROVIDERS/LOGIN_OPTIONS + * Use one of these blocks to enable/configure buttons for logging into the IdP's defined in the OPENID_CONNECT block. +5. DEFAULT_LOGIN_IDP/DEFAULT_LOGIN_URL + * These blocks will define the default login option, which will be used by most external oidc clients. +6. dbGaP + * This will be used to connect to an sftp server which will contain telemetry files for usersync. Is necessary for setting up authorizations outside of the useryaml. +7. AWS_CREDENTIALS/S3_BUCKETS/DATA_UPLOAD_BUCKET + * The AWS_CREDENTIALS block will define credentials for service accounts used to access s3 buckets. The s3 buckets are defined in the S3_BUCKETS block, which will reference a credential in the AWS_CREDENTIALS block. The DATA_UPLOAD_BUCKET block defines the data upload bucket, which is the bucket used in the data upload flow, to upload files to a commons. +8. CIRRUS_CFG + * If google buckets are used you need to configure this block. It is used to setup the google bucket workflow, which essentially creates google users and google bucket access groups, which get filled with users and added to bucket policies to allow implicit access to users. + +For more infomation, [see this](https://github.com/uc-cdis/fence/blob/master/fence/config-default.yaml) + + +A user.yaml will control access to your data commons. To see how to construct a user.yaml properly: + +https://github.com/uc-cdis/fence/blob/master/docs/additional_documentation/user.yaml_guide.md + +## Extra Information + +### Fence Pods + +Fence is split into 2 deployments. There is the regular fence deployment which handles commons authentication. We also split the presigned url feature of fence into a seperate deployment, the presigned-url-fence deployment. They will both get setup/deployed with a gen3 installation. + +### Troubleshooting Fence + +There are [some commons sql queries that can be found here](https://github.com/uc-cdis/cdis-wiki/blob/master/dev/gen3-sql-queries.md#fence-database). + +### Setting up OIDC clients + +OIDC (OpenID Connect) clients allow applications to authenticate with Fence. This setup is often necessary for external users who want to integrate their applications with Gen3. For each application, you'll need to create a unique OIDC client, which will provide a client_id and client_secret for the application to use. + +Once the client is created, share the client_id and client_secret with the application owner so they can configure their application to authenticate with Fence. To create these clients, you will need to exec into a fence container and run the [following commands](https://github.com/uc-cdis/fence/blob/master/docs/additional_documentation/setup.md#register-oauth-client). + + +--- + +# Guppy + +## What Does it Do + +Guppy is used to render the explorer page. It uses elastic search indices to render the page. + +## How to Configure it + + +For a full set of configuration see the [helm README.md for guppy](../helm/guppy/README.md) or read the [values.yaml](../helm/guppy/values.yaml) directly + + +There is also config that needs to be set within the global block around the tier access level, defining how the explorer page should handle displaying unauthorized files, and the limit to how far unauthorized user can filter down files. Last, there is a guppy block that needs to be configured with the elastic search indices guppy will use to render the explorer page. + +``` +global: + tierAccessLevel: "(libre|regular|private)" + +guppy: + # -- (int) Only relevant if tireAccessLevel is set to "regular". + # The minimum amount of files unauthorized users can filter down to + tierAccessLimit: "1000" + + # -- (list) Elasticsearch index configurations + indices: + - index: dev_case + type: case + - index: dev_file + type: file + + # -- (string) The Elasticsearch configuration index + configIndex: dev_case-array-config + # -- (string) The field used for access control and authorization filters + authFilterField: auth_resource_path + # -- (bool) Whether or not to enable encryption for specified fields + enableEncryptWhitelist: true + # -- (string) A comma-separated list of fields to encrypt + encryptWhitelist: test1 + + + # -- (string) Elasticsearch endpoint. + # defaults to "elasticsearch:9200" + esEndpoint: "" +``` + + +You will also need a mapping file to map the fields you want to pull from postgres into the elasticsearch indices. There are too many fields to describe here, but [an example mapping file can be found here](https://github.com/uc-cdis/cdis-manifest/blob/master/gen3.biodatacatalyst.nhlbi.nih.gov/etlMapping.yaml). + +Last, guppy works closely with portal to render the explorer page. You will need to ensure a proper [dataExplorer block](https://github.com/uc-cdis/cdis-manifest/blob/master/gen3.biodatacatalyst.nhlbi.nih.gov/portal/gitops.json#L212) is setup within the gitops.json file, referencing fields that have been pulled from postgres into the elasticsearch indices. + +## Extra Information + +Guppy relies on indices being created to run, if there are no indices created guppy will fail to start up. + +To create these indices you can run etl, however a valid ETL mapping file needs to be created and data needs to be submitted to the commons. + + +--- +# Hatchery + +## What Does it Do + +Hatchery is used to create workspaces. It contains information about workspaces images and resources set around those images to run. + +## How to Configure it + + +For a full set of configuration see the [helm README.md for hatchery](../helm/hatchery/README.md) or read the [values.yaml](../helm/hatchery/values.yaml) directly + + +``` +hatchery: + enabled: true + image: + repository: + tag: + + + # -- (map) Hatchery sidcar container configuration. + hatchery: + sidecarContainer: + cpu-limit: '0.1' + memory-limit: 256Mi + image: quay.io/cdis/ecs-ws-sidecar:master + + env: + NAMESPACE: "{{ .Release.Namespace }}" + HOSTNAME: "{{ .Values.global.hostname }}" + + args: [] + + command: + - "/bin/bash" + - "./sidecar.sh" + + lifecycle-pre-stop: + - su + - "-c" + - echo test + - "-s" + - "/bin/sh" + - root + + containers: + - target-port: 8888 + cpu-limit: '1.0' + memory-limit: 2Gi + name: "(Tutorials) Example Analysis Jupyter Lab Notebooks" + image: quay.io/cdis/heal-notebooks:combined_tutorials__latest + env: + FRAME_ANCESTORS: https://{{ .Values.global.hostname }} + args: + - "--NotebookApp.base_url=/lw-workspace/proxy/" + - "--NotebookApp.default_url=/lab" + - "--NotebookApp.password=''" + - "--NotebookApp.token=''" + - "--NotebookApp.shutdown_no_activity_timeout=5400" + - "--NotebookApp.quit_button=False" + command: + - start-notebook.sh + path-rewrite: "/lw-workspace/proxy/" + use-tls: 'false' + ready-probe: "/lw-workspace/proxy/" + lifecycle-post-start: + - "/bin/sh" + - "-c" + - export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; + ln -s /data /home/$IAM/pd/; true + user-uid: 1000 + fs-gid: 100 + user-volume-location: "/home/jovyan/pd" + gen3-volume-location: "/home/jovyan/.gen3" +``` + + +## Extra Information + +--- + +# Indexd + +## What Does it Do + +Indexd is a core service of the commons. It is used to index files within the commons, to be used by fence to download data. + +## How to Configure it + +For a full set of configuration see the [helm README.md for indexd](../helm/indexd/README.md) or read the [values.yaml](../helm/indexd/values.yaml) directly + + +```yaml +indexd: + enabled: true + + image: + repository: + tag: + + # default prefix that gets added to all indexd records. + defaultPrefix: "TEST/" + + # Secrets for fence and sheepdog to use to authenticate with indexd. + # If left blank, will be autogenerated. + secrets: + userdb: + fence: + sheepdog: +``` + +## Extra Information + +Indexd is used to hold information regarding files in the commons. We can index any files we want, but should ensure that bucket in indexd are configured within fence, so that downloading the files will work. To index files We have a variety of tools. First, data upload will automatically create indexd records for files uploaded. If we want to index files from external buckets we can also use [indexd-utils](https://github.com/uc-cdis/indexd_utils), or if the commons has dirm setup, create a manifest and upload it to the /indexing endpoint of a commons. From there GUID's will be created and/or assigned to objects. You can view the information about the records by hitting the (commons url)/index/(GUID) endpoint. To test that the download works for these files you will want to hit the (commons url)/user/data/download/(GUID) endpoint, while ensuring you user has the proper access to the ACL/Authz assigned to the indexd record. + +# Manifestservice + +## What Does it Do + +The manifestservice is used by the workspaces to mount files to a workspace. Workspace pods get setup with a sidecar container which will mount files to the data directory. This is used so that users can access files directly on the worskpace container. The files pulled are defined by manifests, created through the export to workspace button in the explorer page. These manifests live in an s3 bucket which the manifestservice can query. + +## How to Configure it + + + +## Extra Information + +--- +# Metadata + +## What Does it Do + +The Metadata Service provides an API for retrieving JSON metadata of GUIDs. It is a flexible option for "semi-structured" data (key:value mappings). + +The GUID (the key) can be any string that is unique within the instance. The value is the metadata associated with the GUID, it’s a JSON blob whose structure is not enforced on the server side. + + + +## How to Configure it + + +``` +manifestservice: + enabled: true + + manifestserviceG3auto: + hostname: testinstall + # -- (string) Bucket for the manifestservice to read and write to. + bucketName: testbucket + # -- (string) Directory name to use within the s3 bucket. + prefix: test + # -- (string) AWS access key. + awsaccesskey: "" + # -- (string) AWS secret access key. + awssecretkey: "" +``` + +## Extra Information + + +--- +# Peregrine + +## What Does it Do + +The peregrine service is used to query data in postgres. It works similar to guppy, but relies on querying postgres directly. It will create the charts on the front page of the commons, as well as the /query endpoint of a commons. + +## How to Configure it + +To configure peregrine we require an entry in the versions block. It also requires a dictionary in the global block. + + +```yaml +``` + + +## Extra Information + + +--- + +# Portal + +## What Does it Do + +Portal is a core service that renders the complete commons webpage, it is the front end service. + +## How to Configure it + +To configure portal we require an entry in the versions block. The portal_app also needs to be defined in the global block. Gitops sets to use the files in the ~/cdis-manifest/(commons url)/ portal directory, dev is the common setup for development environments and [there are default gitops.json](https://github.com/uc-cdis/data-portal/tree/master/data/config) files for most commons that the portal app can be set to. + +```yaml +portal: + enabled: true + + gitops: + # -- (string) multiline string - gitops.json + json: | + {} + # -- (string) - favicon in base64 + favicon: "" + # -- (string) - multiline string - gitops.css + css: | + /* gitops default css */ + # -- (string) - logo in base64 + logo: "" + # -- (string) - createdby.png - base64 + createdby: "" + sponsors: +``` + + +To do this you can follow [the example here](https://github.com/uc-cdis/data-portal/blob/master/docs/portal_config.md). + +Portal can also be configured with different images and icons by updating the values, [similar to this](https://github.com/uc-cdis/cdis-manifest/tree/master/gen3.biodatacatalyst.nhlbi.nih.gov/portal). + +## Extra Information + +--- +# Revproxy + +## What Does it Do + +Revproxy is a core service to a commons which handles networking within the kuberentes cluster. + +## How to Configure it + + + +## Extra Information + +Revproxy is essentially an nginx container, which contains informtation about the endpoints within the cluster. There needs to be an endpoint setup for revproxy to be able to send traffic to it and start normally. Because we have many services that may or may not be setup, we only configure revproxy with the services that are deployed to a commons. The kube-setup-revproxy script will look at current deployments and add configuration files from [here](https://github.com/uc-cdis/cloud-automation/tree/master/kube/services/revproxy/gen3.nginx.conf) to the pod. So if a new service is added, you will need to run kube-setup-revproxy to setup the endpoint. + +--- + +# Sheepdog + +## What Does it Do + +Sheepdog is a core service that handles data submission. Data gets submitted to the commons, using the dictionary as a schema, which is reflected within the sheepdog database. + +## How to Configure it + + +## Extra Information +--- +# Sower + +## What Does it Do + +Sower is a job dispatching service. Jobs are configured with .Values.sowerConfig and sower handles dispatching the jobs. + +## How to Configure it + +```yaml +sower: + enabled: true + sowerConfig: + - name: pelican-export + action: export + container: + name: job-task + image: quay.io/cdis/pelican-export:master + pull_policy: Always + env: + - name: DICTIONARY_URL + valueFrom: + configMapKeyRef: + name: manifest-global + key: dictionary_url + - name: GEN3_HOSTNAME + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname + - name: ROOT_NODE + value: subject + volumeMounts: + - name: pelican-creds-volume + readOnly: true + mountPath: "/pelican-creds.json" + subPath: config.json + - name: peregrine-creds-volume + readOnly: true + mountPath: "/peregrine-creds.json" + subPath: creds.json + cpu-limit: '1' + memory-limit: 12Gi + volumes: + - name: pelican-creds-volume + secret: + secretName: pelicanservice-g3auto + - name: peregrine-creds-volume + secret: + secretName: peregrine-creds + restart_policy: Never + - name: pelican-export-files + action: export-files + container: + name: job-task + image: quay.io/cdis/pelican-export:master + pull_policy: Always + env: + - name: DICTIONARY_URL + valueFrom: + configMapKeyRef: + name: manifest-global + key: dictionary_url + - name: GEN3_HOSTNAME + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname + - name: ROOT_NODE + value: file + - name: EXTRA_NODES + value: '' + volumeMounts: + - name: pelican-creds-volume + readOnly: true + mountPath: "/pelican-creds.json" + subPath: config.json + - name: peregrine-creds-volume + readOnly: true + mountPath: "/peregrine-creds.json" + subPath: creds.json + cpu-limit: '1' + memory-limit: 12Gi + volumes: + - name: pelican-creds-volume + secret: + secretName: pelicanservice-g3auto + - name: peregrine-creds-volume + secret: + secretName: peregrine-creds + restart_policy: Never +``` + +## Extra Information --> diff --git a/docs/INGRESS.md b/docs/INGRESS.md new file mode 100644 index 00000000..cf97466a --- /dev/null +++ b/docs/INGRESS.md @@ -0,0 +1,35 @@ +# Ingress in Gen3 + +# Dev +if `global.dev` is set to true, a very basic ingress is created. + +# AWS + +if `global.aws.` is set to true, there is a special ingress resource that will be created, that has prepoulated annotations for the `alb-controller` to create an alb with similar settings as `cloud-automation`. + +# Custom Ingress + +There is a custom ingress that can be deployed and manipulated. This is a default helm ingress resource, and can be enabled using values like the ones below: + + +``` +revproxy: + ingress: + # Enable the custom ingress resource included by helm. Add any configurations as needed. + enabled: true + # Any annotations that needs to be passed to the ingress resource + annotations: + hosts: + # Replace with your hostname + - host: qureshi.planx-pla.net + paths: + - path: / + pathType: Prefix + tls: + # this is the secret generated by the cert and key from global.tls + # if you have your own secret, reference that. + - secretName: gen3-certs + hosts: + # Replace with your hostname + - qureshi.planx-pla.net +``` \ No newline at end of file diff --git a/docs/PREREQUISITES.md b/docs/PREREQUISITES.md new file mode 100644 index 00000000..8f022d57 --- /dev/null +++ b/docs/PREREQUISITES.md @@ -0,0 +1,94 @@ +# Pre-Requisites + +Before deploying the Gen3 application using Helm, ensure that the following prerequisites are met: + +- Kubernetes cluster with minimum version 1.21. We use [Amazon EKS](#) for our production deployments + +- + +- Postgres 13. + + - We use Amazon aurora serverless v2 for our production deployments. + - **Note:** Managing a database on Kubernetes can be a complex topic, and may result in data loss. We recommend using a managed database service outside of Kubernetes. In production, we use Amazon Aurora Serverless V2 or RDS for Postgres. + - It is possible for development purposes to run Postgres on kubernetes, if you set `global.dev=true` in your `Values.yaml` file, the postgres will be deployed. See [this document](docs/databases.md) for more information on running postgres on kubernetes + + + +- Elasticsearch version 6.8 + - **Note:** Managing elasticsearch on Kubernetes can be a complex topic, and may result in data loss. We recommend using a managed elasticsearch service outside of Kubernetes. In production, we use Amazon Opensearch service for Elasticsearch. + - We are working on supporting newer versions of elasticsearch in gen3, and should soon support ES7/8, but as of now we require version 6.8 + + + + +## Prerequisites + +### Kubernetes cluster +Any kubernetes cluster _should_ work. We are testing with EKS, AKS, GKE. + + +### Postgres + +We recommend managing postgres outside of Gen3 deployments, so your deployments are stateless, and the state of gen3 is managed outside of the helm deployments. This way you may upgrade, or take down the helm deployments, and can restore the state of your gen3 deployment. + +However, there is a possibility to run postgres on kubernetes, alongside gen3 for development purposes. + +To deploy `postgres` and `elasticsearch` that is bundled with gen3 helm charts set the `global.dev` to true. By default these will run with no persistence storage enabled, and are mostly for CI/Development environments. + +If you want to enable persistence for postgres add the following to your values.yaml file for the gen3 chart + +``` +postgresql: + primary: + persistence: + enabled: true +``` + +This will create a [PVC]() for the postgres container. Unfortunately, helm [does not delete PVC on uninstall](https://github.com/helm/helm/issues/5156), so if you enable persistence you might have to manually clean this up between installs by running the following command: + +```bash +kubectl delete pvc data--postgresql-0 +``` + + +**NOTE**: Gen3 will autogenerate the secrets for postgres if you delete and re-install, unless the credentials for each service are supplied via the values.yaml + +In cases where you are using persistent postgres, provide the postgres username and password explicitely using the values.yaml file, so the gen3 deployments will skip generating those credentials. + +Example: + +``` +arborist: + postgres: + dbCreate: true + username: gen3_arborist + password: + +(Repeat for all services) +``` + + + + + + +For a detailed description of each service and it's configuration options see [CONFIGURATION.md](./docs/CONFIGURATION.md) for more information. + + + +We need a postgres database. + +For development/CI clusters an instance of postgres is deployed and automatically configured for you. + +For production environments please provision postgres outside of helm, and fill out these values to provide a master password for postgres. + +``` +global: + postgres: + dbCreate: true + master: + host: insert.postgres.hostname.here + username: postgres + password: + port: "5432" +``` diff --git a/docs/PRODUCTION.md b/docs/PRODUCTION.md new file mode 100644 index 00000000..c15bd388 --- /dev/null +++ b/docs/PRODUCTION.md @@ -0,0 +1,26 @@ +# Gen3 using helm in production + + +The postgres and helm charts are included as conditionals in the Gen3 [umbrella chart](https://helm.sh/docs/howto/charts_tips_and_tricks/#complex-charts-with-many-dependencies) + +``` +- name: elasticsearch + version: "0.1.3" + repository: "file://../elasticsearch" + condition: global.dev +- name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: global.dev + ``` + +### Kubernetes Configmap and Secret Configuration +For the seamless operation of our services, we utilize Kubernetes secrets. To streamline the integration and management of these secrets, we highly recommend deploying External Secret Manager alongside any existing secret management systems you may already have in place. For a comprehensive guide and best practices on implementing External Secrets within our ecosystem, please consult our dedicated External Secrets Documentation available [here](https://github.com/uc-cdis/gen3-helm/blob/master/docs/external_secrets.md). + +Our services also utilize non-secret configuration variables provided via Kubernetes ConfigMaps. For streamlined management, we advise keeping your values.yaml and configuration files files in source control and utilizing ArgoCD for automatic updates and efficient management of your Gen3 Helm chart. + +Each service is designed to seamlessly integrate and manage the combination of Kubernetes secrets and ConfigMaps, ensuring the encapsulated information is effectively injected into the underlying application. + +Please see the diagram provided that details how External Secrets operates. We also mention the use of Argo CD as our choice option for Helm deployments. +![External Secrets Diagram](./images/lucidChart.png "Helm Secrets Manager") + diff --git a/docs/SECRETS.md b/docs/SECRETS.md new file mode 100644 index 00000000..1831d49c --- /dev/null +++ b/docs/SECRETS.md @@ -0,0 +1,2 @@ +# Gen3 Secrets +TBD \ No newline at end of file diff --git a/docs/databases.md b/docs/databases.md index 47d784cb..2a03ecba 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -1,21 +1,37 @@ # Databases in gen3 helm charts -This document will describe how databases are provisioned in gen3 when deploying with helm charts +This document will describe how databases are provisioned, and used in gen3 when deploying gen3 with helm charts. -## Database credentials -The detault behaviour of gen3 helm charts is to auto-generate database credentials and save them as kubernetes secrets. +We hihgly recommend the use of a managed postgres service such as AWS RDS/Aurora, or manage postgres outside of the helm deployment, when deploying gen3 to production environments. -Each service then consumes this same secret and mounts them as ENV vars to access databases. +The bundled version of postgres, that is used for development purposes, is deployed using this helm chart https://bitnami.com/stack/postgresql/helm -You can override this default behaviour by providing postgres credentials through Values.yaml files. +## Database credentials -If you are deploying a dev/CI environment, a postgres server is deployed alongside gen3, and that is used to hold databases for testing. +Every service that requires a postgres database, has the it's credentials stored in a kubernetes secret. + +Example (The secret values have been base64 decoded for documentation purposes): + +```yaml +kubectl get secret fence-dbcreds -o yaml +apiVersion: v1 +kind: Secret +data: + database: fence_gen3 # The default value of this is _ + dbcreated: true # This is updated by the dbCreate job, when a database is created, and configured. + host: gen3-postgresql # Default depends on whether or not `global.dev` is true or false. If it's true this will default to -postgresql. If this is a production deployment, it will look for either `global.postgres.master.host` or `postgres.host` in the Values.yaml + password: example_pass # If not explicitely provided via the values, this is auto-generated. + port: 5432 # Defaults to 5432, will read from `global.postgres.master.port` or `postgres.port` for ovverrides. + username: fence_gen3 # Defaults to _. Will look for overrides in `postgres.username`. +``` -For production deployments you need to provide the master credentials for a postgres server through these values. +Each service then consumes this same secret and mounts them as ENV vars to access databases. + +For production deployments you must at minimum provide the master credentials for a postgres server through these values. ``` global: postgres: - db_create: true + dbCreate: true master: host: insert.postgres.hostname.here username: postgres @@ -24,23 +40,20 @@ global: ``` -These values will then be used to provision databases for the environment. +These values can then be used to provision and configure databases for the gen3 environment. ## Automatic database creation through jobs -When deploying gen3 helm charts you need to specifiy a postgres server. For dev/CI environments an installation of postgres is included, and is not intended for use in production. - -We hihgly recommend the use of a managed postgres service such as RDS when deploying gen3 to cloud environments. -The dev/ci postgres is deployed using this helm chart https://bitnami.com/stack/postgresql/helm -If you set the `global.postgres.db_create` value to true, then a job is kicked off for each service that relies on postgres to provision databases. +If you set the `global.postgres.dbCreate` value to true, then a job is kicked off for each service that relies on postgres to provision databases. This will kick off a [database creation job](../helm/common/templates/_db_setup_job.tpl) + -## Database restoration. +## Database restoration. (BETA) There is a job to restore dummy data for Postgres and Elasticsearch to speed up setting up ephemeral enviornments for testing purposes, and to avoid running expensive ETL jobs in CI to have a fully featured gen3 environment In the future this job may be used to set up fully tested production environments, negating the need to run ETL in production, and have all your databases tested before doing a data-release. diff --git a/docs/etl.md b/docs/etl.md new file mode 100644 index 00000000..946438e8 --- /dev/null +++ b/docs/etl.md @@ -0,0 +1,29 @@ +# ETL + +The Gen3 Tube ETL is designed to translate data from a graph data model, stored in a PostgreSQL database, to indexed documents in ElasticSearch (ES), which supports efficient ways to query data from the front-end. The purpose of the Gen3 Tube ETL is to create indexed documents to reduce the response time of requests to query data. It is configured through an etlMapping.yaml configuration file, which describes which tables and fields to ETL to ElasticSearch. + + +You can configure the ETL like this: + +```yaml +etl: + enabled: true + esEndpoint: "" + etlMapping: + +``` + +To kick off etl job run this command: + +```bash +kubectl create job --from=cronjob/etl-cronjob etl +``` + +If you already have a job called etl run the following. This will delete the old job and create a new instance. + +```bash +kubectl delete job etl +kubectl create job --from=cronjob/etl-cronjob etl +``` + +For more information about our ETL read [here github.com/uc-cdis/tube](https://github.com/uc-cdis/tube) \ No newline at end of file diff --git a/docs/external_secrets.md b/docs/external_secrets.md new file mode 100644 index 00000000..cf4dd5ca --- /dev/null +++ b/docs/external_secrets.md @@ -0,0 +1,179 @@ +# External Secrets Operator + + "External Secrets Operator" is a tool that was created by the Kubernetes community to manage external secrets in a Kubernetes cluster. It allows you to fetch and sync external secret values from various external secret management systems into Kubernetes secrets. One of the external secret management systems it can connect to is AWS Secrets Manager. AWS Secrets Manager allows for the secure storing of your secrets as well as the ability to periodically and automatically rotate your secrets. + +This document will guide you through setting up the essential resources to access your secrets in AWS Secrets Manager and download the External Secrets Operator Helm chart. This way, you can effectively utilize your stored secrets with Helm. + +## Download External Secrets Operator and Create Resources in AWS. +You can use the following Bash script to apply the External Secrets Operator to your cluster and create the necessary AWS resources. Fill in the variables below to get started: + +***Notice: +The Gen3 Helm chart has various jobs and uses for an IAM user. To enhance code reusability, we've implemented the option for jobs and services to share the same AWS IAM global user. If you would like to use the same IAM user for External Secrets and jobs like ["Fence Usersync"](fence_usersync_job.md) or our "AWS ES Proxy Service", you can follow [THIS](global_IAM_helm_user.md) guide that details how to setup a Helm global user. In case you opt for a global IAM user, please comment out the "create_iam_policy" and "create_iam_user" functions at the end of the script.*** + +``` +#!/bin/bash + +AWS_ACCOUNT="" +region="us-east-1" +iam_policy="external_secrets_policy" +iam_user="external_secrets_user" + +helm_install() +{ + echo "# ------------------ Install external-secrets via helm --------------------------#" + helm repo add external-secrets https://charts.external-secrets.io + helm install external-secrets \ + external-secrets/external-secrets \ + -n external-secrets \ + --create-namespace \ + --set installCRDs=true +} + +create_iam_policy() +{ + echo "# ------------------ create iam policy for aws secrets manager --------------------------#" + POLICY_ARN=$(aws iam create-policy --policy-name $iam_policy --policy-document '{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "secretsmanager:ListSecrets", + "secretsmanager:GetSecretValue" + ], + "Resource": [ + "*" + ] + } + ] + }') + + iam_policy_arn=$(aws iam list-policies --query "Policies[?PolicyName=='$iam_policy'].Arn" --output text) + echo "Policy Arn: $iam_policy_arn" + # return $iam_policy_arn +} + +create_iam_user() +{ + echo "# ------------------ create user $iam_user --------------------------#" + aws iam create-user --user-name $iam_user + + echo "# ------------------ add iam user $iam_user to policy $iam_policy --------------------------#" + aws iam attach-user-policy --user-name $iam_user --policy-arn $iam_policy_arn + echo "aws iam attach-user-policy --user-name $iam_user --policy-arn $iam_policy_arn" + + echo "# ------------------ create access key and secret key for external-secrets --------------------------#" + aws iam create-access-key --user-name $iam_user > keys.json + access_key=$(jq -r .AccessKey.AccessKeyId keys.json) + secret_key=$(jq -r .AccessKey.SecretAccessKey keys.json) + kubectl create secret generic "$iam_user"-secret --from-literal=access-key=$access_key --from-literal=secret-access-key=$secret_key + rm keys.json +} + +helm_install +#comment out the below if using global iam user. +create_iam_policy +create_iam_user +``` + +***Please note that Terraform for the creation and population of Gen3 Secrets in AWS Secrets Manager is in development currently. This Terraform will also create the Iam user and policies necessary to access these secrets.*** + +## Enabling External Secrets in Helm charts +Our Helm architecture includes a comprehensive [umbrella](https://github.com/uc-cdis/gen3-helm/tree/master/helm/gen) chart designed to streamline the deployment of external secrets across both the umbrella chart itself and its associated subcharts. By configuring the `.Values.global.externalSecrets.deploy` setting within this umbrella chart, users can effortlessly initiate the deployment of all related external secret resources. This includes the external secret resources within the subcharts and the secret store required for their management. + +#### Global Deployment of External Secrets +Upon deployment of the umbrella chart, the `.Values.global.externalSecrets.deploy` setting automatically provisions external secret resources for every subchart. This occurs regardless of the individual external secrets deployment settings within subcharts, even if they are explicitly set to `false`. This feature ensures a unified approach to secret management across all components of the architecture. + +#### Selective Secret Management +For users requiring a more selective application of external secrets — targeting specific secrets while excluding others — the system is designed to accommodate such scenarios with ease. + +External secret resources will only attempt to replace Kubernetes secrets when a corresponding secret is successfully located within the Secrets Manager. In instances where a specific secret is not found, the External Secrets resource will indicate a `SecretSyncedError`, signaling the absence of the targeted resource within the Secrets Manager. This error is acceptable and helpful for users who want to enable the use of AWS Secrets Manager for some, but not all the secrets in a specific Helm chart. + +However, if you wish to utilize External Secrets for managing non-database secrets while still automating the creation of your database secrets, you can configure this behavior explicitly. Set `.Values.global.externalSecrets.dbCreate` to true alongside `.Values.global.postgres.dbCreate` or `.Values.postgres.dbCreate` to initiate the database creation job. This configuration will result in the creation of the necessary databases with their credentials stored securely within Kubernetes Secrets. Subsequently, you also choose to create Secrets in Secrets manager with the values that were generated from the dbCreate job if you wish to store these credentials long term. + +By default, the following services will not create the Helm internal secrets when Secrets Manager is enabled: +- Audit +- Fence +- Indexd +- Manifestservice +- Metadata + +This is because CD tools like Argocd will have trouble syncing resources if the K8s secret was generated via Helm and External Secrets continues to override it. You can configure Helm to still create these secrets with External Secrets enabled by setting the appropriate variable to true. + +For example, to ensure the "audit-g3auto" secret is still created by Helm, you would need to set the following in your values.yaml file: +``` +audit: + # -- (map) External Secrets settings. + externalSecrets: + # -- (string) Will create the Helm "audit-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sAuditSecret: true +``` + +#### Independent Subchart Deployment +In scenarios where subcharts are deployed independently, outside the scope of the umbrella chart, it is crucial to set the `.Values.global.externalSecrets.deploy` directive within the `values.yaml` file for each specific service. + +Additionally, to facilitate the creation of a Secret Store capable of authenticating with AWS Secrets Manager, the `.Values.global.externalSecrets.separateSecretStore` should be set to true in the relevant charts. This configuration is essential for establishing proper authentication mechanisms for secret retrieval. + +#### Configuring Separate Secret Stores +The `.Values.global.externalSecrets.separateSecretStore` setting can also be applied within the context of the umbrella chart deployment. Utilizing this setting allows for the creation of distinct Secret Stores dedicated to individual services. This approach is particularly beneficial for environments where it is preferable to limit access to Secrets Manager, ensuring that services only have access to the secrets explicitly required for their operation. + +## Helm IAM User +If you are using a separate IAM user for AWS Secrets Manager please follow the below instructions: + +This script Bash script at the beginning of this document should have created a secret titled "NameofIAMuser-user-secret" in your cluster. You will need to retrieve these values to input into your Helm chart for the Secret Store to authenticate with AWS Secrets Manager. + + +Access Key: +``` +kubectl get secret "your secret name" -o jsonpath="{.data.access-key}" | base64 --decode +``` + + +Secret Access Key: +``` +kubectl get secret "your secret name" -o jsonpath="{.data.secret-access-key}" | base64 --decode +``` + +You can paste the IAM access key and secret access key in the `.Values.secrets.awsAccessKeyId`/`.Values.secrets.awsSecretAccessKey` fields in the values.yaml file for the chart(s) you would like to use external secrets for. + +If you are deploying external secrets with the Gen3 umbrella chart, you can utilize a local secret to avoid pasting credentials in the values.yaml file. Just set `.global.aws.useLocalSecret.enabled` to true and supply your secret name. + +Please note that only some Helm charts are compatible with External Secrets currently. We hope to expand this functionality in the future. If a chart is able to use External Secrets, you can see a `.Values.externalSecrets` section in the values.yaml file. + +## How External Secrets Works. +External Secrets relies on three main resources to function properly. (The below have links to examples of each resource) +1. [Aws-config](https://github.com/uc-cdis/gen3-helm/blob/master/helm/common/templates/_aws_config.tpl)- Contains Access and Secret Access keys used by the Cluster Secret Store to authenticate with AWS Secrets Manager +2. [Secret Store](https://github.com/uc-cdis/gen3-helm/blob/master/helm/common/templates/_external_secrets.tpl#L41-L62)- Resource to Authenticate with AWS Secrets Manager +3. [External Secret](https://github.com/uc-cdis/gen3-helm/blob/master/helm/common/templates/_external_secrets.tpl#L15-L38)- References the Secret Store and is used as a "map" to tell External Secrets Operator what secret to grab from External Secrets and the name of the Kubernetes Secret to create locally. + + Anatomy of an ExternalSecret: + ``` + apiVersion: external-secrets.io/v1beta1 + kind: ExternalSecret + metadata: + # Name of the External Secret resource + name: audit-g3auto + spec: + #How often to Sync with AWS Secrets Manager + refreshInterval: 5m + secretStoreRef: + # The name of the Secret Store to use. + name: {{include "cluster-secret-store" .}} + kind: SecretStore + target: + # What Kubernetes secret to create from the secret pulled from AWS Secrets Manager. + name: audit-g3auto + creationPolicy: Owner + data: + # the key inside the new Kubernetes secret + - secretKey: audit-service-config.yaml + remoteRef: + #name of secret in AWS Secrets Manager + key: {{include "audit-g3auto" .}} + ``` + +## Customizing the AWS Secrets Manager Secrets Name. +When pulling a secret from AWS Secrets Manager, you want to ensure that the External Secret resource is referencing the proper name of the secret in AWS Secrets Manager. +You can customize the name of the secret to pull from in the `.Values.externalSecrets` section of a Chart. You can see the name for the confiugrable secrets in a chart by looking in this section as well. + +Any string you put in this section will override the name of the secret that is pulled from AWS Secrets Manager NOT the name of the Kubernetes secret that is created from the External Secret resource. diff --git a/docs/fence_usersync_job.md b/docs/fence_usersync_job.md new file mode 100644 index 00000000..b01f170f --- /dev/null +++ b/docs/fence_usersync_job.md @@ -0,0 +1,70 @@ +# Fence Usersync CronJob + +If `.Values.usersync.usersync` is set to true, the Fence usersync-cron.yaml will be deployed to the cluster, otherwise useryaml job will be deployed instead of usersync. + +User lists can be synced from three sources: + +1. A ftp/sftp server that hosts user csv files that follows the format provided by dbgap, enabled if `.Values.usersync.syncFromDbgap` is set to "true". Please follow the [Sftp Setup](#sftp-setup) guide before enabling this option. + +2. A user.yaml file that is pulled from the S3 bucket specified in the `.Values.usersync.userYamlS3Path` field is used to update fence's user-access database. Please note an IAM policy with S3 read is required for this option. Please follow [S3 user.yaml Setup](#s3-setup) guide below. + +3. If the `.Values.usersync.userYamlS3Path` string is set to "none", the user.yaml file specified in the fence values.yaml [HERE](https://github.com/uc-cdis/gen3-helm/blob/c7b8959cdf5f7756b29c33ff330923e95981827c/helm/fence/values.yaml#L449-L1319) will be used. + + + +# S3 user.yaml Setup {#s3-setup} +Please see [this](https://github.com/uc-cdis/fence/blob/master/docs/additional_documentation/user.yaml_guide.md) documentation that details user.yaml formatting. + +You can pull this file from an S3 bucket that is set in the `.Values.usersync.userYamlS3Path` field. Then input the IAM credentials for a user that has read access to the specified S3 bucket in the `.Values.secrets.awsAccessKeyId` and `.Values.secrets.awsSecretAccessKey` fields. + +You can utilize a local secret to avoid pasting credentials in the values.yaml file. Just set `.global.aws.useLocalSecret.enabled` to true and supply your secret name. + +***Notice: +The Gen3 Helm chart has various jobs and uses for an IAM user. To enhance code reusability, we've implemented the option for jobs and services to share the same AWS IAM global user. If you would like to use the same IAM user for Fence Usersync, External Secrets, etc.- you can follow [THIS guide that details how to setup a Helm global user](global_iam_helm_user.md).*** + +As previously mentioned, if the `.Values.usersync.userYamlS3Path` string is set to "none", the user.yaml file from Fence values.yaml will be used. + + + +# Dbgap +## Sftp Setup {#sftp-setup} +You can configure one or more dbGaP SFTP servers to sync telemetry files from. To configure one single dbGaP server, add credentials and information to the fence-config.yaml under dbGaP, [this is outlined here](https://github.com/uc-cdis/gen3-helm/blob/c7b8959cdf5f7756b29c33ff330923e95981827c/helm/fence/values.yaml#L1796). + +To configure additional dbGaP servers, include in the config.yaml a list of dbGaP servers under dbGaP, like so: + +``` +dbGaP: +- info: + host: + username: + password: + ... + protocol: 'sftp' + ... + ... +- info: + host: + username: + ... +```` + +You can find more detailed information on the setup with examples [here](https://github.com/uc-cdis/fence/blob/master/docs/additional_documentation/usersync.md). + +For an example of a dbGap auth file (csv), please see [this](https://github.com/uc-cdis/fence/blob/master/docs/additional_documentation/usersync.md#example-of-dbgap-authorization-file-csv-format) example for formatting. + +## Dbgap Options + Set `.Values.usersync.addDbgap` to "true" to attempt a dbgap sync and fall back on user.yaml. + + Set `.Values.usersync.onlyDbgap` to "true" to run only a dbgap sync and ignore the user.yaml. + +## Slack Options + Set `.Values.usersync.slack_webhook` to configure a webhook endpoint to be used for regular usersync updates to Slack. + + Set `.Values.usersync.slack_send_dbgap` to "true" to echo the files that are being seen on dbgap ftp to Slack. + + + +## Other Customizations + The `.Values.usersync.schedule` option can be set to customize the cron schedule expression. The default setting is to have the job run once every 30 minutes. + + The `.Values.usersync.custom_image` can be set to override the default "awshelper" image for the init container of the userync cronjob. diff --git a/docs/gen3_developer_environments.md b/docs/gen3_developer_environments.md deleted file mode 100644 index 81994149..00000000 --- a/docs/gen3_developer_environments.md +++ /dev/null @@ -1,199 +0,0 @@ -# Running Gen3 On A Laptop, For Devs - -Welcome to Gen3! - -If you're reading this, I assume you're a developer looking to get started working with Gen3. If so, this guide will help you get an instance of Gen3 up and running on your laptop, which will let you work on Gen3 from anywhere you can write code, no other setup needed. - - -# Kubernetes -Gen3 runs entirely on Kubernetes. Kubernetes is a container orchestrator, and you'll often see it referred to as k8s, or 'k', followed by the 8 letters in 'ubernete', followed by 's'. It is responsible for managing the lifecycle, storage, and networking for a collection of containers, which are packaged into discrete units called "pods". - -If you're not familiar with containers, they're a set of technologies that allow you to run code inside isolated environments on your machine. This provides benefits such as allowing you to manage "machnes" that only exist in software, instead of physical hardware, and isolating applications from each other, to prevent failures from affecting other applications or even all applications on your machine. These are the basic units software that Kubernetes starts up, stops, and schedules, in order to match a state that you define and provide it. - -Kubernetes is primarly meant to run on cloud services, and the big 3 (Amazon Web Services, Microsoft Azure, and Google Cloud) all have robust offerings. While it is designed to run primarily on the cloud, for the sake of developers' ability to work almost anywhere, there are versions (also called flavors) of Kubernetes designed primarily with the laptop or local desktop in mind. The next section will introduce you to the one we'll recommend you use, Rancher Desktop. However you can use another flavor if you think it works better, so long as you can follow the rest of these directions while using them. - -### Installing `kubectl` -Kubectl (I pronounce it cube-cuddle, but others say it differently) is an application that allows you to interface with and control a Kubernetes cluster. In this context, "cluster" simply refers to a group of machines, which can be a group of one, that work together to manage containers. - -It will be an important tool for you as you get more comfortable working with Kubernetes, so we'll install it now. It is a highly-configurable tool that you can install in a lot of ways, so rather than tell you how to do it, we'll let you pick the method that works best for your platform: [Windows](https://kubernetes.io/docs/tasks/tools/install-kubectl-windows) [Mac](https://kubernetes.io/docs/tasks/tools/install-kubectl-macos) [Linux](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux). - -Once you have kubectl installed, you can verify by running `kubectl`. The output should be a help guide. - -### Installing `helm` -Helm is a package manager for Kubernetes that makes it easy to install, upgrade, and manage applications on a Kubernetes cluster. It simplifies the process of installing and configuring complex applications by providing a set of pre-configured templates and options. In this guide, we will show you how to install the Helm command-line interface (CLI) on your machine. - -The installation steps for Helm are rather straightforward. If you're a Homebrew user on Mac, you can use the command `brew install helm` to get it on your machine. If not, head over to the Helm website [here](https://helm.sh/docs/intro/install/) and follow the instructions for your setup. You'll know you've set it up correctly if the output of running the command `helm list` looks like this: - -![image](images/succesfulHelmOutput.png) - -This means that Helm was able to connect to your Rancher k8s cluster, and will be able to install Gen3 in the next step. - - -### Kubernetes on your laptop -There are several ways to run Kubernetes on your laptop, depending on your needs and the resources available on your machine. Some of the most popular options include: - -**Minikube:** Minikube is a lightweight Kubernetes distribution that runs a single-node cluster on your laptop. It is easy to set up and is well suited for local development and testing. - -**Docker for Desktop:** Docker for Desktop includes built-in support for Kubernetes, allowing you to run a single-node cluster on your laptop using the Docker engine. This option is also easy to set up and is well suited for local development and testing. - -**k3s:** k3s is a lightweight Kubernetes distribution that is designed to run on resource-constrained environments. It is a great option for running Kubernetes on your laptop if you have limited resources or need to run multiple clusters. - -**Kind (Kubernetes in Docker):** Kind is a tool for running local Kubernetes clusters using Docker container “nodes”. It creates a cluster by starting multiple Docker containers on the local host. - -**Microk8s:** Microk8s is a fast and efficient Kubernetes distribution that is easy to install and run on a local machine. It uses snaps to package and distribute Kubernetes, making it a great option for users on Ubuntu and other Linux distributions. - -**Vagrant and Virtualbox:** Vagrant is a tool that enables you to create and configure lightweight, reproducible, and portable development environments. You can use it to create a virtual machine running Kubernetes on your laptop using Virtualbox. - -Each of these methods has its own advantages and disadvantages, so you should choose the one that best fits your needs. - -Another option to run Kubernetes on your laptop is **Rancher Desktop.** - -**Rancher Desktop** is an easy-to-use, all-in-one Kubernetes platform that runs on your local machine. It provides a simple and intuitive UI for managing your local Kubernetes cluster, and includes built-in support for Ingress, cert-manager, and other popular add-ons. Rancher Desktop also comes with a built-in Kubernetes dashboard and a set of tools for managing and monitoring your cluster. - -**Rancher Desktop** is our preferred way of running Kubernetes on a laptop, because it provides a user-friendly interface and comes with many pre-configured components, which make it easy to set up and manage your cluster. Additionally, it works well on M1 macbooks, which have new ARM-based processors. - - -### Installing Rancher Desktop -This guide is primarily written with Mac users in mind, but most Linux users should be able to install Rancher Desktop using their distro's package manager. If you're on Mac and using Homebrew, you can type `brew install --cask rancher`. If not, install directions for Linux, Mac, and Windows are available at [the Rancher website.](https://docs.rancherdesktop.io/getting-started/installation/#macOS) Once you have Rancher succesfully installed and the application opened, we can go over how to get your Kubernetes cluster ready for Gen3! - -### Configuring Rancher Desktop -![image](images/rancherReadyForSetup.png) - -Once you can see a blank screen like this, you are ready to begin. If you can't get a screen similar to this, without any warnings, reach out to a Gen3 resource, either the community, or the platform team if you work directly for us. Now, we're going to make a few small tweaks to help Kubernetes run better. - -Click on the gear icon in the top right of your window, then navigate to "virtual machine." These settings control the VM that Kubernetes is going to run on on your laptop, and so striking a the right balance between performance and resource usage is key. - -This guide was developed by people mostly using M1 Macbook Pros with 16GB of RAM and and 8 CPU cores. In a similar situation, this guide recommends allocating half of each (so 8GB of RAM/4 CPU cores) to allow you to run other applications while still deploying all of our services quickly. - -Once you've settled on a CPU and RAM allocation, click on the "Kubernetes" tab. Make sure that Kubernetes is enabled, and the version is set appropriately (if you're not sure, just leave it default). - -Now that you have these steps out of the way, in our next step, we'll install Helm onto our laptop. Helm is a tool for packaging Kubernetes services, much like a Linux package manager or Homebrew for Mac. This will allow us to more easily install Gen3 onto our laptops. - - - -### Installing Gen3 -The first step to installing Gen3 is adding the Gen3 Helm repository. This is just how we package up all the components that make up Gen3, and make them accessible to the public. - -The command to do this is: - -``` -helm repo add gen3 http://helm.gen3.org -helm repo update -``` - -Once you have your repo added, you can install it with the command - -``` -helm upgrade --install dev gen3/gen3 -``` - -If you want to provide overrides you can do so by passing in one, or several values.yaml files. F.ex if you want to pass in user.yaml and fence-config (NB! New format, check out sample files in [this](../sample-values/) folder) - -``` -helm upgrade --install dev gen3/gen3 -f values.yaml -f fence-config.yaml -f user.yaml -``` - - - -See example files: -- [values.yaml](../sample-values/values.yaml) -- [fence-config.yaml](../sample-values/fence-config.yaml) -- [user.yaml](../sample-values/user.yaml) - - -You can combine it all in a single file too if that's easier. - -This command calls out to the repository you created before, named `gen3`, and grabs an "umbrella chart" containing all the services needed to run Gen3. - -Confusingly, this umbrella chart is also called `gen3`, and these two parts combine to form the `gen3/gen3` in the command you see. - -The first `dev` in that command refers to the "release name," or what Helm will call the deployment of Gen3 on your laptop. If that command runs successfully, you will see an output like this: - -![image](helmSuccesfulGen3Install.png) - -### Accessing Gen3 -If everything went well with deploying you should now have an ingress resource, listening for the hostname you provided. - -``` -kubectl get ingress -``` - -If you used Rancher Desktop, and used localhost as your hostname, you should be able to access your application at `https://localhost/` - - -# Troubleshooting - -## Error: couldn't find key dbcreated in Secret default/\*-dbcreds - -This is by design, it’s waiting for your dbcreate job(s) to finish which signals that the services can start. - -## Local Dev Linux (Ubuntu) / Rancher Desktop Problems: - -If you've followed the instructions, see services running, but can't go to `https://localhost` to see portal... keep reading. - -If you see an Apache webpage at `localhost` (installed by default in Ubuntu), you have to kill the Apache service. - -``` -sudo systemctl stop apache2 -``` - -If things still aren't working, reference [this](https://github.com/rancher-sandbox/rancher-desktop/issues/1668), which suggests trying this: - -``` -sudo sysctl net.ipv4.ip_unprivileged_port_start -sudo sysctl net.ipv4.ip_unprivileged_port_start=80 -``` - -Then restart Rancher and try again. - -If things still aren't working, double check your `values.yaml` and make sure you don't have anything in the `global` block (if you're doing local development). Just let it use the defaults (so don't put anything in `values.yaml` for `global`). - - -## Elasticsearch error: - -When you deploy the elasticsearch chart to Rancher Desktop you may see this error and elasticsearch failing to start: - -``` -ERROR: [1] bootstrap checks failed -[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144] -``` - -To fix it do the following: - -Get a shell to the VM running k8s/docker in rancher -``` -rdctl shell -``` - -Run this: - -``` -sudo sysctl -w vm.max_map_count=262144 -``` - -If you want to set this permanently, you need to edit `/etc/sysctl.conf` and set `vm.max_map_count` to `262144` . Remember to do this inside the `rdctl shell` too. - -When the host reboots, you can verify that the setting is still correct by running - -``` -sysctl vm.max_map_count -``` - - -# Cool kubernetes tools -Another way to interact with and manage a Kubernetes cluster is by using command-line tools, such as k9s. - -K9s is a terminal-based tool that provides a simple and intuitive UI for interacting with your local Kubernetes cluster. It includes features such as pod management, resource monitoring, and log viewing. k9s also provides a live view of your cluster, making it easy to identify and troubleshoot issues. - -Other similar tools for developers include: - -- Stern: a multi-pod and container log tailing for Kubernetes -- Kube-ps1: a Kubernetes prompt for bash and zsh -- Kube-shell: An integrated shell for working with the Kubernetes CLI -- Skaffold: a command line tool that facilitates continuous development for Kubernetes applications. - - -These command-line tools can be a great option for developers who prefer to work in the terminal and want a more streamlined and efficient way to interact with their cluster. They are lightweight, easy to install and have a small footprint. They provide a simple and efficient way to manage and monitor your cluster. - - - diff --git a/docs/global_iam_helm_user.md b/docs/global_iam_helm_user.md new file mode 100644 index 00000000..e2a35d9c --- /dev/null +++ b/docs/global_iam_helm_user.md @@ -0,0 +1,55 @@ +# AWS IAM Global User + +For Helm code reusability, we have added the functionality to use one IAM user for various jobs/services. + +We are currently in the process of integrating this user into our Terraform code. In the meantime, you can manually create a global user by referring to this guide. + +## What this user can do in Helm +- Fence Usersync Job +- ES Index Restore +- Restore PGdump +- External Secrets +- AWS ES Proxy Service + + + +Example policy containing all the proper permissions: +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": [ + "arn:aws:s3:::$BUCKET/$ENVIRONMENT/*", + # Fence Usersync Job: Name of the userYamlS3Path containing the user.yaml file + "arn:aws:s3:::$BUCKET/$ENVIRONMENT/$VERSION/elasticsearch/*", + # ES Index Restore Job: Name of the dbRestoreBucket with the proper path to the ES dump files. + "arn:aws:s3:::$BUCKET/$ENVIRONMENT/$VERSION/pgdumps/*" + # DB PG Dump Restore Job: Name of the dbRestoreBucket with the proper path to the SQL dump files. + ] + }, + { + "Effect": "Allow", + "Action": [ + "secretsmanager:ListSecrets", + "secretsmanager:GetSecretValue" + ], + "Resource": [ + "*" + # External Secrets: Leave as is to allow External Secrets access to your secrets in Secrets Manager. + ] + }, + { + "Effect": "Allow", + "Action": "es:*", + "Resource": "arn:aws:es:REGION:ACCOUNT_ID:domain/CLUSTER_NAME/*" + # AWS ES Proxy Service: Arn of your Elasticsearch Cluster in AWS. + } + ] +} +``` + +## After Creating the User +In order to integrate the user in Helm, paste in the values of your Access and Secret Access key in `.Values.global.aws.awsAccessKeyId` and `.Values.global.aws.awsSecretAccessKey` \ No newline at end of file diff --git a/docs/images/lucidChart.png b/docs/images/lucidChart.png new file mode 100644 index 00000000..dbb986c4 Binary files /dev/null and b/docs/images/lucidChart.png differ diff --git a/docs/kubernetes-in-docker.md b/docs/kubernetes-in-docker.md new file mode 100644 index 00000000..91707a20 --- /dev/null +++ b/docs/kubernetes-in-docker.md @@ -0,0 +1,115 @@ +# Gen3 in KIND +## Kind (Kubernetes IN Docker) + +### Overview +KIND runs Kubernetes inside a Docker container, making it an excellent choice for local development and testing. It is also used by the Kubernetes team to test Kubernetes itself. + +### Pros: + +Fast cluster creation (around 20 seconds). +Robust and reliable, thanks to containerd usage. +Suitable for CI environments (e.g., TravisCI, CircleCI). + +### Cons: + +Ingress controllers needs to be deployed manually + + + + +# Step 1. Create cluster + +```bash +cat < OAuth client ID. + +Select the Web application application type. Name your OAuth 2.0 client and click Create. + +For Authorized Javascript Origins add https:// + +For "Authorized redirect URIs" add https:///user/login/google/login/ + +After configuration is complete, take note of the client ID that was created. You will need the client ID and client secret to complete the next steps. + +## Prepare values.yaml + +Create a file called values.yaml and populate it like this (This is the main way of configuring gen3. this is just some default values that will help you get started) + +```yaml +global: + # This can be anything you want! + hostname: dev.planx-pla.net + +fence: + FENCE_CONFIG: + OPENID_CONNECT: + google: + client_id: "" + client_secret: "" + +# Use a prebuilt portal image if you're deploying to a laptop, less resources consumed by gen3 +portal: + resources: + requests: + cpu: "0.2" + memory: 100Mi + image: + repository: quay.io/cdis/data-portal-prebuilt + tag: dev + +``` + +## deploy gen3 + +```bash +helm repo add gen3 http://helm.gen3.org +helm upgrade --install gen3 gen3/gen3 -f ./values.yaml +``` \ No newline at end of file diff --git a/docs/portal/prebuild-portal.md b/docs/portal/prebuild-portal.md index b352c945..f6e71727 100644 --- a/docs/portal/prebuild-portal.md +++ b/docs/portal/prebuild-portal.md @@ -15,7 +15,7 @@ This documentation will provide instructions on how to set up a static Gen3 Port Setup the configuration locally on your machine. -The dockerfile expects your portal configuration under the configurations folder. See exapmle for `dev.planx-pla.net` +The dockerfile expects your portal configuration under the configurations folder. See example for `dev.planx-pla.net` ``` configurations @@ -36,7 +36,10 @@ https:///api/v0/submission/_dictionary/_all **Hint:** both of these are served via sheepdog service -Use the provided Dockerfile as a template for building your container. +Use the provided Dockerfile as a template for building your container. Update: +``` +ARG PORTAL_HOSTNAME= +``` Build your container using the following command inside the same folder as the Dockerfile: @@ -44,6 +47,10 @@ Build your container using the following command inside the same folder as the D docker build -t : . ``` +If you are using localhost, you will need to add the `--network="host"` option in the above command, e.g: +``` +docker build -t : . --network="host" +``` Push the container to your repository using the command: ``` @@ -54,6 +61,26 @@ Update the image and tag in the Gen3 Portal configuration to use the new contain Note: Make sure to replace the with the actual name you want to give to your image. +Update or create the `values.yaml`, for example: +``` +global: + dev: true + hostname: localhost + +portal: + image: + repository: + tag: + resources: + requests: + cpu: 0.2 + memory: 500Mi +``` + +Update helm charts +``` +helm upgrade --install dev gen3/gen3 -f values.yaml +``` # Conclusion: Using a static Gen3 Data Portal can significantly improve the performance of the Gen3 Portal by pre-running the WebPack build and creating static files, which are then served using nginx. diff --git a/examples/aws_dev_values.yaml b/examples/aws_dev_values.yaml new file mode 100644 index 00000000..a579069c --- /dev/null +++ b/examples/aws_dev_values.yaml @@ -0,0 +1,61 @@ +global: + # Deploys aws specific ingress + aws: + enabled: true + environment: devplanetv2 + # Deploys elasticsearch and postgres in k8s + dev: true + # Replace with your dev environment url. + hostname: qureshi.planx-pla.net + # this is arn to a certificate in AWS that needs to match the hostname. + # This one is for *.planx-pla.net + revproxyArn: arn:aws:acm:us-east-1:707767160287:certificate/520ede2f-fc82-4bb9-af96-4b4af7deabbd + + +# configuration for fence helm chart. You can add it for all our services. +fence: + # Override image + image: + repository: quay.io/cdis/fence + tag: master + + # Fence config overrides + FENCE_CONFIG: + APP_NAME: 'Gen3 Data Commons' + # A URL-safe base64-encoded 32-byte key for encrypting keys in db + # in python you can use the following script to generate one: + # import base64 + # import os + # key = base64.urlsafe_b64encode(os.urandom(32)) + # print(key) + ENCRYPTION_KEY: REPLACEME + + DEBUG: True + OPENID_CONNECT: + google: + client_id: "" + client_secret: "" + + AWS_CREDENTIALS: + 'fence-bot': + aws_access_key_id: '' + aws_secret_access_key: '' + + S3_BUCKETS: + # Name of the actual s3 bucket + jq-helm-testing: + cred: 'fence-bot' + region: us-east-1 + + # This is important for data upload. + DATA_UPLOAD_BUCKET: 'jq-helm-testing' + + + +# -- (map) To configure postgresql subchart +# Persistence is disabled by default +postgresql: + primary: + persistence: + # -- (bool) Option to persist the dbs data. + enabled: true diff --git a/examples/gke_dev_values.yaml b/examples/gke_dev_values.yaml new file mode 100644 index 00000000..8011210c --- /dev/null +++ b/examples/gke_dev_values.yaml @@ -0,0 +1,69 @@ +global: + # to disable local es/postgre + dev: true + hostname: qureshi.planx-pla.net + tls: + cert: + key: + # Postgres instance that is managed outside of helm + postgres: + master: + host: "postgres-host-address" + username: "postgres" + password: "postgres-password" + + +# configuration for fence helm chart. You can add it for all our services. +fence: + # Fence config overrides + FENCE_CONFIG: + OPENID_CONNECT: + google: + client_id: "" + client_secret: "" + + AWS_CREDENTIALS: + 'fence-bot': + aws_access_key_id: '' + aws_secret_access_key: '' + + S3_BUCKETS: + # Name of the actual s3 bucket + jq-helm-testing: + cred: 'fence-bot' + region: us-east-1 + + # This is important for data upload. + DATA_UPLOAD_BUCKET: 'jq-helm-testing' + + + +revproxy: + ingress: + # Enable the default ingress included by helm. Add any configurations as needed. + enabled: true + hosts: + # Replace with your hostname + - host: qureshi.planx-pla.net + paths: + - path: / + pathType: Prefix + tls: + # this is the secret generated by the cert and key from global.tls + # if you have your own secret, reference that. + - secretName: gen3-certs + hosts: + # Replace with your hostname + - qureshi.planx-pla.net + + +# -- (map) To configure postgresql subchart +# Persistence is disabled by default +postgresql: + primary: + persistence: + # -- (bool) Option to persist the dbs data. + enabled: true + + +# Add configuration overrides for fence and other services below as needed \ No newline at end of file diff --git a/examples/gke_values.yaml b/examples/gke_values.yaml new file mode 100644 index 00000000..6e83543c --- /dev/null +++ b/examples/gke_values.yaml @@ -0,0 +1,84 @@ +global: + # to disable local es/postgre + dev: false + hostname: qureshi.planx-pla.net + esEndpoint: "" + tls: + cert: + key: + # Postgres instance that is managed outside of k8s + postgres: + master: + host: "postgres-host-address" + username: "postgres" + password: "postgres-password" + +# configuration for fence helm chart. You can add it for all our services. +fence: + # Override image + image: + repository: quay.io/cdis/fence + tag: master + + # Fence config overrides + FENCE_CONFIG: + APP_NAME: 'Gen3 Data Commons' + # A URL-safe base64-encoded 32-byte key for encrypting keys in db + # in python you can use the following script to generate one: + # import base64 + # import os + # key = base64.urlsafe_b64encode(os.urandom(32)) + # print(key) + ENCRYPTION_KEY: REPLACEME + + DEBUG: True + OPENID_CONNECT: + google: + client_id: "" + client_secret: "" + + AWS_CREDENTIALS: + 'fence-bot': + aws_access_key_id: '' + aws_secret_access_key: '' + + S3_BUCKETS: + # Name of the actual s3 bucket + jq-helm-testing: + cred: 'fence-bot' + region: us-east-1 + + # This is important for data upload. + DATA_UPLOAD_BUCKET: 'jq-helm-testing' + + + +# -- (map) To configure postgresql subchart +# Persistence is disabled by default +postgresql: + primary: + persistence: + # -- (bool) Option to persist the dbs data. + enabled: true + + +revproxy: + ingress: + # Enable the default ingress included by helm. Add any configurations as needed. + enabled: true + hosts: + # Replace with your hostname + - host: qureshi.planx-pla.net + paths: + - path: / + pathType: Prefix + tls: + # this is the secret generated by the cert and key from global.tls + # if you have your own secret, reference that. + - secretName: gen3-certs + hosts: + # Replace with your hostname + - qureshi.planx-pla.net + + +# Add configuration overrides for fence and other services below as needed \ No newline at end of file diff --git a/examples/local_dev_values.yaml b/examples/local_dev_values.yaml new file mode 100644 index 00000000..1250b2c5 --- /dev/null +++ b/examples/local_dev_values.yaml @@ -0,0 +1,36 @@ +global: + dev: true + hostname: localhost + +# configuration for fence helm chart. You can add it for all our services. +fence: + # Fence config overrides + FENCE_CONFIG: + OPENID_CONNECT: + google: + client_id: "" + client_secret: "" + + AWS_CREDENTIALS: + 'fence-bot': + aws_access_key_id: '' + aws_secret_access_key: '' + + S3_BUCKETS: + # Name of the actual s3 bucket + jq-helm-testing: + cred: 'fence-bot' + region: us-east-1 + + # This is important for data upload. + DATA_UPLOAD_BUCKET: 'jq-helm-testing' + + +portal: + image: + repository: quay.io/cdis/data-portal-prebuilt + tag: brh.data-commons.org-feat-pr_comment + resources: + requests: + cpu: 0.2 + memory: 500Mi \ No newline at end of file diff --git a/helm/alloy/Chart.yaml b/helm/alloy/Chart.yaml new file mode 100644 index 00000000..ac429f84 --- /dev/null +++ b/helm/alloy/Chart.yaml @@ -0,0 +1,30 @@ +apiVersion: v2 +name: alloy +description: A Helm chart for deploying Grafana Alloy + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.2 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "master" + +# Dependencies +dependencies: + - name: alloy + version: "0.9.1" + repository: "https://grafana.github.io/helm-charts" diff --git a/helm/alloy/README.md b/helm/alloy/README.md new file mode 100644 index 00000000..97a37142 --- /dev/null +++ b/helm/alloy/README.md @@ -0,0 +1,29 @@ +# alloy + +![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) + +A Helm chart for deploying Grafana Alloy + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://grafana.github.io/helm-charts | alloy | 0.9.1 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| alloy.alloy.clustering.enabled | bool | `true` | | +| alloy.alloy.configMap.key | string | `"config"` | | +| alloy.alloy.configMap.name | string | `"alloy-gen3"` | | +| alloy.alloy.extraPorts | list | `[{"name":"otel-grpc","port":4317,"protocol":"TCP","targetPort":4317},{"name":"otel-http","port":4318,"protocol":"TCP","targetPort":4318}]` | Extra ports to expose on the Alloy container. | +| alloy.alloy.resources.requests.cpu | string | `"1000m"` | | +| alloy.alloy.resources.requests.memory | string | `"1Gi"` | | +| alloy.alloy.stabilityLevel | string | `"public-preview"` | | +| alloy.alloy.uiPathPrefix | string | `"/alloy"` | | +| alloy.alloyConfigmapData | string | `"logging {\n level = \"info\"\n format = \"json\"\n write_to = [loki.write.endpoint.receiver]\n}\n\n/////////////////////// OTLP START ///////////////////////\n\notelcol.receiver.otlp \"default\" {\n grpc {}\n http {}\n\n output {\n metrics = [otelcol.processor.batch.default.input]\n traces = [otelcol.processor.batch.default.input]\n }\n}\n\notelcol.processor.batch \"default\" {\n output {\n metrics = [otelcol.exporter.prometheus.default.input]\n traces = [otelcol.exporter.otlp.tempo.input]\n }\n}\n\notelcol.exporter.prometheus \"default\" {\n forward_to = [prometheus.remote_write.default.receiver]\n}\n\notelcol.exporter.otlp \"tempo\" {\n client {\n endpoint = \"http://monitoring-tempo-distributor.monitoring:4317\"\n // Configure TLS settings for communicating with the endpoint.\n tls {\n // The connection is insecure.\n insecure = true\n // Do not verify TLS certificates when connecting.\n insecure_skip_verify = true\n }\n }\n}\n\n\n/////////////////////// OTLP END ///////////////////////\n\n// discover all pods, to be used later in this config\ndiscovery.kubernetes \"pods\" {\n role = \"pod\"\n}\n\n// discover all services, to be used later in this config\ndiscovery.kubernetes \"services\" {\n role = \"service\"\n}\n\n// discover all nodes, to be used later in this config\ndiscovery.kubernetes \"nodes\" {\n role = \"node\"\n}\n\n// Generic scrape of any pod with Annotation \"prometheus.io/scrape: true\"\ndiscovery.relabel \"annotation_autodiscovery_pods\" {\n targets = discovery.kubernetes.pods.targets\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_scrape\"]\n regex = \"true\"\n action = \"keep\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_job\"]\n action = \"replace\"\n target_label = \"job\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_instance\"]\n action = \"replace\"\n target_label = \"instance\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_path\"]\n action = \"replace\"\n target_label = \"__metrics_path__\"\n }\n\n // Choose the pod port\n // The discovery generates a target for each declared container port of the pod.\n // If the metricsPortName annotation has value, keep only the target where the port name matches the one of the annotation.\n rule {\n source_labels = [\"__meta_kubernetes_pod_container_port_name\"]\n target_label = \"__tmp_port\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_portName\"]\n regex = \"(.+)\"\n target_label = \"__tmp_port\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_container_port_name\"]\n action = \"keepequal\"\n target_label = \"__tmp_port\"\n }\n\n // If the metrics port number annotation has a value, override the target address to use it, regardless whether it is\n // one of the declared ports on that Pod.\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_port\", \"__meta_kubernetes_pod_ip\"]\n regex = \"(\\\\d+);(([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})\"\n replacement = \"[$2]:$1\" // IPv6\n target_label = \"__address__\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_port\", \"__meta_kubernetes_pod_ip\"]\n regex = \"(\\\\d+);((([0-9]+?)(\\\\.|$)){4})\" // IPv4, takes priority over IPv6 when both exists\n replacement = \"$2:$1\"\n target_label = \"__address__\"\n }\n\n rule {\n source_labels = [\"__meta_kubernetes_pod_annotation_prometheus_io_scheme\"]\n action = \"replace\"\n target_label = \"__scheme__\"\n }\n\n\n // add labels\n rule {\n source_labels = [\"__meta_kubernetes_pod_name\"]\n target_label = \"pod\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_container_name\"]\n target_label = \"container\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_controller_name\"]\n target_label = \"controller\"\n }\n\n rule {\n source_labels = [\"__meta_kubernetes_namespace\"]\n target_label = \"namespace\"\n }\n\n\n rule {\n source_labels = [\"__meta_kubernetes_pod_label_app\"]\n target_label = \"app\"\n }\n\n // map all labels\n rule {\n action = \"labelmap\"\n regex = \"__meta_kubernetes_pod_label_(.+)\"\n }\n}\n\n// Generic scrape of any service with\n// Annotation Autodiscovery\ndiscovery.relabel \"annotation_autodiscovery_services\" {\n targets = discovery.kubernetes.services.targets\n rule {\n source_labels = [\"__meta_kubernetes_service_annotation_prometheus_io_scrape\"]\n regex = \"true\"\n action = \"keep\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_annotation_prometheus_io_job\"]\n action = \"replace\"\n target_label = \"job\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_annotation_prometheus_io_instance\"]\n action = \"replace\"\n target_label = \"instance\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_annotation_prometheus_io_path\"]\n action = \"replace\"\n target_label = \"__metrics_path__\"\n }\n\n // Choose the service port\n rule {\n source_labels = [\"__meta_kubernetes_service_port_name\"]\n target_label = \"__tmp_port\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_annotation_prometheus_io_portName\"]\n regex = \"(.+)\"\n target_label = \"__tmp_port\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_port_name\"]\n action = \"keepequal\"\n target_label = \"__tmp_port\"\n }\n\n rule {\n source_labels = [\"__meta_kubernetes_service_port_number\"]\n target_label = \"__tmp_port\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_annotation_prometheus_io_port\"]\n regex = \"(.+)\"\n target_label = \"__tmp_port\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_port_number\"]\n action = \"keepequal\"\n target_label = \"__tmp_port\"\n }\n\n rule {\n source_labels = [\"__meta_kubernetes_service_annotation_prometheus_io_scheme\"]\n action = \"replace\"\n target_label = \"__scheme__\"\n }\n}\n\nprometheus.scrape \"metrics\" {\n job_name = \"integrations/autodiscovery_metrics\"\n targets = concat(discovery.relabel.annotation_autodiscovery_pods.output, discovery.relabel.annotation_autodiscovery_services.output)\n honor_labels = true\n clustering {\n enabled = true\n }\n forward_to = [prometheus.relabel.metrics_service.receiver]\n}\n\n\n// Node Exporter\n// TODO: replace with https://grafana.com/docs/alloy/latest/reference/components/prometheus.exporter.unix/\ndiscovery.relabel \"node_exporter\" {\n targets = discovery.kubernetes.pods.targets\n rule {\n source_labels = [\"__meta_kubernetes_pod_label_app_kubernetes_io_instance\"]\n regex = \"monitoring-extras\"\n action = \"keep\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_label_app_kubernetes_io_name\"]\n regex = \"node-exporter\"\n action = \"keep\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_node_name\"]\n action = \"replace\"\n target_label = \"instance\"\n }\n}\n\nprometheus.scrape \"node_exporter\" {\n job_name = \"integrations/node_exporter\"\n targets = discovery.relabel.node_exporter.output\n scrape_interval = \"60s\"\n clustering {\n enabled = true\n }\n forward_to = [prometheus.relabel.node_exporter.receiver]\n}\n\nprometheus.relabel \"node_exporter\" {\n rule {\n source_labels = [\"__name__\"]\n regex = \"up|node_cpu.*|node_network.*|node_exporter_build_info|node_filesystem.*|node_memory.*|process_cpu_seconds_total|process_resident_memory_bytes\"\n action = \"keep\"\n }\n forward_to = [prometheus.relabel.metrics_service.receiver]\n}\n\n// Logs from all pods\ndiscovery.relabel \"all_pods\" {\n targets = discovery.kubernetes.pods.targets\n rule {\n source_labels = [\"__meta_kubernetes_namespace\"]\n target_label = \"namespace\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_name\"]\n target_label = \"pod\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_container_name\"]\n target_label = \"container\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_pod_controller_name\"]\n target_label = \"controller\"\n }\n\n rule {\n source_labels = [\"__meta_kubernetes_pod_label_app\"]\n target_label = \"app\"\n }\n\n // map all labels\n rule {\n action = \"labelmap\"\n regex = \"__meta_kubernetes_pod_label_(.+)\"\n }\n\n}\n\nloki.source.kubernetes \"pods\" {\n targets = discovery.relabel.all_pods.output\n forward_to = [loki.write.endpoint.receiver]\n}\n\n// kube-state-metrics\ndiscovery.relabel \"relabel_kube_state_metrics\" {\n targets = discovery.kubernetes.services.targets\n rule {\n source_labels = [\"__meta_kubernetes_namespace\"]\n regex = \"monitoring\"\n action = \"keep\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_service_name\"]\n regex = \"monitoring-extras-kube-state-metrics\"\n action = \"keep\"\n }\n}\n\nprometheus.scrape \"kube_state_metrics\" {\n targets = discovery.relabel.relabel_kube_state_metrics.output\n job_name = \"kube-state-metrics\"\n metrics_path = \"/metrics\"\n forward_to = [prometheus.remote_write.default.receiver]\n}\n\n// Kubelet\ndiscovery.relabel \"kubelet\" {\n targets = discovery.kubernetes.nodes.targets\n rule {\n target_label = \"__address__\"\n replacement = \"kubernetes.default.svc.cluster.local:443\"\n }\n rule {\n source_labels = [\"__meta_kubernetes_node_name\"]\n regex = \"(.+)\"\n replacement = \"/api/v1/nodes/${1}/proxy/metrics\"\n target_label = \"__metrics_path__\"\n }\n}\n\nprometheus.scrape \"kubelet\" {\n job_name = \"integrations/kubernetes/kubelet\"\n targets = discovery.relabel.kubelet.output\n scheme = \"https\"\n scrape_interval = \"60s\"\n bearer_token_file = \"/var/run/secrets/kubernetes.io/serviceaccount/token\"\n tls_config {\n insecure_skip_verify = true\n }\n clustering {\n enabled = true\n }\n forward_to = [prometheus.relabel.kubelet.receiver]\n}\n\nprometheus.relabel \"kubelet\" {\n rule {\n source_labels = [\"__name__\"]\n regex = \"up|container_cpu_usage_seconds_total|kubelet_certificate_manager_client_expiration_renew_errors|kubelet_certificate_manager_client_ttl_seconds|kubelet_certificate_manager_server_ttl_seconds|kubelet_cgroup_manager_duration_seconds_bucket|kubelet_cgroup_manager_duration_seconds_count|kubelet_node_config_error|kubelet_node_name|kubelet_pleg_relist_duration_seconds_bucket|kubelet_pleg_relist_duration_seconds_count|kubelet_pleg_relist_interval_seconds_bucket|kubelet_pod_start_duration_seconds_bucket|kubelet_pod_start_duration_seconds_count|kubelet_pod_worker_duration_seconds_bucket|kubelet_pod_worker_duration_seconds_count|kubelet_running_container_count|kubelet_running_containers|kubelet_running_pod_count|kubelet_running_pods|kubelet_runtime_operations_errors_total|kubelet_runtime_operations_total|kubelet_server_expiration_renew_errors|kubelet_volume_stats_available_bytes|kubelet_volume_stats_capacity_bytes|kubelet_volume_stats_inodes|kubelet_volume_stats_inodes_used|kubernetes_build_info|namespace_workload_pod|rest_client_requests_total|storage_operation_duration_seconds_count|storage_operation_errors_total|volume_manager_total_volumes\"\n action = \"keep\"\n }\n forward_to = [prometheus.relabel.metrics_service.receiver]\n}\n\n// Cluster Events\nloki.source.kubernetes_events \"cluster_events\" {\n job_name = \"integrations/kubernetes/eventhandler\"\n log_format = \"logfmt\"\n forward_to = [loki.write.endpoint.receiver]\n}\n\nprometheus.relabel \"metrics_service\" {\n forward_to = [prometheus.remote_write.default.receiver]\n}\n\n\n// Write Endpoints\n// prometheus write endpoint\nprometheus.remote_write \"default\" {\n external_labels = {\n cluster = \"{{ .Values.cluster }}\",\n project = \"{{ .Values.project }}\",\n }\n endpoint {\n url = \"https://mimir.example.com/api/v1/push\"\n\n headers = {\n \"X-Scope-OrgID\" = \"anonymous\",\n }\n\n }\n}\n\n// loki write endpoint\nloki.write \"endpoint\" {\n external_labels = {\n cluster = \"{{ .Values.cluster }}\",\n project = \"{{ .Values.project }}\",\n }\n endpoint {\n url = \"https://loki.example.com/loki/api/v1/push\"\n }\n}\n"` | | +| alloy.controller.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key | string | `"topology.kubernetes.io/zone"` | | +| alloy.controller.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | | +| alloy.controller.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values[0] | string | `"us-east-1a"` | | +| alloy.controller.type | string | `"deployment"` | | diff --git a/helm/alloy/SETUP.md b/helm/alloy/SETUP.md new file mode 100644 index 00000000..6a0b28f2 --- /dev/null +++ b/helm/alloy/SETUP.md @@ -0,0 +1,55 @@ +# Grafana Alloy + +## Overview + +This document provides a guide for deploying Grafana Alloy to your Kubernetes cluster using Helm. Grafana Alloy is a powerful observability tool that collects and ships logs and metrics from your services to Grafana Loki and Mimir for storage and analysis. By deploying Alloy, you can gain deep insights into your system’s performance, track key metrics, and troubleshoot issues efficiently. + +In this deployment, the Alloy ConfigMap plays a crucial role in configuring which logs are collected for Loki and which metrics are gathered for Mimir. It also specifies the endpoints for Loki and Mimir where the data will be sent. + +Before deploying Alloy, it is important to first deploy the "observability" Helm chart, as it provides the necessary components and configuration for Alloy to function properly. Please refer to the [SETUP.md](https://github.com/uc-cdis/gen3-helm/blob/master/helm/observability/SETUP.md) observability chart documentation for instructions on how to set it up before proceeding with the Alloy deployment. + +## Configuring Alloy + +### Helm Chart Configuration + +The Alloy configuration is the key component that allows users to customize what logs are collected for Loki and which metrics are collected for Mimir. Through this configuration, you can define the specific endpoints where logs and metrics should be sent, ensuring that data is properly routed for observability and analysis. + +In this configuration, it is important to replace the placeholder hostnames (*.example.com) with the actual Loki and Mimir hostnames that were configured in the "observability" Helm chart. This ensures that logs are sent to the correct Loki endpoint and metrics are forwarded to the appropriate Mimir endpoint, allowing your observability stack to function effectively. Additionally, you can fine-tune the alloyConfigmapData to suit your environment's needs. Please click [here](https://grafana.com/docs/alloy/latest/reference/components/#components) to see in-depth documentation on how to do so. + +```yaml + // Write Endpoints + // prometheus write endpoint + prometheus.remote_write "default" { + external_labels = { + cluster = "{{ .Values.cluster }}", + project = "{{ .Values.project }}", + } + endpoint { + url = "https://mimir.example.com/api/v1/push" + + headers = { + "X-Scope-OrgID" = "anonymous", + } + + } + } + + // loki write endpoint + loki.write "endpoint" { + external_labels = { + cluster = "{{ .Values.cluster }}", + project = "{{ .Values.project }}", + } + endpoint { + url = "https://loki.example.com/loki/api/v1/push" + } + } +``` +### Helm Chart Links +The link below will take you to the Grafana Alloy chart, providing a comprehensive list of configurable options to help you further customize your setup. + +[Alloy Helm Chart](https://github.com/grafana/alloy/blob/main/operations/helm/charts/alloy/values.yaml) + +--- + +By following this guide, you'll successfully configure Alloy to send logs and metrics to Grafana Loki and Mimir. The setup will ensure that Alloy collects the necessary observability data from your environment and forwards logs to Loki and metrics to Mimir for analysis and storage. This configuration will allow you to monitor your system's logs and metrics efficiently through Grafana. \ No newline at end of file diff --git a/helm/alloy/templates/alloy-config.yaml b/helm/alloy/templates/alloy-config.yaml new file mode 100644 index 00000000..0bf02875 --- /dev/null +++ b/helm/alloy/templates/alloy-config.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: alloy-gen3 +data: + config: | + {{- with .Values.alloy.alloyConfigmapData }} + {{- toYaml . | nindent 4 }} + {{ end }} \ No newline at end of file diff --git a/helm/alloy/values.yaml b/helm/alloy/values.yaml new file mode 100644 index 00000000..27232540 --- /dev/null +++ b/helm/alloy/values.yaml @@ -0,0 +1,445 @@ +alloy: + controller: + type: "deployment" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: topology.kubernetes.io/zone + operator: In + values: + - us-east-1a + + alloy: + stabilityLevel: "public-preview" + uiPathPrefix: /alloy + # -- Extra ports to expose on the Alloy container. + extraPorts: + - name: "otel-grpc" + port: 4317 + targetPort: 4317 + protocol: "TCP" + - name: "otel-http" + port: 4318 + targetPort: 4318 + protocol: "TCP" + clustering: + enabled: true + configMap: + name: alloy-gen3 + key: config + resources: + requests: + cpu: 1000m + memory: 1Gi + + alloyConfigmapData: | + logging { + level = "info" + format = "json" + write_to = [loki.write.endpoint.receiver] + } + + /////////////////////// OTLP START /////////////////////// + + otelcol.receiver.otlp "default" { + grpc {} + http {} + + output { + metrics = [otelcol.processor.batch.default.input] + traces = [otelcol.processor.batch.default.input] + } + } + + otelcol.processor.batch "default" { + output { + metrics = [otelcol.exporter.prometheus.default.input] + traces = [otelcol.exporter.otlp.tempo.input] + } + } + + otelcol.exporter.prometheus "default" { + forward_to = [prometheus.remote_write.default.receiver] + } + + otelcol.exporter.otlp "tempo" { + client { + endpoint = "http://monitoring-tempo-distributor.monitoring:4317" + // Configure TLS settings for communicating with the endpoint. + tls { + // The connection is insecure. + insecure = true + // Do not verify TLS certificates when connecting. + insecure_skip_verify = true + } + } + } + + + /////////////////////// OTLP END /////////////////////// + + // discover all pods, to be used later in this config + discovery.kubernetes "pods" { + role = "pod" + } + + // discover all services, to be used later in this config + discovery.kubernetes "services" { + role = "service" + } + + // discover all nodes, to be used later in this config + discovery.kubernetes "nodes" { + role = "node" + } + + // Generic scrape of any pod with Annotation "prometheus.io/scrape: true" + discovery.relabel "annotation_autodiscovery_pods" { + targets = discovery.kubernetes.pods.targets + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_scrape"] + regex = "true" + action = "keep" + } + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_job"] + action = "replace" + target_label = "job" + } + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_instance"] + action = "replace" + target_label = "instance" + } + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_path"] + action = "replace" + target_label = "__metrics_path__" + } + + // Choose the pod port + // The discovery generates a target for each declared container port of the pod. + // If the metricsPortName annotation has value, keep only the target where the port name matches the one of the annotation. + rule { + source_labels = ["__meta_kubernetes_pod_container_port_name"] + target_label = "__tmp_port" + } + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_portName"] + regex = "(.+)" + target_label = "__tmp_port" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_port_name"] + action = "keepequal" + target_label = "__tmp_port" + } + + // If the metrics port number annotation has a value, override the target address to use it, regardless whether it is + // one of the declared ports on that Pod. + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_port", "__meta_kubernetes_pod_ip"] + regex = "(\\d+);(([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})" + replacement = "[$2]:$1" // IPv6 + target_label = "__address__" + } + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_port", "__meta_kubernetes_pod_ip"] + regex = "(\\d+);((([0-9]+?)(\\.|$)){4})" // IPv4, takes priority over IPv6 when both exists + replacement = "$2:$1" + target_label = "__address__" + } + + rule { + source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_scheme"] + action = "replace" + target_label = "__scheme__" + } + + + // add labels + rule { + source_labels = ["__meta_kubernetes_pod_name"] + target_label = "pod" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + target_label = "container" + } + rule { + source_labels = ["__meta_kubernetes_pod_controller_name"] + target_label = "controller" + } + + rule { + source_labels = ["__meta_kubernetes_namespace"] + target_label = "namespace" + } + + + rule { + source_labels = ["__meta_kubernetes_pod_label_app"] + target_label = "app" + } + + // map all labels + rule { + action = "labelmap" + regex = "__meta_kubernetes_pod_label_(.+)" + } + } + + // Generic scrape of any service with + // Annotation Autodiscovery + discovery.relabel "annotation_autodiscovery_services" { + targets = discovery.kubernetes.services.targets + rule { + source_labels = ["__meta_kubernetes_service_annotation_prometheus_io_scrape"] + regex = "true" + action = "keep" + } + rule { + source_labels = ["__meta_kubernetes_service_annotation_prometheus_io_job"] + action = "replace" + target_label = "job" + } + rule { + source_labels = ["__meta_kubernetes_service_annotation_prometheus_io_instance"] + action = "replace" + target_label = "instance" + } + rule { + source_labels = ["__meta_kubernetes_service_annotation_prometheus_io_path"] + action = "replace" + target_label = "__metrics_path__" + } + + // Choose the service port + rule { + source_labels = ["__meta_kubernetes_service_port_name"] + target_label = "__tmp_port" + } + rule { + source_labels = ["__meta_kubernetes_service_annotation_prometheus_io_portName"] + regex = "(.+)" + target_label = "__tmp_port" + } + rule { + source_labels = ["__meta_kubernetes_service_port_name"] + action = "keepequal" + target_label = "__tmp_port" + } + + rule { + source_labels = ["__meta_kubernetes_service_port_number"] + target_label = "__tmp_port" + } + rule { + source_labels = ["__meta_kubernetes_service_annotation_prometheus_io_port"] + regex = "(.+)" + target_label = "__tmp_port" + } + rule { + source_labels = ["__meta_kubernetes_service_port_number"] + action = "keepequal" + target_label = "__tmp_port" + } + + rule { + source_labels = ["__meta_kubernetes_service_annotation_prometheus_io_scheme"] + action = "replace" + target_label = "__scheme__" + } + } + + prometheus.scrape "metrics" { + job_name = "integrations/autodiscovery_metrics" + targets = concat(discovery.relabel.annotation_autodiscovery_pods.output, discovery.relabel.annotation_autodiscovery_services.output) + honor_labels = true + clustering { + enabled = true + } + forward_to = [prometheus.relabel.metrics_service.receiver] + } + + + // Node Exporter + // TODO: replace with https://grafana.com/docs/alloy/latest/reference/components/prometheus.exporter.unix/ + discovery.relabel "node_exporter" { + targets = discovery.kubernetes.pods.targets + rule { + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_instance"] + regex = "monitoring-extras" + action = "keep" + } + rule { + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"] + regex = "node-exporter" + action = "keep" + } + rule { + source_labels = ["__meta_kubernetes_pod_node_name"] + action = "replace" + target_label = "instance" + } + } + + prometheus.scrape "node_exporter" { + job_name = "integrations/node_exporter" + targets = discovery.relabel.node_exporter.output + scrape_interval = "60s" + clustering { + enabled = true + } + forward_to = [prometheus.relabel.node_exporter.receiver] + } + + prometheus.relabel "node_exporter" { + rule { + source_labels = ["__name__"] + regex = "up|node_cpu.*|node_network.*|node_exporter_build_info|node_filesystem.*|node_memory.*|process_cpu_seconds_total|process_resident_memory_bytes" + action = "keep" + } + forward_to = [prometheus.relabel.metrics_service.receiver] + } + + // Logs from all pods + discovery.relabel "all_pods" { + targets = discovery.kubernetes.pods.targets + rule { + source_labels = ["__meta_kubernetes_namespace"] + target_label = "namespace" + } + rule { + source_labels = ["__meta_kubernetes_pod_name"] + target_label = "pod" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + target_label = "container" + } + rule { + source_labels = ["__meta_kubernetes_pod_controller_name"] + target_label = "controller" + } + + rule { + source_labels = ["__meta_kubernetes_pod_label_app"] + target_label = "app" + } + + // map all labels + rule { + action = "labelmap" + regex = "__meta_kubernetes_pod_label_(.+)" + } + + } + + loki.source.kubernetes "pods" { + targets = discovery.relabel.all_pods.output + forward_to = [loki.write.endpoint.receiver] + } + + // kube-state-metrics + discovery.relabel "relabel_kube_state_metrics" { + targets = discovery.kubernetes.services.targets + rule { + source_labels = ["__meta_kubernetes_namespace"] + regex = "monitoring" + action = "keep" + } + rule { + source_labels = ["__meta_kubernetes_service_name"] + regex = "monitoring-extras-kube-state-metrics" + action = "keep" + } + } + + prometheus.scrape "kube_state_metrics" { + targets = discovery.relabel.relabel_kube_state_metrics.output + job_name = "kube-state-metrics" + metrics_path = "/metrics" + forward_to = [prometheus.remote_write.default.receiver] + } + + // Kubelet + discovery.relabel "kubelet" { + targets = discovery.kubernetes.nodes.targets + rule { + target_label = "__address__" + replacement = "kubernetes.default.svc.cluster.local:443" + } + rule { + source_labels = ["__meta_kubernetes_node_name"] + regex = "(.+)" + replacement = "/api/v1/nodes/${1}/proxy/metrics" + target_label = "__metrics_path__" + } + } + + prometheus.scrape "kubelet" { + job_name = "integrations/kubernetes/kubelet" + targets = discovery.relabel.kubelet.output + scheme = "https" + scrape_interval = "60s" + bearer_token_file = "/var/run/secrets/kubernetes.io/serviceaccount/token" + tls_config { + insecure_skip_verify = true + } + clustering { + enabled = true + } + forward_to = [prometheus.relabel.kubelet.receiver] + } + + prometheus.relabel "kubelet" { + rule { + source_labels = ["__name__"] + regex = "up|container_cpu_usage_seconds_total|kubelet_certificate_manager_client_expiration_renew_errors|kubelet_certificate_manager_client_ttl_seconds|kubelet_certificate_manager_server_ttl_seconds|kubelet_cgroup_manager_duration_seconds_bucket|kubelet_cgroup_manager_duration_seconds_count|kubelet_node_config_error|kubelet_node_name|kubelet_pleg_relist_duration_seconds_bucket|kubelet_pleg_relist_duration_seconds_count|kubelet_pleg_relist_interval_seconds_bucket|kubelet_pod_start_duration_seconds_bucket|kubelet_pod_start_duration_seconds_count|kubelet_pod_worker_duration_seconds_bucket|kubelet_pod_worker_duration_seconds_count|kubelet_running_container_count|kubelet_running_containers|kubelet_running_pod_count|kubelet_running_pods|kubelet_runtime_operations_errors_total|kubelet_runtime_operations_total|kubelet_server_expiration_renew_errors|kubelet_volume_stats_available_bytes|kubelet_volume_stats_capacity_bytes|kubelet_volume_stats_inodes|kubelet_volume_stats_inodes_used|kubernetes_build_info|namespace_workload_pod|rest_client_requests_total|storage_operation_duration_seconds_count|storage_operation_errors_total|volume_manager_total_volumes" + action = "keep" + } + forward_to = [prometheus.relabel.metrics_service.receiver] + } + + // Cluster Events + loki.source.kubernetes_events "cluster_events" { + job_name = "integrations/kubernetes/eventhandler" + log_format = "logfmt" + forward_to = [loki.write.endpoint.receiver] + } + + prometheus.relabel "metrics_service" { + forward_to = [prometheus.remote_write.default.receiver] + } + + + // Write Endpoints + // prometheus write endpoint + prometheus.remote_write "default" { + external_labels = { + cluster = "{{ .Values.cluster }}", + project = "{{ .Values.project }}", + } + endpoint { + url = "https://mimir.example.com/api/v1/push" + + headers = { + "X-Scope-OrgID" = "anonymous", + } + + } + } + + // loki write endpoint + loki.write "endpoint" { + external_labels = { + cluster = "{{ .Values.cluster }}", + project = "{{ .Values.project }}", + } + endpoint { + url = "https://loki.example.com/loki/api/v1/push" + } + } diff --git a/helm/ambassador/Chart.yaml b/helm/ambassador/Chart.yaml index 849fad31..d356536b 100644 --- a/helm/ambassador/Chart.yaml +++ b/helm/ambassador/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.4 +version: 0.1.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "1.4.2" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/ambassador/README.md b/helm/ambassador/README.md index 9f6c9574..98cc6e06 100644 --- a/helm/ambassador/README.md +++ b/helm/ambassador/README.md @@ -1,9 +1,15 @@ # ambassador -![Version: 0.1.4](https://img.shields.io/badge/Version-0.1.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.4.2](https://img.shields.io/badge/AppVersion-1.4.2-informational?style=flat-square) +![Version: 0.1.16](https://img.shields.io/badge/Version-0.1.16-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.4.2](https://img.shields.io/badge/AppVersion-1.4.2-informational?style=flat-square) A Helm chart for deploying ambassador for gen3 +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -14,17 +20,24 @@ A Helm chart for deploying ambassador for gen3 | autoscaling.maxReplicas | int | `10` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `60` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | fullnameOverride | string | `"ambassador-deployment"` | Override the full name of the deployment. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/datawire/ambassador","tag":"1.4.2"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/datawire/ambassador"` | Docker repository. | | image.tag | string | `"1.4.2"` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | | nodeSelector | map | `{}` | Node selector labels. | +| partOf | string | `"Workspace-Tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `nil` | Annotations to add to the pod. | -| podLabels | map | `nil` | Labels to add to the pod. | | podSecurityContext | map | `{"runAsUser":8888}` | Pod-level security context. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"memory":"400Mi"},"requests":{"cpu":"100m","memory":"100Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"memory":"400Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -33,7 +46,7 @@ A Helm chart for deploying ambassador for gen3 | resources.requests.cpu | string | `"100m"` | The amount of CPU requested | | resources.requests.memory | string | `"100Mi"` | The amount of memory requested | | securityContext | map | `{}` | Container-level security context. | -| selectorLabels | map | `{"service":"ambassador"}` | Labels to use for selecting the deployment. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":8877,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `8877` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -43,6 +56,3 @@ A Helm chart for deploying ambassador for gen3 | serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template. | | tolerations | list | `[]` | Tolerations to use for the deployment. | | userNamespace | string | `"jupyter-pods"` | Namespace to use for user resources. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/ambassador/templates/_helpers.tpl b/helm/ambassador/templates/_helpers.tpl index f54ca8a8..913976ff 100644 --- a/helm/ambassador/templates/_helpers.tpl +++ b/helm/ambassador/templates/_helpers.tpl @@ -34,21 +34,27 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "ambassador.labels" -}} -helm.sh/chart: {{ include "ambassador.chart" . }} -{{ include "ambassador.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "ambassador.selectorLabels" -}} -service: ambassador +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} - {{/* Create the name of the service account to use */}} diff --git a/helm/ambassador/templates/deployment.yaml b/helm/ambassador/templates/deployment.yaml index 33710fc9..6caaa168 100644 --- a/helm/ambassador/templates/deployment.yaml +++ b/helm/ambassador/templates/deployment.yaml @@ -10,18 +10,22 @@ spec: {{- end }} selector: matchLabels: - app: ambassador + {{- include "ambassador.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: - app: ambassador - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end}} + netnolimit: "yes" + public: "yes" + userhelper: "yes" + {{- include "ambassador.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: affinity: podAntiAffinity: diff --git a/helm/ambassador/templates/pdb.yaml b/helm/ambassador/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/ambassador/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/ambassador/templates/service.yaml b/helm/ambassador/templates/service.yaml index 082ddbfd..8fc57bfe 100644 --- a/helm/ambassador/templates/service.yaml +++ b/helm/ambassador/templates/service.yaml @@ -17,7 +17,7 @@ apiVersion: v1 kind: Service metadata: labels: - service: ambassador + app: ambassador name: ambassador-service spec: ports: @@ -25,4 +25,4 @@ spec: targetPort: 8080 name: proxy selector: - service: ambassador \ No newline at end of file + {{- include "ambassador.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/helm/ambassador/templates/tests/test-connection.yaml b/helm/ambassador/templates/tests/test-connection.yaml index 363d01c8..3b88ad1c 100644 --- a/helm/ambassador/templates/tests/test-connection.yaml +++ b/helm/ambassador/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "ambassador.fullname" . }}-test-connection" + name: "ambassador-test-connection" labels: {{- include "ambassador.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "ambassador.fullname" . }}:{{ .Values.service.port }}'] + args: ['ambassador-service:80/ambassador/v0/check_ready'] restartPolicy: Never diff --git a/helm/ambassador/values.yaml b/helm/ambassador/values.yaml index 4f71208a..3c6e1398 100644 --- a/helm/ambassador/values.yaml +++ b/helm/ambassador/values.yaml @@ -1,10 +1,21 @@ # Default values for ambassador. # This is a YAML-formatted file. # Declare variables to be passed into your templates. +# Global configuration +global: + # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + environment: default + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 # -- (int) Number of replicas for the deployment. replicaCount: 1 +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + # -- (map) Docker image information. image: # -- (string) Docker repository. @@ -36,9 +47,6 @@ serviceAccount: # -- (map) Annotations to add to the pod. podAnnotations: -# -- (map) Labels to add to the pod. -podLabels: - # -- (map) Pod-level security context. podSecurityContext: runAsUser: 8888 @@ -60,10 +68,6 @@ service: # -- (int) The port number that the service exposes. port: 8877 -# -- (map) Labels to use for selecting the deployment. -selectorLabels: - service: ambassador - # -- (string) Namespace to use for user resources. userNamespace: "jupyter-pods" @@ -101,3 +105,15 @@ tolerations: [] # -- (map) Affinity to use for the deployment. affinity: {} + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Workspace-Tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/arborist/Chart.yaml b/helm/arborist/Chart.yaml index cb18f4e9..1c13ff60 100644 --- a/helm/arborist/Chart.yaml +++ b/helm/arborist/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 +version: 0.1.14 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -24,10 +24,10 @@ version: 0.1.5 appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/arborist/README.md b/helm/arborist/README.md index 5623f8fd..68301926 100644 --- a/helm/arborist/README.md +++ b/helm/arborist/README.md @@ -1,6 +1,6 @@ # arborist -![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.14](https://img.shields.io/badge/Version-0.1.14-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 arborist @@ -8,7 +8,7 @@ A Helm chart for gen3 arborist | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values @@ -21,22 +21,33 @@ A Helm chart for gen3 arborist | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | env | list | `[{"name":"JWKS_ENDPOINT","value":"http://fence-service/.well-known/jwks"}]` | Environment variables to pass to the container | | env[0] | string | `{"name":"JWKS_ENDPOINT","value":"http://fence-service/.well-known/jwks"}` | The URL of the JSON Web Key Set (JWKS) endpoint for authentication | +| externalSecrets | map | `{"dbcreds":null}` | External Secrets settings. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| global | map | `{"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any arborist secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -44,16 +55,16 @@ A Helm chart for gen3 arborist | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | | image | map | `{"pullPolicy":"IfNotPresent","repository":"quay.io/cdis/arborist","tag":""}` | Docker image information. | | image.pullPolicy | string | `"IfNotPresent"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/arborist"` | Docker repository. | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | | nodeSelector | map | `{}` | Node selector to apply to the pod | +| partOf | string | `"Authentication"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod | | podSecurityContext | map | `nil` | Security context to apply to the pod | | postgres | map | `{"database":null,"dbCreate":null,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | @@ -66,6 +77,7 @@ A Helm chart for gen3 arborist | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -74,7 +86,11 @@ A Helm chart for gen3 arborist | resources.requests | map | `{"cpu":0.1,"memory":"12Mi"}` | The amount of resources that the container requests | | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information for External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | | securityContext | map | `{}` | Security context to apply to the container | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -85,6 +101,3 @@ A Helm chart for gen3 arborist | tolerations | list | `[]` | Tolerations to apply to the pod | | volumeMounts | list | `[]` | Volume mounts to attach to the container | | volumes | list | `[]` | Volumes to attach to the pod | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/arborist/templates/_helpers.tpl b/helm/arborist/templates/_helpers.tpl index db6153b5..9a85f4bd 100644 --- a/helm/arborist/templates/_helpers.tpl +++ b/helm/arborist/templates/_helpers.tpl @@ -34,20 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "arborist.labels" -}} -helm.sh/chart: {{ include "arborist.chart" . }} -{{ include "arborist.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "arborist.selectorLabels" -}} -app.kubernetes.io/name: {{ include "arborist.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -71,4 +77,5 @@ Create the name of the service account to use {{- else }} {{- default .Values.postgres.password }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} + diff --git a/helm/arborist/templates/aws-config.yaml b/helm/arborist/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/arborist/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/arborist/templates/db-init.yaml b/helm/arborist/templates/db-init.yaml index abbefb6e..5ef14e87 100644 --- a/helm/arborist/templates/db-init.yaml +++ b/helm/arborist/templates/db-init.yaml @@ -1,6 +1,6 @@ -{{ include "common.db_setup_job" . }} ---- {{ include "common.db-secret" . }} --- -{{ include "common.db_setup_sa" . }} +{{ include "common.db_setup_job" . }} --- +{{ include "common.db_setup_sa" . }} +--- \ No newline at end of file diff --git a/helm/arborist/templates/deployment.yaml b/helm/arborist/templates/deployment.yaml index 5c7d900d..4f04a80b 100644 --- a/helm/arborist/templates/deployment.yaml +++ b/helm/arborist/templates/deployment.yaml @@ -13,12 +13,19 @@ spec: {{- include "arborist.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: + authprovider: "yes" + dbarborist: "yes" + public: "yes" {{- include "arborist.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: {{- with .Values.volumes }} volumes: @@ -68,6 +75,7 @@ spec: # run arborist /go/src/github.com/uc-cdis/arborist/bin/arborist env: + {{- toYaml .Values.env | nindent 12 }} - name: PGPASSWORD valueFrom: @@ -123,4 +131,4 @@ spec: {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} \ No newline at end of file diff --git a/helm/arborist/templates/external-secret.yaml b/helm/arborist/templates/external-secret.yaml new file mode 100644 index 00000000..70c278fe --- /dev/null +++ b/helm/arborist/templates/external-secret.yaml @@ -0,0 +1 @@ +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/arborist/templates/netpolicy.yaml b/helm/arborist/templates/netpolicy.yaml new file mode 100644 index 00000000..70a5c3b5 --- /dev/null +++ b/helm/arborist/templates/netpolicy.yaml @@ -0,0 +1 @@ +{{ include "common.db_netpolicy" . }} \ No newline at end of file diff --git a/helm/arborist/templates/pdb.yaml b/helm/arborist/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/arborist/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/arborist/templates/secret-store.yaml b/helm/arborist/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/arborist/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/arborist/templates/tests/test-connection.yaml b/helm/arborist/templates/tests/test-connection.yaml index c072755b..2a913dd0 100644 --- a/helm/arborist/templates/tests/test-connection.yaml +++ b/helm/arborist/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "arborist.fullname" . }}-test-connection" + name: "arborist-test-connection" labels: {{- include "arborist.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "arborist.fullname" . }}:{{ .Values.service.port }}'] + args: ['arborist-service:{{ .Values.service.port }}/health'] restartPolicy: Never diff --git a/helm/arborist/values.yaml b/helm/arborist/values.yaml index db83200b..32cb516a 100644 --- a/helm/arborist/values.yaml +++ b/helm/arborist/values.yaml @@ -2,14 +2,24 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: + # -- (map) AWS configuration + aws: + # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. + enabled: false + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -34,20 +44,39 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any arborist secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: +# -- (map) Secret information for External Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -74,7 +103,6 @@ postgresql: # -- (bool) Option to persist the dbs data. enabled: false - # -- (int) Number of replicas for the deployment. replicaCount: 1 @@ -115,7 +143,8 @@ podSecurityContext: # fsGroup: 2000 # -- (map) Security context to apply to the container -securityContext: {} +securityContext: + {} # -- (map) Linux capabilities to drop # capabilities: @@ -186,3 +215,15 @@ env: # -- (string) The URL of the JSON Web Key Set (JWKS) endpoint for authentication - name: JWKS_ENDPOINT value: "http://fence-service/.well-known/jwks" + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Authentication" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/argo-wrapper/Chart.yaml b/helm/argo-wrapper/Chart.yaml index 6ca1354a..6c3db9d2 100644 --- a/helm/argo-wrapper/Chart.yaml +++ b/helm/argo-wrapper/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.1 +version: 0.1.10 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/argo-wrapper/README.md b/helm/argo-wrapper/README.md index 6d9b04d9..75f3d778 100644 --- a/helm/argo-wrapper/README.md +++ b/helm/argo-wrapper/README.md @@ -1,9 +1,15 @@ # argo-wrapper -![Version: 0.1.1](https://img.shields.io/badge/Version-0.1.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.10](https://img.shields.io/badge/Version-0.1.10-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Argo Wrapper Service +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -20,16 +26,29 @@ A Helm chart for gen3 Argo Wrapper Service | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | -| dataDog | bool | `{"enabled":false,"env":"dev"}` | Whether Datadog is enabled. | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"false"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | environment | string | `"default"` | Environment name. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | bool | `{"dbSubnet":"","enabled":false}` | Global flags to control and manage network policies for a Gen3 installation NOTE: Network policies are currently a beta feature. Use with caution! | +| global.netPolicy.dbSubnet | array | `""` | A CIDR range representing a database subnet, that services with a database need access to | +| global.netPolicy.enabled | bool | `false` | Whether network policies are enabled | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/argo-wrapper","tag":""}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/argo-wrapper"` | Docker repository. | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | | indexdAdminUser | string | `"fence"` | Admin user for Indexd. | | internalS3Bucket | string | `"argo-internal-bucket"` | Name of the internal Argo bucket for Argo artifacts (does not allow pre-signed URLs). | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| netPolicy | map | `{"egressApps":["argo-wrapper"],"ingressApps":["argo-wrapper"]}` | Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true | +| netPolicy.egressApps | array | `["argo-wrapper"]` | List of apps that this app requires egress to | +| netPolicy.ingressApps | array | `["argo-wrapper"]` | List of app labels that require ingress to this service | +| partOf | string | `"Apps-Tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{"gen3.io/network-ingress":"argo-wrapper"}` | Annotations to add to the pod. | | pvc | string | `"test-pvc"` | PVC for Argo. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":"100m","memory":"128Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":"100m","memory":"128Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -38,6 +57,7 @@ A Helm chart for gen3 Argo Wrapper Service | revisionHistoryLimit | int | `2` | Number of old revisions to retain | | s3Bucket | string | `"argo-artifact-downloadable"` | S3 bucket name for Argo artifacts (allows pre-signed URLs). | | scalingGroups | list | `[{"user1":"workflow1"},{"user2":"workflow2"},{"user3":"workflow3"}]` | The workflow scaling groups to be used by Argo. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":8000,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `8000` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -46,6 +66,3 @@ A Helm chart for gen3 Argo Wrapper Service | strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | | volumeMounts | list | `[{"mountPath":"/argo.json","name":"argo-config","readOnly":true,"subPath":"argo.json"}]` | Volumes to mount to the pod. | | volumes | list | `[{"configMap":{"items":[{"key":"argo.json","path":"argo.json"}],"name":"manifest-argo"},"name":"argo-config"}]` | Volumes to attach to the pod. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/argo-wrapper/templates/_helpers.tpl b/helm/argo-wrapper/templates/_helpers.tpl index 01c2e9d2..cd6c98ae 100644 --- a/helm/argo-wrapper/templates/_helpers.tpl +++ b/helm/argo-wrapper/templates/_helpers.tpl @@ -34,21 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "argo-wrapper.labels" -}} -helm.sh/chart: {{ include "argo-wrapper.chart" . }} -{{ include "argo-wrapper.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "argo-wrapper.selectorLabels" -}} -app.kubernetes.io/name: {{ include "argo-wrapper.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ include "argo-wrapper.name" . }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -71,15 +76,4 @@ Define environment {{- else}} {{- .Values.environment }} {{- end }} -{{- end }} - -{{/* -Define ddEnabled -*/}} -{{- define "argo-wrapper.ddEnabled" -}} -{{- if .Values.global }} -{{- .Values.global.ddEnabled }} -{{- else}} -{{- .Values.dataDog.enabled }} -{{- end }} {{- end }} \ No newline at end of file diff --git a/helm/argo-wrapper/templates/deployment.yaml b/helm/argo-wrapper/templates/deployment.yaml index 49c068b0..a4d8ba20 100644 --- a/helm/argo-wrapper/templates/deployment.yaml +++ b/helm/argo-wrapper/templates/deployment.yaml @@ -23,16 +23,16 @@ spec: template: metadata: labels: + {{- include "argo-wrapper.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} # gen3 networkpolicy labels netnolimit: 'yes' public: 'yes' - {{- if eq (include "argo-wrapper.ddEnabled" . ) "true" }} - tags.datadoghq.com/service: "argo-wrapper" - # TODO: move this to helpers so we can have this populated from a configmap - tags.datadoghq.com/env: {{ .Values.dataDog.env }} - tags.datadoghq.com/version: {{ .Values.image.tag | default .Chart.AppVersion }} - {{- end }} - {{- include "argo-wrapper.selectorLabels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/argo-wrapper-config.yaml") . | sha256sum }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: {{- with .Values.affinity }} affinity: @@ -61,4 +61,5 @@ spec: {{- toYaml . | nindent 10 }} {{- end }} resources: - {{- toYaml .Values.resources | nindent 12 }} \ No newline at end of file + {{- toYaml .Values.resources | nindent 12 }} + env: \ No newline at end of file diff --git a/helm/argo-wrapper/templates/netpolicy.yaml b/helm/argo-wrapper/templates/netpolicy.yaml new file mode 100644 index 00000000..0a469d51 --- /dev/null +++ b/helm/argo-wrapper/templates/netpolicy.yaml @@ -0,0 +1,5 @@ +{{ include "common.ingress_netpolicy" . }} + +--- + +{{ include "common.egress_netpolicy" . }} \ No newline at end of file diff --git a/helm/argo-wrapper/templates/pdb.yaml b/helm/argo-wrapper/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/argo-wrapper/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/argo-wrapper/templates/tests/test-connection.yaml b/helm/argo-wrapper/templates/tests/test-connection.yaml index 0233f0e2..02e99617 100644 --- a/helm/argo-wrapper/templates/tests/test-connection.yaml +++ b/helm/argo-wrapper/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "argo-wrapper.fullname" . }}:{{ .Values.service.port }}'] + args: ['argo-wrapper-service:{{ .Values.service.port }}/test'] restartPolicy: Never diff --git a/helm/argo-wrapper/values.yaml b/helm/argo-wrapper/values.yaml index b8ed7154..d1c90550 100644 --- a/helm/argo-wrapper/values.yaml +++ b/helm/argo-wrapper/values.yaml @@ -2,8 +2,27 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# Deployment +# Global configuration +global: + # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + environment: default + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (bool) Global flags to control and manage network policies for a Gen3 installation + # NOTE: Network policies are currently a beta feature. Use with caution! + netPolicy: + # -- (bool) Whether network policies are enabled + enabled: false + + # -- (array) A CIDR range representing a database subnet, that services with a database need access to + dbSubnet: "" + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false +# Deployment # -- (map) Annotations to add to the pod. podAnnotations: {"gen3.io/network-ingress": "argo-wrapper"} @@ -35,30 +54,25 @@ strategy: # -- (int) Maximum amount of pods that can be unavailable during the update. maxUnavailable: 0 -# -- (bool) Whether Datadog is enabled. -dataDog: - enabled: false - env: dev - # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - argo-wrapper - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - argo-wrapper + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (list) Volumes to attach to the pod. volumes: @@ -101,12 +115,22 @@ service: # -- (int) The port number that the service exposes. port: 8000 +# -- (map) Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true +netPolicy: + # -- (array) List of app labels that require ingress to this service + ingressApps: + - argo-wrapper + + # -- (array) List of apps that this app requires egress to + egressApps: + - argo-wrapper + # Configmap # -- (list) The workflow scaling groups to be used by Argo. scalingGroups: -- user1: "workflow1" -- user2: "workflow2" -- user3: "workflow3" + - user1: "workflow1" + - user2: "workflow2" + - user3: "workflow3" # -- (string) S3 bucket name for Argo artifacts (allows pre-signed URLs). s3Bucket: "argo-artifact-downloadable" # -- (string) Name of the internal Argo bucket for Argo artifacts (does not allow pre-signed URLs). @@ -117,3 +141,15 @@ indexdAdminUser: "fence" environment: "default" # -- (string) PVC for Argo. pvc: "test-pvc" + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "false" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Apps-Tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/audit/Chart.yaml b/helm/audit/Chart.yaml index 4b2666b1..7099e8b3 100644 --- a/helm/audit/Chart.yaml +++ b/helm/audit/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 +version: 0.1.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,10 +23,10 @@ version: 0.1.5 appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/audit/README.md b/helm/audit/README.md index 4f35337d..c2882cb8 100644 --- a/helm/audit/README.md +++ b/helm/audit/README.md @@ -1,6 +1,6 @@ # audit -![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.16](https://img.shields.io/badge/Version-0.1.16-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for Kubernetes @@ -8,7 +8,7 @@ A Helm chart for Kubernetes | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values @@ -30,21 +30,34 @@ A Helm chart for Kubernetes | autoscaling.maxReplicas | int | `4` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"false"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | env | list | `[{"name":"DEBUG","value":"false"},{"name":"ARBORIST_URL","valueFrom":{"configMapKeyRef":{"key":"arborist_url","name":"manifest-global","optional":true}}}]` | Environment variables to pass to the container | +| externalSecrets | map | `{"auditG3auto":null,"createK8sAuditSecret":false,"dbcreds":null}` | External Secrets settings. | +| externalSecrets.auditG3auto | string | `nil` | Will override the name of the aws secrets manager secret. Default is "audit-g3auto" | +| externalSecrets.createK8sAuditSecret | string | `false` | Will create the Helm "audit-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | | fullnameOverride | string | `""` | Override the full name of the chart, which is used as the name of resources created by the chart | -| global | map | `{"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any audit secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -52,9 +65,7 @@ A Helm chart for Kubernetes | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/audit-service","tag":"master"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | When to pull the image. This value should be "Always" to ensure the latest image is used. | | image.repository | string | `"quay.io/cdis/audit-service"` | The Docker image repository for the audit service | @@ -62,15 +73,13 @@ A Helm chart for Kubernetes | imagePullSecrets | list | `[]` | Docker image pull secrets. | | initEnv | list | `{}` | Volumes to attach to the init container. | | initVolumeMounts | list | `[]` | Volumes to mount to the init container. | -| labels | map | `{"app":"audit","authprovider":"yes","netnolimit":"yes","public":"yes","release":"production","tags.datadoghq.com/service":"audit","userhelper":"yes"}` | Labels to add to the pod. | -| labels.app | string | `"audit"` | Application name. | -| labels.authprovider | string | `"yes"` | Grants egress from all pods to pods labeled with authrpovider=yes. For network policy selectors. | -| labels.netnolimit | string | `"yes"` | Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs | -| labels.public | string | `"yes"` | Grants ingress from the revproxy service for pods labeled with public=yes | -| labels.release | string | `"production"` | Release name. | -| labels.userhelper | string | `"yes"` | Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. This can be used to provide a unique name for a chart | +| netPolicy | map | `{"egressApps":["fence","presigned-url-fence"],"ingressApps":["fence","presigned-url-fence"]}` | Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true | +| netPolicy.egressApps | array | `["fence","presigned-url-fence"]` | List of apps that this app requires egress to | +| netPolicy.ingressApps | array | `["fence","presigned-url-fence"]` | List of app labels that require ingress to this service | | nodeSelector | map | `{}` | Node Selector for the pods | +| partOf | string | `"Logging"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod | | podSecurityContext | map | `{}` | Security context for the pod | | postgres | map | `{"database":null,"dbCreate":null,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | @@ -83,15 +92,20 @@ A Helm chart for Kubernetes | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of desired replicas | -| resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | +| resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.2,"memory":"120Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | | resources.limits.cpu | string | `1` | The maximum amount of CPU the container can use | | resources.limits.memory | string | `"512Mi"` | The maximum amount of memory the container can use | -| resources.requests | map | `{"cpu":0.1,"memory":"12Mi"}` | The amount of resources that the container requests | -| resources.requests.cpu | string | `0.1` | The amount of CPU requested | -| resources.requests.memory | string | `"12Mi"` | The amount of memory requested | +| resources.requests | map | `{"cpu":0.2,"memory":"120Mi"}` | The amount of resources that the container requests | +| resources.requests.cpu | string | `0.2` | The amount of CPU requested | +| resources.requests.memory | string | `"120Mi"` | The amount of memory requested | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information for External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | | securityContext | map | `{}` | Security context for the containers in the pod | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | server.AWS_CREDENTIALS | map | `{}` | AWS credentials to access SQS queue. | | server.debug | bool | `false` | Whether to enable or disable debug mode. | | server.pull_from_queue | bool | `false` | Whether to pull logs from sqs queue. | @@ -108,6 +122,3 @@ A Helm chart for Kubernetes | tolerations | list | `[]` | Tolerations for the pods | | volumeMounts | list | `[]` | Volumes to mount to the container. | | volumes | list | `[]` | Volumes to attach to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/audit/templates/_helpers.tpl b/helm/audit/templates/_helpers.tpl index 65a9c211..6f70cc66 100644 --- a/helm/audit/templates/_helpers.tpl +++ b/helm/audit/templates/_helpers.tpl @@ -34,20 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "audit.labels" -}} -helm.sh/chart: {{ include "audit.chart" . }} -{{ include "audit.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "audit.selectorLabels" -}} -app.kubernetes.io/name: {{ include "audit.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -61,16 +67,6 @@ Create the name of the service account to use {{- end }} {{- end }} - - -{{/* -Create the name of the service account to use -*/}} -{{- define "audit.secretName" -}} -{{- default "audit-g3auto" }} -{{- end }} - - {{/* Postgres Password lookup */}} @@ -81,4 +77,13 @@ Create the name of the service account to use {{- else }} {{- default .Values.postgres.password }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} + + +{{/* + Audit g3 Auto Secrets Manager Name +*/}} +{{- define "audit-g3auto" -}} +{{- default "audit-g3auto" .Values.externalSecrets.auditG3auto }} +{{- end }} + diff --git a/helm/audit/templates/aws-config.yaml b/helm/audit/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/audit/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/audit/templates/db-init.yaml b/helm/audit/templates/db-init.yaml index 50bd8e8f..5ef14e87 100644 --- a/helm/audit/templates/db-init.yaml +++ b/helm/audit/templates/db-init.yaml @@ -1,6 +1,6 @@ -{{ include "common.db_setup_job" . }} ---- {{ include "common.db-secret" . }} --- +{{ include "common.db_setup_job" . }} +--- {{ include "common.db_setup_sa" . }} --- \ No newline at end of file diff --git a/helm/audit/templates/deployment.yaml b/helm/audit/templates/deployment.yaml index 68fa5186..7cf86c02 100644 --- a/helm/audit/templates/deployment.yaml +++ b/helm/audit/templates/deployment.yaml @@ -13,12 +13,17 @@ spec: {{- include "audit.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: - {{- include "audit.labels" . | nindent 8 }} + {{- include "audit.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: serviceAccountName: {{ include "audit.serviceAccountName" . }} volumes: @@ -86,6 +91,11 @@ spec: readOnly: true mountPath: "/src/audit-service-config.yaml" subPath: "audit-service-config.yaml" + # Added an additional volume mount for new images using the / directory, while retaining the 'src' mount for backward compatibility. + - name: "config-volume" + readOnly: true + mountPath: "/audit/audit-service-config.yaml" + subPath: "audit-service-config.yaml" {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} @@ -130,12 +140,18 @@ spec: args: - "-c" - | - /env/bin/alembic upgrade head + # Managing virtual environments via poetry instead of python since the AL base image update, but retaining backwards compatibility + poetry run alembic upgrade head || /env/bin/alembic upgrade head volumeMounts: - name: "config-volume" readOnly: true mountPath: "/src/audit-service-config.yaml" subPath: "audit-service-config.yaml" + # Added an additional volume mount for new images using the / directory, while retaining the 'src' mount for backward compatibility. + - name: "config-volume" + readOnly: true + mountPath: "/audit/audit-service-config.yaml" + subPath: "audit-service-config.yaml" {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/helm/audit/templates/external-secret.yaml b/helm/audit/templates/external-secret.yaml new file mode 100644 index 00000000..d925feaa --- /dev/null +++ b/helm/audit/templates/external-secret.yaml @@ -0,0 +1,21 @@ +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: audit-g3auto +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: audit-g3auto + creationPolicy: Owner + data: + - secretKey: audit-service-config.yaml + remoteRef: + #name of secret in secrets manager + key: {{include "audit-g3auto" .}} +{{- end }} +--- +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/audit/templates/netpolicy.yaml b/helm/audit/templates/netpolicy.yaml new file mode 100644 index 00000000..93949e3a --- /dev/null +++ b/helm/audit/templates/netpolicy.yaml @@ -0,0 +1,9 @@ +{{ include "common.db_netpolicy" . }} + +--- + +{{ include "common.ingress_netpolicy" . }} + +--- + +{{ include "common.egress_netpolicy" . }} \ No newline at end of file diff --git a/helm/audit/templates/pdb.yaml b/helm/audit/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/audit/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/audit/templates/secret-store.yaml b/helm/audit/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/audit/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/audit/templates/secrets.yaml b/helm/audit/templates/secrets.yaml index 945c8d36..b1073381 100644 --- a/helm/audit/templates/secrets.yaml +++ b/helm/audit/templates/secrets.yaml @@ -1,7 +1,8 @@ +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sAuditSecret) }} apiVersion: v1 kind: Secret metadata: - name: {{ include "audit.secretName" . }} + name: audit-g3auto labels: {{- include "audit.labels" . | nindent 4 }} stringData: @@ -49,4 +50,5 @@ stringData: # whether to return usernames in query responses, # and to allow querying by username - QUERY_USERNAMES: true \ No newline at end of file + QUERY_USERNAMES: true +{{- end }} \ No newline at end of file diff --git a/helm/audit/templates/tests/test-connection.yaml b/helm/audit/templates/tests/test-connection.yaml index 72841458..d0a13d3a 100644 --- a/helm/audit/templates/tests/test-connection.yaml +++ b/helm/audit/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "audit.fullname" . }}:{{ .Values.service.port }}'] + args: ['audit-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/audit/values.yaml b/helm/audit/values.yaml index be4928de..e8656e49 100644 --- a/helm/audit/values.yaml +++ b/helm/audit/values.yaml @@ -1,14 +1,25 @@ # Default values for audit. # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. + +# Global configuration global: + # -- (map) AWS configuration + aws: + # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. + enabled: false + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -33,20 +44,43 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any audit secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will create the Helm "audit-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sAuditSecret: false + # -- (string) Will override the name of the aws secrets manager secret. Default is "audit-g3auto" + auditG3auto: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: +# -- (map) Secret information for External Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -115,7 +149,8 @@ podAnnotations: {} podSecurityContext: {} # -- (map) Security context for the containers in the pod -securityContext: {} +securityContext: + {} # capabilities: # drop: @@ -131,14 +166,25 @@ service: # -- (int) Port on which the service is exposed port: 80 +# -- (map) Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true +netPolicy: + # -- (array) List of app labels that require ingress to this service + ingressApps: + - fence + - presigned-url-fence + # -- (array) List of apps that this app requires egress to + egressApps: + - fence + - presigned-url-fence + # -- (map) Resource requests and limits for the containers in the pod resources: # -- (map) The amount of resources that the container requests requests: # -- (string) The amount of CPU requested - cpu: 0.1 + cpu: 0.2 # -- (string) The amount of memory requested - memory: 12Mi + memory: 120Mi # -- (map) The maximum amount of resources that the container is allowed to use limits: # -- (string) The maximum amount of CPU the container can use @@ -158,48 +204,31 @@ autoscaling: targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 - # -- (map) Node Selector for the pods nodeSelector: {} # -- (list) Tolerations for the pods tolerations: [] -# -- (map) Labels to add to the pod. -labels: - # -- (string) Application name. - app: audit - # -- (string) Release name. - release: production - # -- (string) Grants egress from all pods to pods labeled with authrpovider=yes. For network policy selectors. - authprovider: "yes" - # -- (string) Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs - netnolimit: "yes" - # -- (string) Grants ingress from the revproxy service for pods labeled with public=yes - public: "yes" - # -- (string) Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes - userhelper: "yes" - tags.datadoghq.com/service: "audit" - # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - audit - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - audit + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (list) Environment variables to pass to the container env: @@ -236,7 +265,8 @@ server: # -- (string) The URL for the SQS queue. url: "http://sqs.com" # -- (map) AWS credentials to access SQS queue. - AWS_CREDENTIALS: {} + AWS_CREDENTIALS: + {} # cred1: # aws_access_key_id: # aws_secret_access_key: @@ -250,3 +280,15 @@ api: # -- (bool) Whether to return usernames in query responses and allow querying by username. QUERY_USERNAMES: true + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "false" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Logging" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/aws-es-proxy/Chart.yaml b/helm/aws-es-proxy/Chart.yaml index d4f84358..a2c7bf35 100644 --- a/helm/aws-es-proxy/Chart.yaml +++ b/helm/aws-es-proxy/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.3 +version: 0.1.13 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/aws-es-proxy/README.md b/helm/aws-es-proxy/README.md index 93cd8902..f0a3fb8c 100644 --- a/helm/aws-es-proxy/README.md +++ b/helm/aws-es-proxy/README.md @@ -1,9 +1,15 @@ # aws-es-proxy -![Version: 0.1.3](https://img.shields.io/badge/Version-0.1.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.13](https://img.shields.io/badge/Version-0.1.13-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for AWS ES Proxy Service for gen3 +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -14,13 +20,33 @@ A Helm chart for AWS ES Proxy Service for gen3 | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"false"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | esEndpoint | str | `"test.us-east-1.es.amazonaws.com"` | Elasticsearch endpoint in AWS | +| externalSecrets | map | `{"awsCreds":"aws-es-proxy-aws-credentials"}` | External Secrets settings. | +| externalSecrets.awsCreds | string | `"aws-es-proxy-aws-credentials"` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any audit secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy.enabled | bool | `false` | | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/aws-es-proxy","tag":""}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/aws-es-proxy"` | Docker repository. | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| netPolicy | map | `{"egressApps":["arranger","arranger-server","arranger-dashboard","guppy","metadata","spark","tube"],"ingressApps":["arranger","arranger-server","arranger-dashboard","guppy","metadata","spark","tube"]}` | Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true | +| netPolicy.egressApps | array | `["arranger","arranger-server","arranger-dashboard","guppy","metadata","spark","tube"]` | List of apps that this app requires egress to | +| netPolicy.ingressApps | array | `["arranger","arranger-server","arranger-dashboard","guppy","metadata","spark","tube"]` | List of app labels that require ingress to this service | +| partOf | string | `"Explorer-Tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `nil` | Annotations to add to the pod | | ports | list | `[{"containerPort":9200}]` | List of container ports | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"memory":"2Gi"},"requests":{"cpu":0.1,"memory":"250Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"memory":"2Gi"}` | The maximum amount of resources that the container is allowed to use | @@ -29,9 +55,10 @@ A Helm chart for AWS ES Proxy Service for gen3 | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"250Mi"` | The amount of memory requested | | revisionHistoryLimit | int | `2` | Number of old revisions to retain | -| secrets | map | `{"awsAccessKeyId":"","awsSecretAccessKey":""}` | Secret information | -| secrets.awsAccessKeyId | str | `""` | AWS access key ID | -| secrets.awsSecretAccessKey | str | `""` | AWS secret access key | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information to access AWS ES cluster. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":9200,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `9200` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -39,7 +66,4 @@ A Helm chart for AWS ES Proxy Service for gen3 | strategy.rollingUpdate.maxSurge | int | `1` | Number of additional replicas to add during rollout. | | strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | | volumeMounts | list | `[{"mountPath":"/root/.aws","name":"credentials","readOnly":true}]` | Volumes to mount to the pod. | -| volumes | list | `[{"name":"credentials","secret":{"secretName":"aws-es-proxy"}}]` | Volumes to attach to the pod | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) +| volumes | list | `nil` | Volumes to attach to the pod | diff --git a/helm/aws-es-proxy/templates/_helpers.tpl b/helm/aws-es-proxy/templates/_helpers.tpl index e33789e2..ed5de17a 100644 --- a/helm/aws-es-proxy/templates/_helpers.tpl +++ b/helm/aws-es-proxy/templates/_helpers.tpl @@ -34,21 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "aws-es-proxy.labels" -}} -helm.sh/chart: {{ include "aws-es-proxy.chart" . }} -{{ include "aws-es-proxy.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "aws-es-proxy.selectorLabels" -}} -app.kubernetes.io/name: {{ include "aws-es-proxy.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ "esproxy" }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* diff --git a/helm/aws-es-proxy/templates/aws-config.yaml b/helm/aws-es-proxy/templates/aws-config.yaml new file mode 100644 index 00000000..4723e6b3 --- /dev/null +++ b/helm/aws-es-proxy/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end }} \ No newline at end of file diff --git a/helm/aws-es-proxy/templates/aws-es-proxy.yaml b/helm/aws-es-proxy/templates/aws-es-proxy.yaml deleted file mode 100644 index 734cb48c..00000000 --- a/helm/aws-es-proxy/templates/aws-es-proxy.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: aws-es-proxy -type: Opaque -stringData: - credentials: | - [default] - aws_access_key_id={{.Values.secrets.awsAccessKeyId}} - aws_secret_access_key={{ .Values.secrets.awsSecretAccessKey}} \ No newline at end of file diff --git a/helm/aws-es-proxy/templates/deployment.yaml b/helm/aws-es-proxy/templates/deployment.yaml index 40f2085e..70c2ec12 100644 --- a/helm/aws-es-proxy/templates/deployment.yaml +++ b/helm/aws-es-proxy/templates/deployment.yaml @@ -2,8 +2,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: aws-es-proxy-deployment - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 4 }} {{- end }} labels: @@ -24,13 +24,18 @@ spec: metadata: labels: {{- include "aws-es-proxy.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} netvpc: "yes" + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} - {{- with .Values.volumes }} volumes: - {{- toYaml . | nindent 8 }} - {{- end }} + - name: credentials + secret: + secretName: {{.Chart.Name}}-aws-config containers: - name: "esproxy" image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" diff --git a/helm/aws-es-proxy/templates/external-secrets.yaml b/helm/aws-es-proxy/templates/external-secrets.yaml new file mode 100644 index 00000000..16517911 --- /dev/null +++ b/helm/aws-es-proxy/templates/external-secrets.yaml @@ -0,0 +1,19 @@ +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: aws-es-proxy-aws-config +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: aws-es-proxy-aws-config + creationPolicy: Owner + data: + - secretKey: credentials + remoteRef: + #name of secret in secrets manager + key: {{ .Values.externalSecrets.awsCreds }} +{{- end }} \ No newline at end of file diff --git a/helm/aws-es-proxy/templates/netpolicy.yaml b/helm/aws-es-proxy/templates/netpolicy.yaml new file mode 100644 index 00000000..0a469d51 --- /dev/null +++ b/helm/aws-es-proxy/templates/netpolicy.yaml @@ -0,0 +1,5 @@ +{{ include "common.ingress_netpolicy" . }} + +--- + +{{ include "common.egress_netpolicy" . }} \ No newline at end of file diff --git a/helm/aws-es-proxy/templates/pdb.yaml b/helm/aws-es-proxy/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/aws-es-proxy/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/aws-es-proxy/templates/secret-store.yaml b/helm/aws-es-proxy/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/aws-es-proxy/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/aws-es-proxy/templates/tests/test-connection.yaml b/helm/aws-es-proxy/templates/tests/test-connection.yaml index 32c8c104..aa28c31d 100644 --- a/helm/aws-es-proxy/templates/tests/test-connection.yaml +++ b/helm/aws-es-proxy/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "aws-es-proxy.fullname" . }}:{{ .Values.service.port }}'] + args: ['elasticsearch:{{ .Values.service.port }}/'] restartPolicy: Never diff --git a/helm/aws-es-proxy/values.yaml b/helm/aws-es-proxy/values.yaml index 1cb7d633..3a54b1b2 100644 --- a/helm/aws-es-proxy/values.yaml +++ b/helm/aws-es-proxy/values.yaml @@ -1,3 +1,39 @@ +# Default values for aws-es-proxy. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Global configuration +global: + # -- (map) AWS configuration + aws: + # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. + enabled: false + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: + # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + environment: default + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any audit secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + netPolicy: + enabled: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + awsCreds: "aws-es-proxy-aws-credentials" + # -- (map) Annotations to add to the pod podAnnotations: @@ -12,6 +48,13 @@ autoscaling: # -- (int) The target CPU utilization percentage for autoscaling targetCPUUtilizationPercentage: 80 +# -- (map) Secret information to access AWS ES cluster. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: + # -- (int) Number of replicas for the deployment. replicaCount: 1 @@ -32,9 +75,6 @@ automountServiceAccountToken: false # -- (list) Volumes to attach to the pod volumes: - - name: credentials - secret: - secretName: aws-es-proxy # -- (map) Docker image information. image: @@ -78,9 +118,35 @@ service: # -- (int) The port number that the service exposes. port: 9200 -# -- (map) Secret information -secrets: - # -- (str) AWS access key ID - awsAccessKeyId: "" - # -- (str) AWS secret access key - awsSecretAccessKey: "" +# -- (map) Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true +netPolicy: + # -- (array) List of app labels that require ingress to this service + ingressApps: + - arranger + - arranger-server + - arranger-dashboard + - guppy + - metadata + - spark + - tube + # -- (array) List of apps that this app requires egress to + egressApps: + - arranger + - arranger-server + - arranger-dashboard + - guppy + - metadata + - spark + - tube + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "false" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Explorer-Tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/common/Chart.yaml b/helm/common/Chart.yaml index ec8b6df8..66e881bd 100644 --- a/helm/common/Chart.yaml +++ b/helm/common/Chart.yaml @@ -15,7 +15,7 @@ type: library # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.4 +version: 0.1.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm/common/README.md b/helm/common/README.md index a17efc00..8ef44573 100644 --- a/helm/common/README.md +++ b/helm/common/README.md @@ -1,6 +1,6 @@ # common -![Version: 0.1.4](https://img.shields.io/badge/Version-0.1.4-informational?style=flat-square) ![Type: library](https://img.shields.io/badge/Type-library-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.16](https://img.shields.io/badge/Version-0.1.16-informational?style=flat-square) ![Type: library](https://img.shields.io/badge/Type-library-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for provisioning databases in gen3 @@ -8,19 +8,22 @@ A Helm chart for provisioning databases in gen3 | Key | Type | Default | Description | |-----|------|---------|-------------| -| global | map | `{"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | +| global.aws | map | `{"region":"us-east-1"}` | AWS configuration | +| global.aws.region | string | `"us-east-1"` | AWS region for this deployment | | global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.netPolicy | map | `{"dbSubnets":[],"enabled":true}` | Configuration for network policies. | +| global.netPolicy.dbSubnets | array | `[]` | A list of subnets where databases reside. This is to enable access in production environments | +| global.netPolicy.enabled | bool | `true` | Whether or not to apply netpolicies | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -28,9 +31,4 @@ A Helm chart for provisioning databases in gen3 | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/common/templates/_aws_config.tpl b/helm/common/templates/_aws_config.tpl new file mode 100644 index 00000000..be48047c --- /dev/null +++ b/helm/common/templates/_aws_config.tpl @@ -0,0 +1,18 @@ +{{/* + Credentials for all AWS stuff. +*/}} +{{ define "common.awsconfig" -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{.Chart.Name}}-aws-config +type: Opaque +stringData: + credentials: | + [default] + aws_access_key_id={{ .Values.secrets.awsAccessKeyId | default .Values.global.aws.awsAccessKeyId}} + aws_secret_access_key={{ .Values.secrets.awsSecretAccessKey | default .Values.global.aws.awsSecretAccessKey}} +data: + access-key: {{ .Values.secrets.awsAccessKeyId | default .Values.global.aws.awsAccessKeyId | b64enc }} + secret-access-key: {{ .Values.secrets.awsSecretAccessKey | default .Values.global.aws.awsSecretAccessKey | b64enc }} +{{- end }} \ No newline at end of file diff --git a/helm/common/templates/_db_setup_job.tpl b/helm/common/templates/_db_setup_job.tpl index 7acd75bb..9ea67dbe 100644 --- a/helm/common/templates/_db_setup_job.tpl +++ b/helm/common/templates/_db_setup_job.tpl @@ -31,6 +31,7 @@ roleRef: # DB Setup Job {{- define "common.db_setup_job" -}} +{{- if or $.Values.global.postgres.dbCreate $.Values.postgres.dbCreate }} apiVersion: batch/v1 kind: Job metadata: @@ -58,16 +59,44 @@ spec: name: {{ .Release.Name }}-postgresql key: postgres-password optional: false + {{- else if $.Values.global.postgres.externalSecret }} + valueFrom: + secretKeyRef: + name: {{ $.Values.global.postgres.externalSecret }} + key: password + optional: false {{- else }} value: {{ .Values.global.postgres.master.password | quote}} {{- end }} - name: PGUSER + {{- if $.Values.global.postgres.externalSecret }} + valueFrom: + secretKeyRef: + name: {{ $.Values.global.postgres.externalSecret }} + key: username + optional: false + {{- else }} value: {{ .Values.global.postgres.master.username | quote }} + {{- end }} - name: PGPORT + {{- if $.Values.global.postgres.externalSecret }} + valueFrom: + secretKeyRef: + name: {{ $.Values.global.postgres.externalSecret }} + key: port + optional: false + {{- else }} value: {{ .Values.global.postgres.master.port | quote }} + {{- end }} - name: PGHOST {{- if $.Values.global.dev }} value: "{{ .Release.Name }}-postgresql" + {{- else if $.Values.global.postgres.externalSecret }} + valueFrom: + secretKeyRef: + name: {{ $.Values.global.postgres.externalSecret }} + key: host + optional: false {{- else }} value: {{ .Values.global.postgres.master.host | quote }} {{- end }} @@ -132,6 +161,7 @@ spec: # Update secret to signal that db has been created, and services can start kubectl patch secret/{{ .Chart.Name }}-dbcreds -p '{"data":{"dbcreated":"dHJ1ZQo="}}' fi +{{- end}} {{- end }} @@ -140,6 +170,7 @@ Create k8s secrets for connecting to postgres */}} # DB Secrets {{- define "common.db-secret" -}} +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.global.externalSecrets.dbCreate) }} apiVersion: v1 kind: Secret metadata: @@ -154,4 +185,5 @@ data: {{- else }} host: {{ ( $.Values.postgres.host | default ( $.Values.global.postgres.master.host)) | b64enc | quote }} {{- end }} +{{- end }} {{- end }} \ No newline at end of file diff --git a/helm/common/templates/_es_index_restore.tpl b/helm/common/templates/_es_index_restore.tpl index 7953a627..c3781c0e 100644 --- a/helm/common/templates/_es_index_restore.tpl +++ b/helm/common/templates/_es_index_restore.tpl @@ -21,7 +21,7 @@ spec: volumes: - name: cred-volume secret: - secretName: aws-config-{{ .Chart.Name }} + secretName: {{.Chart.Name}}-aws-config containers: - name: create-indices image: quay.io/cdis/awshelper:master @@ -29,7 +29,7 @@ spec: - name: GEN3_HOME value: /home/ubuntu/cloud-automation - name: ESHOST - value: elasticsearch:9200 + value: {{ default "gen3-elasticsearch-master:9200" $.Values.esEndpoint }} - name: GUPPY_INDICES value: {{ range $.Values.indices }} {{ .index }} {{ end }} - name: GUPPY_CONFIGINDEX diff --git a/helm/common/templates/_external_secrets.tpl b/helm/common/templates/_external_secrets.tpl new file mode 100644 index 00000000..d684ceed --- /dev/null +++ b/helm/common/templates/_external_secrets.tpl @@ -0,0 +1,87 @@ +{{/* + Service DB Creds Secrets Manager Name +*/}} +{{- define "common.externalSecret.dbcreds.name" -}} +{{- if .Values.externalSecrets.dbcreds }} + {{- default .Values.externalSecrets.dbcreds }} +{{- else }} + {{- .Values.global.environment }}- {{- .Chart.Name }}-creds +{{- end -}} +{{- end -}} + + + + +{{/* + ExternalSecrets Object +*/}} +{{- define "common.externalSecret.db" -}} +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: {{ $.Chart.Name }}-dbcreds +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: {{ $.Chart.Name }}-dbcreds + creationPolicy: Owner + dataFrom: + - extract: + key: {{include "common.externalSecret.dbcreds.name" .}} + conversionStrategy: Default + decodingStrategy: None +{{- end }} +{{- end -}} + + +{{/* + External Secrets Secret Store will allow all charts to allow for authentication to AWS Secrets Manager +*/}} +{{ define "common.secretstore" -}} +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: {{.Chart.Name}}-secret-store +spec: + provider: + aws: + service: SecretsManager + region: {{ .Values.global.aws.region }} + auth: + {{- if .Values.global.aws.secretStoreServiceAccount.enabled }} + jwt: + serviceAccountRef: + name: {{ .Values.global.aws.secretStoreServiceAccount.name }} + {{- else }} + secretRef: + accessKeyIDSecretRef: + name: {{.Chart.Name}}-aws-config + key: access-key + secretAccessKeySecretRef: + name: {{.Chart.Name}}-aws-config + key: secret-access-key + {{- end}} +{{- end }} + + + +{{/* + # Name of the SecretStore + # We want to allow override here, in case a chart is being deployed without the umbrella chart, + # or any other needs to deploy a separate secret store per service. +*/}} + +{{/* + Cluster Secret Store for External Secrets +*/}} +{{- define "common.SecretStore" -}} +{{- if .Values.global.externalSecrets.separateSecretStore }} + {{- .Chart.Name }}-secret-store +{{- else }} +{{- default "gen3-secret-store"}} +{{- end -}} +{{- end -}} diff --git a/helm/common/templates/_labels_setup.tpl b/helm/common/templates/_labels_setup.tpl new file mode 100644 index 00000000..a012a32e --- /dev/null +++ b/helm/common/templates/_labels_setup.tpl @@ -0,0 +1,51 @@ +{{/* + Gen3 Chart Labels + Will use the parent chart's chart, release, and version as well as the values "release", "criticalService", and "partOf" defined in the values.yaml file. + These values can be completely overwritten with the "selectorLabels" and "commonLabels" provided in the parent chart's values.yaml file. + "selectorLabels" are mainly used for the matchLabels and pod labels in the deployment. + "commonLabels" are mainly used for the deployment's labels. +*/}} + +{{- define "common.commonLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.Version }} +app.kubernetes.io/part-of: {{ .Values.partOf }} +app.kubernetes.io/managed-by: "Helm" +app: {{ .Chart.Name }} +{{- if eq .Values.criticalService "true"}} +critical-service: "true" +{{- else }} +critical-service: "false" +{{- end }} +{{- if eq .Values.release "production"}} +release: "production" +{{- else }} +release: "dev" +{{- end }} +{{- end }} + +{{- define "common.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app: {{ .Chart.Name }} +{{- if eq .Values.release "production"}} +release: "production" +{{- else }} +release: "dev" +{{- end }} +{{- end }} + +{{- define "common.extraLabels" -}} +hostname: {{ .Values.global.hostname }} +{{- if .Values.extraLabels }} + {{- with .Values.extraLabels }} + {{- toYaml . }} + {{- end }} +{{- end }} +{{- end }} + +{{- define "common.grafanaAnnotations" -}} +prometheus.io/path: /metrics +prometheus.io/scrape: "true" +{{- end }} \ No newline at end of file diff --git a/helm/common/templates/_netpolicy_templates.tpl b/helm/common/templates/_netpolicy_templates.tpl new file mode 100644 index 00000000..9613db41 --- /dev/null +++ b/helm/common/templates/_netpolicy_templates.tpl @@ -0,0 +1,68 @@ +{{/* + Templates for network policies that can be used by various subcharts +*/}} + +{{- define "common.db_netpolicy" -}} + {{- if .Values.global.netPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ .Chart.Name }}-db-netpolicy +spec: + egress: + {{- range .Values.global.netPolicy.dbSubnets }} + - to: + - ipBlock: + cidr: {{ . }} + {{- end }} + podSelector: + matchLabels: + app: {{ .Chart.Name }} + policyTypes: + - Egress + {{- end }} +{{- end }} + +{{ define "common.ingress_netpolicy" -}} + {{- if .Values.global.netPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ .Chart.Name }}-ingress-netpolicy +spec: + podSelector: + matchLabels: + app: {{ .Chart.Name }} + ingress: + - from: + - podSelector: + matchExpressions: + - key: app + operator: In + values: {{ toYaml .Values.netPolicy.ingressApps | nindent 12 }} + policyTypes: + - Ingress + {{- end }} +{{- end }} + +{{ define "common.egress_netpolicy" -}} + {{- if .Values.global.netPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ .Chart.Name }}-egress-netpolicy +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: {{ toYaml .Values.netPolicy.ingressApps | nindent 6 }} + egress: + - to: + - podSelector: + matchLabels: + app: {{ .Chart.Name }} + policyTypes: + - Egress + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/common/templates/_pdb.tpl b/helm/common/templates/_pdb.tpl new file mode 100644 index 00000000..9fca9c95 --- /dev/null +++ b/helm/common/templates/_pdb.tpl @@ -0,0 +1,16 @@ +{{/* + Gen3 Pod Disruption Budgets + Pdb will help increase availability by ensuring that one pod for each service is always avialable. + Will use the parent chart's name. +*/}} +{{ define "common.pod_disruption_budget" -}} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ .Chart.Name }}-pdb +spec: + minAvailable: {{ .Values.global.minAvialable }} + selector: + matchLabels: + app: {{ .Chart.Name }} +{{- end }} \ No newline at end of file diff --git a/helm/common/templates/_restore_pgdump.tpl b/helm/common/templates/_restore_pgdump.tpl index 7f849e0c..dffa024d 100644 --- a/helm/common/templates/_restore_pgdump.tpl +++ b/helm/common/templates/_restore_pgdump.tpl @@ -17,7 +17,12 @@ spec: volumes: - name: cred-volume secret: - secretName: aws-config + secretName: {{.Chart.Name}}-aws-config + {{- if .Values.global.aws.useLocalSecret.enabled -}} + secretName: {{ .Values.global.aws.useLocalSecret.localSecretName }} + {{ else }} + secretName: {{.Chart.Name}}-aws-config + {{ end }} containers: - name: restore-dbs image: quay.io/cdis/awshelper:master diff --git a/helm/common/values.yaml b/helm/common/values.yaml index 5524892c..c30dfc91 100644 --- a/helm/common/values.yaml +++ b/helm/common/values.yaml @@ -2,14 +2,20 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: + # -- (map) AWS configuration + aws: + # -- (string) AWS region for this deployment + region: us-east-1 # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -34,17 +40,18 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Configuration for network policies. + netPolicy: + # -- (bool) Whether or not to apply netpolicies + enabled: true + + # -- (array) A list of subnets where databases reside. This is to enable access in production environments + dbSubnets: [] # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 + dispatcherJobNum: "10" # -- (bool) Whether Datadog is enabled. ddEnabled: false diff --git a/helm/dicom-server/Chart.yaml b/helm/dicom-server/Chart.yaml index be600b52..caaed1d9 100644 --- a/helm/dicom-server/Chart.yaml +++ b/helm/dicom-server/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.2 +version: 0.1.11 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/dicom-server/README.md b/helm/dicom-server/README.md index 6355ffa0..58c6394c 100644 --- a/helm/dicom-server/README.md +++ b/helm/dicom-server/README.md @@ -1,9 +1,15 @@ # dicom-server -![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.11](https://img.shields.io/badge/Version-0.1.11-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Dicom Server +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -13,10 +19,19 @@ A Helm chart for gen3 Dicom Server | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"false"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Settings for network policies | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/gen3-orthanc","tag":"master"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/gen3-orthanc"` | Docker repository. | | image.tag | string | `"master"` | Overrides the image tag whose default is the chart appVersion. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| partOf | string | `"Imaging"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | secrets | map | `{"authenticationEnabled":false,"dataBase":"postgres","enableIndex":true,"enableStorage":true,"host":"postgres-postgresql.postgres.svc.cluster.local","indexConnectionsCount":5,"lock":false,"password":"postgres","port":"5432","userName":"postgres"}` | Secret information | | secrets.authenticationEnabled | bool | `false` | Whether or not the password protection is enabled. | @@ -29,11 +44,9 @@ A Helm chart for gen3 Dicom Server | secrets.password | string | `"postgres"` | Password for Postgres. | | secrets.port | string | `"5432"` | Port for Postgres. | | secrets.userName | string | `"postgres"` | Username for postgres. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"targetport":8042}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.targetport | int | `8042` | The port on the host machine that traffic is directed to. | | volumeMounts | list | `[{"mountPath":"/etc/orthanc/orthanc_config_overwrites.json","name":"config-volume-g3auto","readOnly":true,"subPath":"orthanc_config_overwrites.json"}]` | Volumes to mount to the pod. | | volumes | list | `[{"name":"config-volume-g3auto","secret":{"secretName":"orthanc-g3auto"}}]` | Volumes to attach to the pod. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/dicom-server/templates/_helpers.tpl b/helm/dicom-server/templates/_helpers.tpl index 2bfab706..0ad87443 100644 --- a/helm/dicom-server/templates/_helpers.tpl +++ b/helm/dicom-server/templates/_helpers.tpl @@ -34,21 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "dicom-server.labels" -}} -helm.sh/chart: {{ include "dicom-server.chart" . }} -{{ include "dicom-server.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "dicom-server.selectorLabels" -}} -app.kubernetes.io/name: {{ include "dicom-server.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: "dicom-server" +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* diff --git a/helm/dicom-server/templates/deployment.yaml b/helm/dicom-server/templates/deployment.yaml index 73758607..596285ca 100644 --- a/helm/dicom-server/templates/deployment.yaml +++ b/helm/dicom-server/templates/deployment.yaml @@ -2,8 +2,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: dicom-server-deployment - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 4 }} {{- end }} labels: @@ -21,8 +21,12 @@ spec: metadata: labels: {{- include "dicom-server.selectorLabels" . | nindent 8 }} - release: "production" + {{- include "common.extraLabels" . | nindent 8 }} public: "yes" + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: {{- with .Values.volumes }} volumes: @@ -48,6 +52,7 @@ spec: timeoutSeconds: 30 ports: - containerPort: 8042 + env: {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 10 }} diff --git a/helm/dicom-server/templates/netpolicy.yaml b/helm/dicom-server/templates/netpolicy.yaml new file mode 100644 index 00000000..70a5c3b5 --- /dev/null +++ b/helm/dicom-server/templates/netpolicy.yaml @@ -0,0 +1 @@ +{{ include "common.db_netpolicy" . }} \ No newline at end of file diff --git a/helm/dicom-server/templates/pdb.yaml b/helm/dicom-server/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/dicom-server/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/dicom-server/values.yaml b/helm/dicom-server/values.yaml index a5538cf1..81533e36 100644 --- a/helm/dicom-server/values.yaml +++ b/helm/dicom-server/values.yaml @@ -2,6 +2,21 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. +# Global configuration +global: + # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + environment: default + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) Settings for network policies + netPolicy: + enabled: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + # Deployment # -- (map) Configuration for autoscaling the number of replicas @@ -70,3 +85,15 @@ secrets: indexConnectionsCount: 5 # -- (bool) Whether to lock the database. lock: false + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "false" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Imaging" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/dicom-viewer/Chart.yaml b/helm/dicom-viewer/Chart.yaml index c178bd8c..30f65737 100644 --- a/helm/dicom-viewer/Chart.yaml +++ b/helm/dicom-viewer/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.2 +version: 0.1.11 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/dicom-viewer/README.md b/helm/dicom-viewer/README.md index 5c7e1f26..fc6fab1f 100644 --- a/helm/dicom-viewer/README.md +++ b/helm/dicom-viewer/README.md @@ -1,9 +1,15 @@ # dicom-viewer -![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.11](https://img.shields.io/badge/Version-0.1.11-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Dicom Viewer +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -13,14 +19,20 @@ A Helm chart for gen3 Dicom Viewer | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"false"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/ohif-viewer","tag":"master"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/ohif-viewer"` | Docker repository. | | image.tag | string | `"master"` | Overrides the image tag whose default is the chart appVersion. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| partOf | string | `"Imaging"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/dicom-viewer/templates/_helpers.tpl b/helm/dicom-viewer/templates/_helpers.tpl index a47308cd..7bbc396e 100644 --- a/helm/dicom-viewer/templates/_helpers.tpl +++ b/helm/dicom-viewer/templates/_helpers.tpl @@ -34,21 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "dicom-viewer.labels" -}} -helm.sh/chart: {{ include "dicom-viewer.chart" . }} -{{ include "dicom-viewer.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "dicom-viewer.selectorLabels" -}} -app.kubernetes.io/name: {{ include "dicom-viewer.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: "dicom-viewer" +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* diff --git a/helm/dicom-viewer/templates/deployment.yaml b/helm/dicom-viewer/templates/deployment.yaml index 9e1c2657..bab4bbac 100644 --- a/helm/dicom-viewer/templates/deployment.yaml +++ b/helm/dicom-viewer/templates/deployment.yaml @@ -2,8 +2,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: dicom-viewer-deployment - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 4 }} {{- end }} labels: @@ -15,14 +15,17 @@ spec: selector: matchLabels: {{- include "dicom-viewer.selectorLabels" . | nindent 6 }} - release: "production" public: "yes" template: metadata: labels: {{- include "dicom-viewer.selectorLabels" . | nindent 8 }} - release: "production" + {{- include "common.extraLabels" . | nindent 8 }} public: "yes" + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: containers: - name: "dicom-viewer" @@ -44,6 +47,7 @@ spec: timeoutSeconds: 30 ports: - containerPort: 80 + env: {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 10 }} diff --git a/helm/dicom-viewer/templates/pdb.yaml b/helm/dicom-viewer/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/dicom-viewer/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/dicom-viewer/values.yaml b/helm/dicom-viewer/values.yaml index 8711a165..7e3eb080 100644 --- a/helm/dicom-viewer/values.yaml +++ b/helm/dicom-viewer/values.yaml @@ -2,6 +2,19 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. + +# Global configuration +global: + # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + environment: default + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + # Deployment # -- (map) Configuration for autoscaling the number of replicas @@ -34,3 +47,15 @@ service: type: ClusterIP # -- (int) The port number that the service exposes. port: 80 + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "false" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Imaging" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/elasticsearch/README.md b/helm/elasticsearch/README.md deleted file mode 100644 index 06627db3..00000000 --- a/helm/elasticsearch/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# elasticsearch - -![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.16.0](https://img.shields.io/badge/AppVersion-1.16.0-informational?style=flat-square) - -A Helm chart for Kubernetes - -## Values - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| affinity | object | `{}` | | -| autoscaling.enabled | bool | `false` | | -| autoscaling.maxReplicas | int | `100` | | -| autoscaling.minReplicas | int | `1` | | -| autoscaling.targetCPUUtilizationPercentage | int | `80` | | -| fullnameOverride | string | `""` | | -| image.pullPolicy | string | `"IfNotPresent"` | | -| image.repository | string | `"quay.io/cdis/elasticsearch"` | | -| image.tag | string | `"feat_es_dockerfile"` | | -| imagePullSecrets | list | `[]` | | -| ingress.annotations | object | `{}` | | -| ingress.className | string | `""` | | -| ingress.enabled | bool | `false` | | -| ingress.hosts[0].host | string | `"chart-example.local"` | | -| ingress.hosts[0].paths[0].path | string | `"/"` | | -| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | -| ingress.tls | list | `[]` | | -| nameOverride | string | `""` | | -| nodeSelector | object | `{}` | | -| podAnnotations | object | `{}` | | -| podSecurityContext | object | `{}` | | -| replicaCount | int | `1` | | -| resources.limits.cpu | string | `"500m"` | | -| resources.limits.memory | string | `"750Mi"` | | -| resources.requests.cpu | string | `"500m"` | | -| resources.requests.memory | string | `"750Mi"` | | -| securityContext | object | `{}` | | -| service.port | int | `9200` | | -| service.type | string | `"ClusterIP"` | | -| serviceAccount.annotations | object | `{}` | | -| serviceAccount.create | bool | `true` | | -| serviceAccount.name | string | `""` | | -| tolerations | list | `[]` | | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/elasticsearch/templates/NOTES.txt b/helm/elasticsearch/templates/NOTES.txt deleted file mode 100644 index bf80dccb..00000000 --- a/helm/elasticsearch/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "elasticsearch.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "elasticsearch.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "elasticsearch.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "elasticsearch.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/helm/elasticsearch/templates/deployment.yaml b/helm/elasticsearch/templates/deployment.yaml deleted file mode 100644 index d1481dc7..00000000 --- a/helm/elasticsearch/templates/deployment.yaml +++ /dev/null @@ -1,69 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: elasticsearch-deployment - labels: - {{- include "elasticsearch.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "elasticsearch.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "elasticsearch.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "elasticsearch.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: ES_JAVA_OPTS - value: "-Xms300m -Xmx300m" - ports: - - name: http - containerPort: 9200 - protocol: TCP - - name: transport - containerPort: 9300 - protocol: TCP - livenessProbe: - httpGet: - path: /_cluster/health?local=true - port: 9200 - initialDelaySeconds: 90 - readinessProbe: - httpGet: - path: /_cluster/health - port: 9200 - initialDelaySeconds: 5 - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/helm/elasticsearch/templates/hpa.yaml b/helm/elasticsearch/templates/hpa.yaml deleted file mode 100644 index 22388451..00000000 --- a/helm/elasticsearch/templates/hpa.yaml +++ /dev/null @@ -1,28 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2beta1 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "elasticsearch.fullname" . }} - labels: - {{- include "elasticsearch.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "elasticsearch.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/helm/elasticsearch/templates/tests/test-connection.yaml b/helm/elasticsearch/templates/tests/test-connection.yaml deleted file mode 100644 index af8dd035..00000000 --- a/helm/elasticsearch/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "elasticsearch.fullname" . }}-test-connection" - labels: - {{- include "elasticsearch.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "elasticsearch.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/helm/elasticsearch/values.yaml b/helm/elasticsearch/values.yaml deleted file mode 100644 index d4cf8526..00000000 --- a/helm/elasticsearch/values.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# Default values for elasticsearch. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: quay.io/cdis/elasticsearch - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "feat_es_dockerfile" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -podAnnotations: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - port: 9200 - -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: - limits: - cpu: 500m - memory: 750Mi - requests: - cpu: 500m - memory: 750Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -nodeSelector: {} - -tolerations: [] - -affinity: {} diff --git a/helm/elasticsearch/Chart.yaml b/helm/etl/Chart.yaml similarity index 87% rename from helm/elasticsearch/Chart.yaml rename to helm/etl/Chart.yaml index 94905e42..83ba97e5 100644 --- a/helm/elasticsearch/Chart.yaml +++ b/helm/etl/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: elasticsearch -description: A Helm chart for Kubernetes +name: etl +description: A Helm chart for gen3 etl # A chart can be either an 'application' or a 'library' chart. # @@ -15,10 +15,11 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.2 +version: 0.1.7 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "1.16.0" +appVersion: "master" + +dependencies: [] diff --git a/helm/etl/README.md b/helm/etl/README.md new file mode 100644 index 00000000..3ef0e7e9 --- /dev/null +++ b/helm/etl/README.md @@ -0,0 +1,107 @@ +# etl + +![Version: 0.1.7](https://img.shields.io/badge/Version-0.1.7-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) + +A Helm chart for gen3 etl + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| esEndpoint | string | `"gen3-elasticsearch-master"` | | +| esGarbageCollect | map | `{"custom_image":null,"enabled":false,"schedule":"0 0 * * *","slack_webhook":"None"}` | Configuration options for es garbage cronjob. | +| esGarbageCollect.custom_image | string | `nil` | To set a custom image for the es garbage collect cronjob. Default is the Gen3 Awshelper image. | +| esGarbageCollect.enabled | bool | `false` | Whether to create es garbage collect cronjob. | +| esGarbageCollect.schedule | string | `"0 0 * * *"` | The cron schedule expression to use in the es garbage collect cronjob. Runs once a day by default. | +| esGarbageCollect.slack_webhook | string | `"None"` | Slack webhook endpoint to use for cronjob. | +| etlForced | string | `"TRUE"` | | +| etlMapping.mappings[0].aggregated_props[0].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[0].name | string | `"_samples_count"` | | +| etlMapping.mappings[0].aggregated_props[0].path | string | `"samples"` | | +| etlMapping.mappings[0].aggregated_props[1].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[1].name | string | `"_aliquots_count"` | | +| etlMapping.mappings[0].aggregated_props[1].path | string | `"samples.aliquots"` | | +| etlMapping.mappings[0].aggregated_props[2].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[2].name | string | `"_submitted_methylations_count"` | | +| etlMapping.mappings[0].aggregated_props[2].path | string | `"samples.aliquots.submitted_methylation_files"` | | +| etlMapping.mappings[0].aggregated_props[3].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[3].name | string | `"_submitted_copy_number_files_on_aliquots_count"` | | +| etlMapping.mappings[0].aggregated_props[3].path | string | `"samples.aliquots.submitted_copy_number_files"` | | +| etlMapping.mappings[0].aggregated_props[4].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[4].name | string | `"_read_groups_count"` | | +| etlMapping.mappings[0].aggregated_props[4].path | string | `"samples.aliquots.read_groups"` | | +| etlMapping.mappings[0].aggregated_props[5].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[5].name | string | `"_submitted_aligned_reads_count"` | | +| etlMapping.mappings[0].aggregated_props[5].path | string | `"samples.aliquots.read_groups.submitted_aligned_reads_files"` | | +| etlMapping.mappings[0].aggregated_props[6].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[6].name | string | `"_submitted_unaligned_reads_count"` | | +| etlMapping.mappings[0].aggregated_props[6].path | string | `"samples.aliquots.read_groups.submitted_unaligned_reads_files"` | | +| etlMapping.mappings[0].aggregated_props[7].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[7].name | string | `"_submitted_copy_number_files_on_read_groups_count"` | | +| etlMapping.mappings[0].aggregated_props[7].path | string | `"samples.aliquots.read_groups.submitted_copy_number_files"` | | +| etlMapping.mappings[0].aggregated_props[8].fn | string | `"count"` | | +| etlMapping.mappings[0].aggregated_props[8].name | string | `"_submitted_somatic_mutations_count"` | | +| etlMapping.mappings[0].aggregated_props[8].path | string | `"samples.aliquots.read_groups.submitted_somatic_mutations"` | | +| etlMapping.mappings[0].doc_type | string | `"case"` | | +| etlMapping.mappings[0].flatten_props[0].path | string | `"demographics"` | | +| etlMapping.mappings[0].flatten_props[0].props[0].name | string | `"gender"` | | +| etlMapping.mappings[0].flatten_props[0].props[0].value_mappings[0].female | string | `"F"` | | +| etlMapping.mappings[0].flatten_props[0].props[0].value_mappings[1].male | string | `"M"` | | +| etlMapping.mappings[0].flatten_props[0].props[1].name | string | `"race"` | | +| etlMapping.mappings[0].flatten_props[0].props[1].value_mappings[0]."american indian or alaskan native" | string | `"Indian"` | | +| etlMapping.mappings[0].flatten_props[0].props[2].name | string | `"ethnicity"` | | +| etlMapping.mappings[0].flatten_props[0].props[3].name | string | `"year_of_birth"` | | +| etlMapping.mappings[0].joining_props[0].index | string | `"file"` | | +| etlMapping.mappings[0].joining_props[0].join_on | string | `"_case_id"` | | +| etlMapping.mappings[0].joining_props[0].props[0].fn | string | `"set"` | | +| etlMapping.mappings[0].joining_props[0].props[0].name | string | `"data_format"` | | +| etlMapping.mappings[0].joining_props[0].props[0].src | string | `"data_format"` | | +| etlMapping.mappings[0].joining_props[0].props[1].fn | string | `"set"` | | +| etlMapping.mappings[0].joining_props[0].props[1].name | string | `"data_type"` | | +| etlMapping.mappings[0].joining_props[0].props[1].src | string | `"data_type"` | | +| etlMapping.mappings[0].joining_props[0].props[2].fn | string | `"set"` | | +| etlMapping.mappings[0].joining_props[0].props[2].name | string | `"_file_id"` | | +| etlMapping.mappings[0].joining_props[0].props[2].src | string | `"_file_id"` | | +| etlMapping.mappings[0].name | string | `"dev_case"` | | +| etlMapping.mappings[0].props[0].name | string | `"submitter_id"` | | +| etlMapping.mappings[0].props[1].name | string | `"project_id"` | | +| etlMapping.mappings[0].props[2].name | string | `"disease_type"` | | +| etlMapping.mappings[0].props[3].name | string | `"primary_site"` | | +| etlMapping.mappings[0].root | string | `"case"` | | +| etlMapping.mappings[0].type | string | `"aggregator"` | | +| etlMapping.mappings[1].category | string | `"data_file"` | | +| etlMapping.mappings[1].doc_type | string | `"file"` | | +| etlMapping.mappings[1].injecting_props.case.props[0].fn | string | `"set"` | | +| etlMapping.mappings[1].injecting_props.case.props[0].name | string | `"_case_id"` | | +| etlMapping.mappings[1].injecting_props.case.props[0].src | string | `"id"` | | +| etlMapping.mappings[1].injecting_props.case.props[1].name | string | `"project_id"` | | +| etlMapping.mappings[1].name | string | `"dev_file"` | | +| etlMapping.mappings[1].props[0].name | string | `"object_id"` | | +| etlMapping.mappings[1].props[1].name | string | `"md5sum"` | | +| etlMapping.mappings[1].props[2].name | string | `"file_name"` | | +| etlMapping.mappings[1].props[3].name | string | `"file_size"` | | +| etlMapping.mappings[1].props[4].name | string | `"data_format"` | | +| etlMapping.mappings[1].props[5].name | string | `"data_type"` | | +| etlMapping.mappings[1].props[6].name | string | `"state"` | | +| etlMapping.mappings[1].root | string | `"None"` | | +| etlMapping.mappings[1].target_nodes[0].name | string | `"slide_image"` | | +| etlMapping.mappings[1].target_nodes[0].path | string | `"slides.samples.cases"` | | +| etlMapping.mappings[1].type | string | `"collector"` | | +| image.spark.pullPolicy | string | `"Always"` | When to pull the image. This value should be "Always" to ensure the latest image is used. | +| image.spark.repository | string | `"quay.io/cdis/gen3-spark"` | The Docker image repository for the spark service | +| image.spark.tag | string | `"master"` | Overrides the image tag whose default is the chart appVersion. | +| image.tube.pullPolicy | string | `"Always"` | When to pull the image. This value should be "Always" to ensure the latest image is used. | +| image.tube.repository | string | `"quay.io/cdis/tube"` | The Docker image repository for the fence service | +| image.tube.tag | string | `"master"` | Overrides the image tag whose default is the chart appVersion. | +| imagePullSecrets | list | `[]` | Docker image pull secrets. | +| legacySupport | bool | `false` | | +| podAnnotations | map | `{}` | Annotations to add to the pod | +| resources | map | `{"spark":{"requests":{"cpu":0.3,"memory":"128Mi"}},"tube":{"requests":{"cpu":0.3,"memory":"128Mi"}}}` | Resource requests and limits for the containers in the pod | +| resources.spark.requests | map | `{"cpu":0.3,"memory":"128Mi"}` | The amount of resources that the container requests | +| resources.spark.requests.cpu | string | `0.3` | The amount of CPU requested | +| resources.spark.requests.memory | string | `"128Mi"` | The amount of memory requested | +| resources.tube.requests | map | `{"cpu":0.3,"memory":"128Mi"}` | The amount of resources that the container requests | +| resources.tube.requests.cpu | string | `0.3` | The amount of CPU requested | +| resources.tube.requests.memory | string | `"128Mi"` | The amount of memory requested | +| schedule | string | `"*/30 * * * *"` | | +| suspendCronjob | bool | `true` | | diff --git a/helm/etl/templates/es-garbage-collect-cronjob.yaml b/helm/etl/templates/es-garbage-collect-cronjob.yaml new file mode 100644 index 00000000..786172a7 --- /dev/null +++ b/helm/etl/templates/es-garbage-collect-cronjob.yaml @@ -0,0 +1,85 @@ +{{- if .Values.esGarbageCollect.enabled -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gitops-sa +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: gitops-sa-role +rules: + - apiGroups: [""] + resources: ["namespaces","services"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch", "create", "update", "delete", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: gitops-sa-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: gitops-sa-role +subjects: + - kind: ServiceAccount + name: gitops-sa + namespace: default +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: es-garbage +spec: + schedule: {{ .Values.esGarbageCollect.schedule | quote }} + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + concurrencyPolicy: Forbid + jobTemplate: + spec: + backoffLimit: 4 + template: + metadata: + labels: + app: gen3job + spec: + restartPolicy: Never + serviceAccountName: gitops-sa + securityContext: + fsGroup: 1000 + containers: + - name: awshelper + image: {{ .Values.esGarbageCollect.custom_image | default "quay.io/cdis/awshelper:master" }} + imagePullPolicy: Always + env: + - name: AWS_STS_REGIONAL_ENDPOINTS + value: regional + - name: ESHOST + value: {{ printf "%s:9200" .Values.esEndpoint | quote }} + - name: slackWebHook + value: {{ .Values.esGarbageCollect.slack_webhook | quote }} + command: ["/bin/bash" ] + args: + - "-c" + - | + export GEN3_HOME="$HOME/cloud-automation" + source "$GEN3_HOME/gen3/gen3setup.sh" + echo $ESHOST + if gen3 klock lock es-garbage-job gitops 900; then + repoList="$(gen3 es garbage)" + for indexName in $repoList; do + echo "deleting index $indexName" + gen3 es delete "$indexName" + done + if [[ -n "$repoList" && -n "$slackWebHook" && "$slackWebHook" != "None" ]]; then + curl -X POST --data-urlencode "payload={\"text\": \"es-garbage-collect in $(gen3 api hostname): \n\`\`\`\n${repoList}\n\`\`\`\"}" "${slackWebHook}" + fi + gen3 klock unlock es-garbage-job gitops + else + echo "Failed to acquire es-garbage-job lock: exiting without attempting to sync" + fi + echo "Exit code: $?" +{{- end }} \ No newline at end of file diff --git a/helm/etl/templates/etl-job.yaml b/helm/etl/templates/etl-job.yaml new file mode 100644 index 00000000..0056c497 --- /dev/null +++ b/helm/etl/templates/etl-job.yaml @@ -0,0 +1,215 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: etl-cronjob +spec: + suspend: {{ .Values.suspendCronjob }} + schedule: {{ .Values.schedule | quote }} + jobTemplate: + spec: + backoffLimit: 0 + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 12 }} + {{- end }} + labels: + app: gen3job + spec: + shareProcessNamespace: true + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: karpenter.sh/capacity-type + operator: In + values: + - on-demand + - weight: 99 + preference: + matchExpressions: + - key: eks.amazonaws.com/capacityType + operator: In + values: + - ONDEMAND + volumes: + {{- if .Values.legacySupport }} + - name: config-volume + secret: + defaultMode: 420 + secretName: etl-secret + {{- end }} + - name: signal-volume + emptyDir: {} + - name: creds-volume + secret: + secretName: "peregrine-dbcreds" + - name: etl-mapping + configMap: + name: etl-mapping + - name: fence-yaml + configMap: + name: fence + containers: + - name: gen3-spark + image: {{ .Values.image.spark.repository }}:{{ .Values.image.spark.tag }} + ports: + - containerPort: 22 + - containerPort: 9000 + - containerPort: 8030 + - containerPort: 8031 + - containerPort: 8032 + - containerPort: 7077 + readinessProbe: + tcpSocket: + port: 9000 + initialDelaySeconds: 10 + periodSeconds: 30 + env: + - name: DICTIONARY_URL + valueFrom: + configMapKeyRef: + name: manifest-global + key: dictionary_url + - name: HADOOP_URL + value: hdfs://0.0.0.0:9000 + - name: HADOOP_HOST + value: 0.0.0.0 + volumeMounts: + - mountPath: /usr/share/pod + name: signal-volume + readOnly: true + imagePullPolicy: {{ .Values.image.spark.pullPolicy }} + resources: + requests: + cpu: {{ .Values.resources.spark.requests.cpu }} + memory: {{ .Values.resources.spark.requests.memory }} + command: ["/bin/bash" ] + args: + - "-c" + - | + trap 'exit 0' SIGINT SIGQUIT SIGTERM + # get /usr/local/share/ca-certificates/cdis-ca.crt into system bundle + ssh server sudo /etc/init.d/ssh start + # update-ca-certificates + python run_config.py + hdfs namenode -format + hdfs --daemon start namenode + hdfs --daemon start datanode + yarn --daemon start resourcemanager + yarn --daemon start nodemanager + hdfs dfsadmin -safemode leave + hdfs dfs -mkdir /result + hdfs dfs -mkdir /jars + hdfs dfs -mkdir /archive + /spark/sbin/start-all.sh + while true; do sleep 5; done + - name: tube + imagePullPolicy: IfNotPresent + image: {{ .Values.image.tube.repository }}:{{ .Values.image.tube.tag }} + ports: + - containerPort: 80 + env: + - name: DB_HOST + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: host + - name: DB_DATABASE + valueFrom: + secretKeyRef: + name: sheepdog-dbcreds + key: database + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: sheepdog-dbcreds + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: sheepdog-dbcreds + key: password + - name: DB_PORT + valueFrom: + secretKeyRef: + name: sheepdog-dbcreds + key: port + - name: DICTIONARY_URL + valueFrom: + configMapKeyRef: + name: manifest-global + key: dictionary_url + - name: HADOOP_URL + value: hdfs://localhost:9000 + - name: ES_URL + value: {{ .Values.esEndpoint }} + - name: HADOOP_HOST + value: localhost + - name: HADOOP_CLIENT_OPTS + value: -Xmx1g + - name: SPARK_EXECUTOR_MEMORY + value: 4g + - name: SPARK_DRIVER_MEMORY + value: 6g + - name: ETL_FORCED + value: {{ .Values.etlForced | quote }} + - name: gen3Env + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname + - name: slackWebHook + valueFrom: + configMapKeyRef: + name: global + key: slack_webhook + optional: true + volumeMounts: + {{- if .Values.legacySupport }} + - mountPath: /tube/tube/settings.py + name: config-volume + subPath: settings.py + {{- end }} + - mountPath: /usr/share/pod + name: signal-volume + - name: "etl-mapping" + readOnly: true + mountPath: "/gen3/tube/etlMapping.yaml" + subPath: "etlMapping.yaml" + - name: "fence-yaml" + readOnly: true + mountPath: "/gen3/tube/user.yaml" + subPath: user.yaml + resources: + requests: + cpu: {{ .Values.resources.tube.requests.cpu }} + memory: {{ .Values.resources.tube.requests.memory }} + command: ["/bin/bash"] + args: + - "-c" + - | + while ! bash -c "echo >/dev/tcp/localhost/9000"; do + echo "Spark is not ready on port 9000... waiting for 10 seconds." + sleep 10 + done + + # Port 9000 is open, continue with the rest of the script + echo "Port 9000 is now open. Continuing with the script..." + if [[ $ETL_FORCED != "false" ]]; then + echo "python run_config.py && python run_etl.py --force" + python run_config.py && python run_etl.py --force + else + echo "python run_config.py && python run_etl.py" + python run_config.py && python run_etl.py + fi + exitcode=$? + + # Kill sidecar and all processes + echo "Exit code: $exitcode" + pkill -u root && exit $exitcode + exit "$exitcode" & + restartPolicy: Never diff --git a/helm/etl/templates/etl-mapping.yaml b/helm/etl/templates/etl-mapping.yaml new file mode 100644 index 00000000..184a3e25 --- /dev/null +++ b/helm/etl/templates/etl-mapping.yaml @@ -0,0 +1,10 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: etl-mapping +data: + etlMapping.yaml: | + {{- with .Values.etlMapping }} + {{- toYaml . | nindent 4 }} + {{ end }} +--- \ No newline at end of file diff --git a/helm/etl/templates/etl-secret.yaml b/helm/etl/templates/etl-secret.yaml new file mode 100644 index 00000000..21a2b117 --- /dev/null +++ b/helm/etl/templates/etl-secret.yaml @@ -0,0 +1,104 @@ +{{ if .Values.legacySupport }} +kind: Secret +apiVersion: v1 +metadata: + name: etl-secret +stringData: + settings.py: |- + import os + import tube.enums as enums + + from cdislogging import get_logger + from tube.config_helper import find_paths, load_json + from .utils.general import get_resource_paths_from_yaml + + + logger = get_logger("__name__", log_level="warn") + + LIST_TABLES_FILES = "tables.txt" + + # + # Load db credentials from a creds.json file. + # See config_helper.py for paths searched for creds.json + # ex: export XDG_DATA_HOME="$HOME/.local/share" + # and setup $XDG_DATA_HOME/.local/share/gen3/tube/creds.json + # + conf_data = load_json("creds.json", "tube") + DB_HOST = os.getenv("DB_HOST") or conf_data.get("db_host", "localhost") + DB_PORT = os.getenv("DB_PORT") or conf_data.get("db_port", "5432") + DB_DATABASE = os.getenv("DB_DATABASE") or conf_data.get("db_database", "sheepdog") + DB_USERNAME = os.getenv("DB_USERNAME") or conf_data.get("db_username", "peregrine") + DB_PASSWORD = os.getenv("DB_PASSWORD") or conf_data.get("db_password", "unknown") + + DB_USE_SSL = os.getenv("DB_USE_SSL") or conf_data.get( + "db_use_ssl", False + ) # optional property to db_use_ssl + JDBC = ( + "jdbc:postgresql://{}:{}/{}".format(DB_HOST, DB_PORT, DB_DATABASE) + if DB_USE_SSL is False + else "jdbc:postgresql://{}:{}/{}?sslmode=require".format( + DB_HOST, DB_PORT, DB_DATABASE + ) + ) + PYDBC = "postgresql://{}:{}@{}:{}/{}".format( + DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, DB_DATABASE + ) + DICTIONARY_URL = os.getenv( + "DICTIONARY_URL", + "https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json", + ) + ES_URL = os.getenv("ES_URL", "esproxy-service") + + HDFS_DIR = "/result" + # Three modes: Test, Dev, Prod + RUNNING_MODE = os.getenv("RUNNING_MODE", enums.RUNNING_MODE_DEV) # 'Prod' or 'Dev' + + PARALLEL_JOBS = 1 + LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") + + ES = { + "es.nodes": ES_URL, + "es.port": "9200", + "es.input.json": "yes", + "es.nodes.client.only": "false", + "es.nodes.discovery": "false", + "es.nodes.data.only": "false", + "es.nodes.wan.only": "true", + } + + HADOOP_HOME = os.getenv("HADOOP_HOME", "/usr/local/Cellar/hadoop/3.1.0/libexec/") + JAVA_HOME = os.getenv( + "JAVA_HOME", "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home" + ) + HADOOP_URL = os.getenv("HADOOP_URL", "http://spark-service:9000") + ES_HADOOP_VERSION = os.getenv("ES_HADOOP_VERSION", "") + ES_HADOOP_HOME_BIN = "{}/elasticsearch-hadoop-{}".format( + os.getenv("ES_HADOOP_HOME", ""), os.getenv("ES_HADOOP_VERSION", "") + ) + HADOOP_HOST = os.getenv("HADOOP_HOST", "spark-service") + # Searches same folders as load_json above + + try: + MAPPING_FILE = find_paths("etlMapping.yaml", "tube")[0] + except: + MAPPING_FILE = None + + try: + USERYAML_FILE = find_paths("user.yaml", "tube")[0] + except IndexError: + USERYAML_FILE = None + PROJECT_TO_RESOURCE_PATH = get_resource_paths_from_yaml(USERYAML_FILE) + + SPARK_MASTER = os.getenv("SPARK_MASTER", "local[1]") # 'spark-service' + SPARK_EXECUTOR_MEMORY = os.getenv("SPARK_EXECUTOR_MEMORY", "2g") + SPARK_DRIVER_MEMORY = os.getenv("SPARK_DRIVER_MEMORY", "512m") + APP_NAME = "Gen3 ETL" + + os.environ[ + "PYSPARK_SUBMIT_ARGS" + ] = "--jars {}/dist/elasticsearch-spark-20_2.11-{}.jar pyspark-shell".format( + ES_HADOOP_HOME_BIN, ES_HADOOP_VERSION + ) + os.environ["HADOOP_CLIENT_OPTS"] = os.getenv("HADOOP_CLIENT_OPTS", "") + +{{- end }} \ No newline at end of file diff --git a/helm/etl/values.yaml b/helm/etl/values.yaml new file mode 100644 index 00000000..ebf11925 --- /dev/null +++ b/helm/etl/values.yaml @@ -0,0 +1,152 @@ +# populate with normal values from a regular chart created by helm create + +image: + tube: + # -- (string) The Docker image repository for the fence service + repository: quay.io/cdis/tube + # -- (string) When to pull the image. This value should be "Always" to ensure the latest image is used. + pullPolicy: Always + # -- (string) Overrides the image tag whose default is the chart appVersion. + tag: "master" + spark: + # -- (string) The Docker image repository for the spark service + repository: quay.io/cdis/gen3-spark + # -- (string) When to pull the image. This value should be "Always" to ensure the latest image is used. + pullPolicy: Always + # -- (string) Overrides the image tag whose default is the chart appVersion. + tag: "master" + + +# -- (list) Docker image pull secrets. +imagePullSecrets: [] + +# -- (map) Annotations to add to the pod +podAnnotations: {} + + +# -- (map) Resource requests and limits for the containers in the pod +resources: + tube: + # -- (map) The amount of resources that the container requests + requests: + # -- (string) The amount of CPU requested + cpu: 0.3 + # -- (string) The amount of memory requested + memory: 128Mi + spark: + # -- (map) The amount of resources that the container requests + requests: + # -- (string) The amount of CPU requested + cpu: 0.3 + # -- (string) The amount of memory requested + memory: 128Mi + + +esEndpoint: gen3-elasticsearch-master + +etlMapping: + mappings: + - name: dev_case + doc_type: case + type: aggregator + root: case + props: + - name: submitter_id + - name: project_id + - name: disease_type + - name: primary_site + flatten_props: + - path: demographics + props: + - name: gender + value_mappings: + - female: F + - male: M + - name: race + value_mappings: + - american indian or alaskan native: Indian + - name: ethnicity + - name: year_of_birth + aggregated_props: + - name: _samples_count + path: samples + fn: count + - name: _aliquots_count + path: samples.aliquots + fn: count + - name: _submitted_methylations_count + path: samples.aliquots.submitted_methylation_files + fn: count + - name: _submitted_copy_number_files_on_aliquots_count + path: samples.aliquots.submitted_copy_number_files + fn: count + - name: _read_groups_count + path: samples.aliquots.read_groups + fn: count + - name: _submitted_aligned_reads_count + path: samples.aliquots.read_groups.submitted_aligned_reads_files + fn: count + - name: _submitted_unaligned_reads_count + path: samples.aliquots.read_groups.submitted_unaligned_reads_files + fn: count + - name: _submitted_copy_number_files_on_read_groups_count + path: samples.aliquots.read_groups.submitted_copy_number_files + fn: count + - name: _submitted_somatic_mutations_count + path: samples.aliquots.read_groups.submitted_somatic_mutations + fn: count + joining_props: + - index: file + join_on: _case_id + props: + - name: data_format + src: data_format + fn: set + - name: data_type + src: data_type + fn: set + - name: _file_id + src: _file_id + fn: set + - name: dev_file + doc_type: file + type: collector + root: None + category: data_file + props: + - name: object_id + - name: md5sum + - name: file_name + - name: file_size + - name: data_format + - name: data_type + - name: state + injecting_props: + case: + props: + - name: _case_id + src: id + fn: set + - name: project_id + target_nodes: + - name: slide_image + path: slides.samples.cases + +# -- (map) Configuration options for es garbage cronjob. +esGarbageCollect: + # -- (bool) Whether to create es garbage collect cronjob. + enabled: false + # -- (string) The cron schedule expression to use in the es garbage collect cronjob. Runs once a day by default. + schedule: "0 0 * * *" + # -- (string) To set a custom image for the es garbage collect cronjob. Default is the Gen3 Awshelper image. + custom_image: + # -- (string) Slack webhook endpoint to use for cronjob. + slack_webhook: None + +schedule: "*/30 * * * *" + +suspendCronjob: true + +legacySupport: false + +etlForced: "TRUE" diff --git a/helm/faro-collector/Chart.yaml b/helm/faro-collector/Chart.yaml new file mode 100644 index 00000000..ac429f84 --- /dev/null +++ b/helm/faro-collector/Chart.yaml @@ -0,0 +1,30 @@ +apiVersion: v2 +name: alloy +description: A Helm chart for deploying Grafana Alloy + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.2 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "master" + +# Dependencies +dependencies: + - name: alloy + version: "0.9.1" + repository: "https://grafana.github.io/helm-charts" diff --git a/helm/faro-collector/README.md b/helm/faro-collector/README.md new file mode 100644 index 00000000..fd1f86db --- /dev/null +++ b/helm/faro-collector/README.md @@ -0,0 +1,31 @@ +# alloy + +![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) + +A Helm chart for deploying Grafana Alloy + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://grafana.github.io/helm-charts | alloy | 0.9.1 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| alloy.alloy.clustering.enabled | bool | `true` | | +| alloy.alloy.configMap.key | string | `"config"` | | +| alloy.alloy.configMap.name | string | `"alloy-gen3"` | | +| alloy.alloy.extraPorts[0].name | string | `"faro"` | | +| alloy.alloy.extraPorts[0].port | int | `12347` | | +| alloy.alloy.extraPorts[0].protocol | string | `"TCP"` | | +| alloy.alloy.extraPorts[0].targetPort | int | `12347` | | +| alloy.alloyConfigmapData | string | `"logging {\n level = \"info\"\n format = \"json\"\n}\n\notelcol.exporter.otlp \"tempo\" {\n client {\n endpoint = \"http://grafana-tempo-distributor.monitoring:4317\"\n tls {\n insecure = true\n insecure_skip_verify = true\n }\n }\n}\n\n// loki write endpoint\nloki.write \"endpoint\" {\n endpoint {\n url = \"http://grafana-loki-gateway.monitoring:80/loki/api/v1/push\"\n }\n}\n\nfaro.receiver \"default\" {\n server {\n listen_address = \"0.0.0.0\"\n listen_port = 12347\n cors_allowed_origins = [\"*\"]\n }\n\n extra_log_labels = {\n service = \"frontend-app\",\n app_name = \"\",\n app_environment = \"\",\n app_namespace = \"\",\n app_version = \"\",\n }\n output {\n logs = [loki.write.endpoint.receiver]\n traces = [otelcol.exporter.otlp.tempo.input]\n }\n}\n"` | | +| alloy.ingress.annotations | object | `{}` | | +| alloy.ingress.enabled | bool | `true` | Enables ingress for Alloy (Faro port) | +| alloy.ingress.faroPort | int | `12347` | | +| alloy.ingress.hosts[0] | string | `"faro.example.com"` | | +| alloy.ingress.ingressClassName | string | `"alb"` | | +| alloy.ingress.labels | object | `{}` | | +| alloy.ingress.path | string | `"/"` | | diff --git a/helm/faro-collector/SETUP.md b/helm/faro-collector/SETUP.md new file mode 100644 index 00000000..72f5e6c4 --- /dev/null +++ b/helm/faro-collector/SETUP.md @@ -0,0 +1,154 @@ +# Grafana Alloy and Faro + +## Overview + +This guide provides a step-by-step approach to configuring an Alloy instance to collect Grafana Faro logs sent over the internet, similar to Real User Monitoring (RUM). The Portal service generates Faro logs, which Alloy collects and forwards to Loki for storage and analysis in Grafana. Additionally, this guide explains how to enable metrics in the Fence service and adjust the Faro URL in the Gen3 Portal configuration to route metrics to your Alloy instance. Future updates will enable more Gen3 services to offer metric collection. + +Before deploying Alloy, it is important to first deploy the "observability" Helm chart, as it provides the necessary components and configuration for Alloy to function properly. Please refer to the [SETUP.md](https://github.com/uc-cdis/gen3-helm/blob/master/helm/observability/SETUP.md) observability chart documentation for instructions on how to set it up before proceeding with the Alloy deployment. + +### Why Does Faro Require an Internet-Facing Ingress? + +Grafana Faro collects Real User Monitoring (RUM) data, such as performance metrics, errors, and user interactions, via the Fence and Portal services. This data is sent from user devices to the backend, which in this case is Alloy. To enable this communication, an internet-facing ingress is required to expose the Faro endpoint to the public, allowing users' browsers to send RUM data to the Alloy instance over the internet. + +## Configuring Alloy for Faro Logs + +### Helm Chart Configuration + +The ingress is configured with AWS ALB (Application Load Balancer) to expose the Alloy Faro port (12347) to the internet. The alb.ingress.kubernetes.io/scheme annotation ensures that the ALB is internet-facing, allowing users to send logs from their browsers to Alloy. + +When configuring the Faro collector, you will need to update the hosts section of the values.yaml file to match the hostname you plan to use for the Faro collector. For example, replace "faro.example.com" with your desired hostname. + +Additionally, it is highly recommended that you uncomment and adjust the annotations provided for AWS ALB (Application Load Balancer) to fit your environment. These annotations will help ensure proper configuration of the load balancer, SSL certificates, and other key settings. For instance, make sure to replace the placeholder values such as "cert arn", "ssl policy", and "environment name" with your specific details. + +```yaml +alloy: + extraPorts: + - name: "faro" + port: 12347 + targetPort: 12347 + protocol: "TCP" + clustering: + enabled: true + configMap: + name: alloy-gen3 + key: config + +ingress: + enabled: true + ingressClassName: "alb" + annotations: + alb.ingress.kubernetes.io/certificate-arn: + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/ssl-policy: + alb.ingress.kubernetes.io/ssl-redirect: '443' + alb.ingress.kubernetes.io/tags: Environment= + alb.ingress.kubernetes.io/target-type: ip + labels: {} + path: / + faroPort: 12347 + hosts: + - faro.example.com + +alloy-configmap-data: | + logging { + level = "info" + format = "json" + } + + otelcol.exporter.otlp "tempo" { + client { + endpoint = "http://grafana-tempo-distributor.monitoring:4317" + tls { + insecure = true + insecure_skip_verify = true + } + } + } + + loki.write "endpoint" { + endpoint { + url = "http://grafana-loki-gateway.monitoring:80/loki/api/v1/push" + } + } + + faro.receiver "default" { + server { + listen_address = "0.0.0.0" + listen_port = 12347 + cors_allowed_origins = ["*"] + } + + extra_log_labels = { + service = "frontend-app", + app_name = "", + app_environment = "", + app_namespace = "", + app_version = "", + } + output { + logs = [loki.write.endpoint.receiver] + traces = [otelcol.exporter.otlp.tempo.input] + } + } +``` + +### Helm Chart Links +The link below will take you to the Grafana Alloy chart, providing a comprehensive list of configurable options to help you further customize your setup. + +[Alloy Helm Chart](https://github.com/grafana/alloy/blob/main/operations/helm/charts/alloy/values.yaml) + +--- + +## Enabling Faro Metrics in Fence + +Fence now has built-in Faro metrics. To enable these metrics, you must update your Fence deployment. + +*** Note: you must be using Fence version 10.2.0 or later + +### Step 1: Enable Prometheus Metrics in the Fence Pod + +Update your Fence deployment with the following annotations to allow Prometheus to scrape the metrics: + +```yaml +fence: + podAnnotations: + prometheus.io/path: /metrics + prometheus.io/scrape: "true" +``` + +### Step 2: Enable Metrics in the Fence Configuration + +Modify the FENCE_CONFIG_PUBLIC section to enable Prometheus metrics: + +```yaml +fence: + FENCE_CONFIG_PUBLIC: + ENABLE_PROMETHEUS_METRICS: true + ENABLE_DB_MIGRATION: true +``` + +--- + +## Updating Faro URL in Gen3 Portal + +If you need to change the Faro URL that metrics are sent to, you will need to update the "grafanaFaroUrl" field by modifying the "gitops.json" value in your values.yaml. You can refer to [this link](https://github.com/uc-cdis/data-portal/blob/master/docs/portal_config.md) for more information. + +```yaml +portal: + # -- (map) GitOps configuration for portal + gitops: + # -- (string) multiline string - gitops.json + json: | + { + "grafanaFaroConfig": { + "grafanaFaroEnable": true, // optional; flag to turn on Grafana Faro RUM, default to false + "grafanaFaroNamespace": "DEV", // optional; the Grafana Faro RUM option specifying the application’s namespace, for example: prod, pre-prod, staging, etc. Can be determined automatically if omitted. But it is highly recommended to customize it to include project information, such as 'healprod' + "grafanaFaroUrl": "", // optional: the Grafana Faro collector url. Defaults to https://faro.example.com/collect + "grafanaFaroSampleRate": 1, // optional; numeric; the Grafana Faro option specifying the percentage of sessions to track: 1 for all, 0 for none. Default to 1 if omitted + }, +``` +--- + +By following this guide, you'll have successfully set up Alloy to receive Grafana Faro logs and metrics while exposing the service over the internet using Kubernetes ingress. You’ll also be able to monitor Faro metrics through Fence and make necessary configurations in Gen3 Portal for seamless Faro integration. \ No newline at end of file diff --git a/helm/faro-collector/templates/alloy-config.yaml b/helm/faro-collector/templates/alloy-config.yaml new file mode 100644 index 00000000..0bf02875 --- /dev/null +++ b/helm/faro-collector/templates/alloy-config.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: alloy-gen3 +data: + config: | + {{- with .Values.alloy.alloyConfigmapData }} + {{- toYaml . | nindent 4 }} + {{ end }} \ No newline at end of file diff --git a/helm/faro-collector/values.yaml b/helm/faro-collector/values.yaml new file mode 100644 index 00000000..0644b927 --- /dev/null +++ b/helm/faro-collector/values.yaml @@ -0,0 +1,77 @@ +alloy: + alloy: + extraPorts: + - name: "faro" + port: 12347 + targetPort: 12347 + protocol: "TCP" + clustering: + enabled: true + configMap: + name: alloy-gen3 + key: config + + ingress: + # -- Enables ingress for Alloy (Faro port) + enabled: true + # For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName + # See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress + ingressClassName: "alb" + annotations: {} + ## Recommended annotations for AWS ALB (Application Load Balancer). + # alb.ingress.kubernetes.io/certificate-arn: + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + # alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + # alb.ingress.kubernetes.io/scheme: internet-facing + # alb.ingress.kubernetes.io/ssl-policy: + # alb.ingress.kubernetes.io/ssl-redirect: '443' + # alb.ingress.kubernetes.io/tags: Environment= + # alb.ingress.kubernetes.io/target-type: ip + labels: {} + path: / + faroPort: 12347 + hosts: + - faro.example.com + + alloyConfigmapData: | + logging { + level = "info" + format = "json" + } + + otelcol.exporter.otlp "tempo" { + client { + endpoint = "http://grafana-tempo-distributor.monitoring:4317" + tls { + insecure = true + insecure_skip_verify = true + } + } + } + + // loki write endpoint + loki.write "endpoint" { + endpoint { + url = "http://grafana-loki-gateway.monitoring:80/loki/api/v1/push" + } + } + + faro.receiver "default" { + server { + listen_address = "0.0.0.0" + listen_port = 12347 + cors_allowed_origins = ["*"] + } + + extra_log_labels = { + service = "frontend-app", + app_name = "", + app_environment = "", + app_namespace = "", + app_version = "", + } + output { + logs = [loki.write.endpoint.receiver] + traces = [otelcol.exporter.otlp.tempo.input] + } + } diff --git a/helm/fence/Chart.yaml b/helm/fence/Chart.yaml index f9b57743..4df81c7f 100644 --- a/helm/fence/Chart.yaml +++ b/helm/fence/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 +version: 0.1.27 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,10 +23,10 @@ version: 0.1.5 appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/fence/README.md b/helm/fence/README.md index b666ae2b..89908be8 100644 --- a/helm/fence/README.md +++ b/helm/fence/README.md @@ -1,6 +1,6 @@ # fence -![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.27](https://img.shields.io/badge/Version-0.1.27-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Fence @@ -8,14 +8,14 @@ A Helm chart for gen3 Fence | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| -| FENCE_CONFIG | map | `{"ACCESS_TOKEN_COOKIE_NAME":"access_token","ACCESS_TOKEN_EXPIRES_IN":1200,"ALLOWED_USER_SERVICE_ACCOUNT_DOMAINS":["developer.gserviceaccount.com","appspot.gserviceaccount.com","iam.gserviceaccount.com"],"ALLOW_GOOGLE_LINKING":true,"APPLICATION_ROOT":"/user","APP_NAME":"Gen3 Data Commons","ARBORIST":null,"ASSUME_ROLE_CACHE_SECONDS":1800,"AUDIT_SERVICE":"http://audit-service","AUTHLIB_INSECURE_TRANSPORT":true,"AWS_CREDENTIALS":{},"AZ_BLOB_CONTAINER_URL":"https://myfakeblob.blob.core.windows.net/my-fake-container/","AZ_BLOB_CREDENTIALS":null,"BILLING_PROJECT_FOR_SA_CREDS":null,"BILLING_PROJECT_FOR_SIGNED_URLS":null,"CIRRUS_CFG":{"GOOGLE_ADMIN_EMAIL":"","GOOGLE_API_KEY":"","GOOGLE_APPLICATION_CREDENTIALS":"","GOOGLE_CLOUD_IDENTITY_ADMIN_EMAIL":"","GOOGLE_IDENTITY_DOMAIN":"","GOOGLE_PROJECT_ID":"","GOOGLE_STORAGE_CREDS":""},"CLIENT_ALLOWED_SCOPES":["openid","user","data","google_credentials","google_service_account","google_link","ga4gh_passport_v1"],"DATA_UPLOAD_BUCKET":"bucket1","DBGAP_ACCESSION_WITH_CONSENT_REGEX":"(?Pphs[0-9]+)(.(?Pv[0-9]+)){0,1}(.(?Pp[0-9]+)){0,1}.(?Pc[0-9]+)","DEBUG":false,"DEFAULT_LOGIN_IDP":"google","DEFAULT_LOGIN_URL":"{{BASE_URL}}/login/google","DEV_LOGIN_COOKIE_NAME":"dev_login","DREAM_CHALLENGE_GROUP":"DREAM","DREAM_CHALLENGE_TEAM":"DREAM","EMAIL_SERVER":"localhost","ENABLED_IDENTITY_PROVIDERS":{},"ENABLE_AUDIT_LOGS":{"login":false,"presigned_url":false},"ENABLE_AUTOMATIC_BILLING_PERMISSION_SA_CREDS":false,"ENABLE_AUTOMATIC_BILLING_PERMISSION_SIGNED_URLS":false,"ENABLE_CSRF_PROTECTION":true,"ENABLE_DB_MIGRATION":true,"ENABLE_PROMETHEUS_METRICS":false,"ENCRYPTION_KEY":"REPLACEME","GA4GH_VISA_ISSUER_ALLOWLIST":["{{BASE_URL}}","https://sts.nih.gov","https://stsstg.nih.gov"],"GEN3_PASSPORT_EXPIRES_IN":43200,"GLOBAL_PARSE_VISAS_ON_LOGIN":null,"GOOGLE_ACCOUNT_ACCESS_EXPIRES_IN":86400,"GOOGLE_BULK_UPDATES":false,"GOOGLE_GROUP_PREFIX":"","GOOGLE_MANAGED_SERVICE_ACCOUNT_DOMAINS":["dataflow-service-producer-prod.iam.gserviceaccount.com","cloudbuild.gserviceaccount.com","cloud-ml.google.com.iam.gserviceaccount.com","container-engine-robot.iam.gserviceaccount.com","dataflow-service-producer-prod.iam.gserviceaccount.com","sourcerepo-service-accounts.iam.gserviceaccount.com","dataproc-accounts.iam.gserviceaccount.com","gae-api-prod.google.com.iam.gserviceaccount.com","genomics-api.google.com.iam.gserviceaccount.com","containerregistry.iam.gserviceaccount.com","container-analysis.iam.gserviceaccount.com","cloudservices.gserviceaccount.com","stackdriver-service.iam.gserviceaccount.com","appspot.gserviceaccount.com","partnercontent.gserviceaccount.com","trifacta-gcloud-prod.iam.gserviceaccount.com","gcf-admin-robot.iam.gserviceaccount.com","compute-system.iam.gserviceaccount.com","gcp-sa-websecurityscanner.iam.gserviceaccount.com","storage-transfer-service.iam.gserviceaccount.com","firebase-sa-management.iam.gserviceaccount.com","firebase-rules.iam.gserviceaccount.com","gcp-sa-cloudbuild.iam.gserviceaccount.com","gcp-sa-automl.iam.gserviceaccount.com","gcp-sa-datalabeling.iam.gserviceaccount.com","gcp-sa-cloudscheduler.iam.gserviceaccount.com"],"GOOGLE_SERVICE_ACCOUNT_KEY_FOR_URL_SIGNING_EXPIRES_IN":2592000,"GOOGLE_SERVICE_ACCOUNT_PREFIX":"","GOOGLE_USER_SERVICE_ACCOUNT_ACCESS_EXPIRES_IN":604800,"GUN_MAIL":{"datacommons.io":{"api_key":"","api_url":"https://api.mailgun.net/v3/mailgun.example.com","default_login":"postmaster@mailgun.example.com","smtp_hostname":"smtp.mailgun.org","smtp_password":""}},"HTTP_PROXY":{"host":null,"port":3128},"INDEXD":null,"INDEXD_PASSWORD":"","INDEXD_USERNAME":"fence","ITRUST_GLOBAL_LOGOUT":"https://auth.nih.gov/siteminderagent/smlogout.asp?mode=nih&AppReturnUrl=","LOGIN_OPTIONS":[{"desc":"description","idp":"google","name":"Login from Google"}],"LOGIN_REDIRECT_WHITELIST":[],"MAX_ACCESS_TOKEN_TTL":3600,"MAX_API_KEY_TTL":2592000,"MAX_PRESIGNED_URL_TTL":3600,"MAX_ROLE_SESSION_INCREASE":false,"MOCK_AUTH":false,"MOCK_GOOGLE_AUTH":false,"MOCK_STORAGE":false,"OAUTH2_JWT_ALG":"RS256","OAUTH2_JWT_ENABLED":true,"OAUTH2_JWT_ISS":"{{BASE_URL}}","OAUTH2_PROVIDER_ERROR_URI":"/api/oauth2/errors","OAUTH2_TOKEN_EXPIRES_IN":{"authorization_code":1200,"implicit":1200},"OPENID_CONNECT":{"cilogon":{"client_id":"","client_secret":"","discovery_url":"https://cilogon.org/.well-known/openid-configuration","mock":false,"mock_default_user":"http://cilogon.org/serverT/users/64703","redirect_url":"{{BASE_URL}}/login/cilogon/login/","scope":"openid email profile"},"cognito":{"client_id":"","client_secret":"","discovery_url":"https://cognito-idp.{REGION}.amazonaws.com/{USER-POOL-ID}/.well-known/openid-configuration","redirect_url":"{{BASE_URL}}/login/cognito/login/","scope":"openid email"},"fence":{"access_token_url":"{{api_base_url}}/oauth2/token","api_base_url":"","authorize_url":"{{api_base_url}}/oauth2/authorize","client_id":"","client_kwargs":{"redirect_uri":"{{BASE_URL}}/login/fence/login","scope":"openid"},"client_secret":"","mock":false,"mock_default_user":"test@example.com","name":"","refresh_token_url":"{{api_base_url}}/oauth2/token","shibboleth_discovery_url":"https://login.bionimbus.org/Shibboleth.sso/DiscoFeed"},"generic_oidc_idp":{"client_id":"","client_secret":"","discovery":{"authorization_endpoint":"","jwks_uri":"","token_endpoint":""},"discovery_url":"https://server.com/.well-known/openid-configuration","email_field":"","name":"some_idp","redirect_url":"{{BASE_URL}}/login/some_idp/login","scope":"","user_id_field":""},"google":{"client_id":"","client_secret":"","discovery_url":"https://accounts.google.com/.well-known/openid-configuration","mock":"","mock_default_user":"test@example.com","redirect_url":"{{BASE_URL}}/login/google/login/","scope":"openid email"},"microsoft":{"client_id":"","client_secret":"","discovery_url":"https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration","mock":false,"mock_default_user":"test@example.com","redirect_url":"{{BASE_URL}}/login/microsoft/login/","scope":"openid email"},"okta":{"client_id":"","client_secret":"","discovery_url":"","redirect_url":"{{BASE_URL}}/login/okta/login/","scope":"openid email"},"orcid":{"client_id":"","client_secret":"","discovery_url":"https://orcid.org/.well-known/openid-configuration","mock":false,"mock_default_user":"0000-0002-2601-8132","redirect_url":"{{BASE_URL}}/login/orcid/login/","scope":"openid"},"ras":{"client_id":"","client_secret":"","discovery_url":"https://sts.nih.gov/.well-known/openid-configuration","mock":false,"mock_default_user":"test@example.com","redirect_url":"{{BASE_URL}}/login/ras/callback","scope":"openid email profile ga4gh_passport_v1"},"shibboleth":{"client_id":"","client_secret":"","redirect_url":"{{BASE_URL}}/login/shib/login"},"synapse":{"client_id":"","client_secret":"","discovery_url":"","redirect_url":"","scope":"openid"}},"OVERRIDE_NGINX_RATE_LIMIT":18,"PRIVACY_POLICY_URL":null,"PROBLEM_USER_EMAIL_NOTIFICATION":{"admin":["admin@example.edu"],"content":"The Data Commons Framework utilizes dbGaP for data access authorization. Another member of a Google project you belong to ({}) is attempting to register a service account to the following additional datasets ({}). Please contact dbGaP to request access.\n","domain":"example.com","from":"do-not-reply@example.com","subject":"Account access error notification"},"PUSH_AUDIT_LOGS_CONFIG":{"aws_sqs_config":{"aws_cred":null,"region":null,"sqs_url":null},"type":"aws_sqs"},"RAS_REFRESH_EXPIRATION":1296000,"RAS_USERINFO_ENDPOINT":"/openid/connect/v1.1/userinfo","REFRESH_TOKEN_EXPIRES_IN":2592000,"REGISTERED_USERS_GROUP":"","REGISTER_USERS_ON":false,"REMOVE_SERVICE_ACCOUNT_EMAIL_NOTIFICATION":{"admin":["admin@example.edu"],"content":"Service accounts were removed from access control data because some users or service accounts of GCP Project {} are not authorized to access the data sets associated to the service accounts, or do not adhere to the security policies.\n","domain":"example.com","enable":false,"from":"do-not-reply@example.com","subject":"User service account removal notification"},"RENEW_ACCESS_TOKEN_BEFORE_EXPIRATION":false,"S3_BUCKETS":{},"SEND_FROM":"example@gmail.com","SEND_TO":"example@gmail.com","SERVICE_ACCOUNT_LIMIT":6,"SESSION_ALLOWED_SCOPES":["openid","user","credentials","data","admin","google_credentials","google_service_account","google_link","ga4gh_passport_v1"],"SESSION_COOKIE_DOMAIN":null,"SESSION_COOKIE_NAME":"fence","SESSION_COOKIE_SECURE":true,"SESSION_LIFETIME":28800,"SESSION_TIMEOUT":1800,"SHIBBOLETH_HEADER":"persistent_id","SSO_URL":"https://auth.nih.gov/affwebservices/public/saml2sso?SPID={{BASE_URL}}/shibboleth&RelayState=","STORAGE_CREDENTIALS":{},"SUPPORT_EMAIL_FOR_ERRORS":null,"SYNAPSE_AUTHZ_TTL":86400,"SYNAPSE_DISCOVERY_URL":null,"SYNAPSE_JWKS_URI":null,"SYNAPSE_URI":"https://repo-prod.prod.sagebase.org/auth/v1","TOKEN_PROJECTS_CUTOFF":10,"USERSYNC":{"fallback_to_dbgap_sftp":false,"sync_from_visas":false,"visa_types":{"ras":["https://ras.nih.gov/visas/v1","https://ras.nih.gov/visas/v1.1"]}},"USER_ALLOWED_SCOPES":["fence","openid","user","data","admin","google_credentials","google_service_account","google_link","ga4gh_passport_v1"],"WHITE_LISTED_GOOGLE_PARENT_ORGS":[],"WHITE_LISTED_SERVICE_ACCOUNT_EMAILS":[],"WTF_CSRF_SECRET_KEY":"{{ENCRYPTION_KEY}}","dbGaP":[{"decrypt_key":"","enable_common_exchange_area_access":false,"info":{"host":"","password":"","port":22,"proxy":"","proxy_user":"","username":""},"parse_consent_code":true,"protocol":"sftp","study_common_exchange_areas":{"example":"test_common_exchange_area"},"study_to_resource_namespaces":{"_default":["/"],"test_common_exchange_area":["/dbgap/"]}}]}` | Configuration settings for Fence app | +| FENCE_CONFIG | map | `{"ACCESS_TOKEN_COOKIE_NAME":"access_token","ACCESS_TOKEN_EXPIRES_IN":1200,"ALLOWED_USER_SERVICE_ACCOUNT_DOMAINS":["developer.gserviceaccount.com","appspot.gserviceaccount.com","iam.gserviceaccount.com"],"ALLOW_GOOGLE_LINKING":true,"APPLICATION_ROOT":"/user","APP_NAME":"Gen3 Data Commons","ARBORIST":"http://arborist-service","ASSUME_ROLE_CACHE_SECONDS":1800,"AUDIT_SERVICE":"http://audit-service","AUTHLIB_INSECURE_TRANSPORT":true,"AWS_CREDENTIALS":{},"AZ_BLOB_CONTAINER_URL":"https://myfakeblob.blob.core.windows.net/my-fake-container/","AZ_BLOB_CREDENTIALS":null,"BILLING_PROJECT_FOR_SA_CREDS":null,"BILLING_PROJECT_FOR_SIGNED_URLS":null,"CIRRUS_CFG":{"GOOGLE_ADMIN_EMAIL":"","GOOGLE_API_KEY":"","GOOGLE_APPLICATION_CREDENTIALS":"","GOOGLE_CLOUD_IDENTITY_ADMIN_EMAIL":"","GOOGLE_IDENTITY_DOMAIN":"","GOOGLE_PROJECT_ID":"","GOOGLE_STORAGE_CREDS":""},"CLIENT_ALLOWED_SCOPES":["openid","user","data","google_credentials","google_service_account","google_link","ga4gh_passport_v1"],"DATA_UPLOAD_BUCKET":"bucket1","DBGAP_ACCESSION_WITH_CONSENT_REGEX":"(?Pphs[0-9]+)(.(?Pv[0-9]+)){0,1}(.(?Pp[0-9]+)){0,1}.(?Pc[0-9]+)","DEBUG":false,"DEFAULT_LOGIN_IDP":"google","DEFAULT_LOGIN_URL":"{{BASE_URL}}/login/google","DEV_LOGIN_COOKIE_NAME":"dev_login","DREAM_CHALLENGE_GROUP":"DREAM","DREAM_CHALLENGE_TEAM":"DREAM","EMAIL_SERVER":"localhost","ENABLED_IDENTITY_PROVIDERS":{},"ENABLE_AUDIT_LOGS":{"login":false,"presigned_url":false},"ENABLE_AUTOMATIC_BILLING_PERMISSION_SA_CREDS":false,"ENABLE_AUTOMATIC_BILLING_PERMISSION_SIGNED_URLS":false,"ENABLE_CSRF_PROTECTION":true,"ENABLE_DB_MIGRATION":true,"ENABLE_PROMETHEUS_METRICS":false,"ENCRYPTION_KEY":"REPLACEME","GA4GH_VISA_ISSUER_ALLOWLIST":["{{BASE_URL}}","https://sts.nih.gov","https://stsstg.nih.gov"],"GEN3_PASSPORT_EXPIRES_IN":43200,"GLOBAL_PARSE_VISAS_ON_LOGIN":false,"GOOGLE_ACCOUNT_ACCESS_EXPIRES_IN":86400,"GOOGLE_BULK_UPDATES":false,"GOOGLE_GROUP_PREFIX":"","GOOGLE_MANAGED_SERVICE_ACCOUNT_DOMAINS":["dataflow-service-producer-prod.iam.gserviceaccount.com","cloudbuild.gserviceaccount.com","cloud-ml.google.com.iam.gserviceaccount.com","container-engine-robot.iam.gserviceaccount.com","dataflow-service-producer-prod.iam.gserviceaccount.com","sourcerepo-service-accounts.iam.gserviceaccount.com","dataproc-accounts.iam.gserviceaccount.com","gae-api-prod.google.com.iam.gserviceaccount.com","genomics-api.google.com.iam.gserviceaccount.com","containerregistry.iam.gserviceaccount.com","container-analysis.iam.gserviceaccount.com","cloudservices.gserviceaccount.com","stackdriver-service.iam.gserviceaccount.com","appspot.gserviceaccount.com","partnercontent.gserviceaccount.com","trifacta-gcloud-prod.iam.gserviceaccount.com","gcf-admin-robot.iam.gserviceaccount.com","compute-system.iam.gserviceaccount.com","gcp-sa-websecurityscanner.iam.gserviceaccount.com","storage-transfer-service.iam.gserviceaccount.com","firebase-sa-management.iam.gserviceaccount.com","firebase-rules.iam.gserviceaccount.com","gcp-sa-cloudbuild.iam.gserviceaccount.com","gcp-sa-automl.iam.gserviceaccount.com","gcp-sa-datalabeling.iam.gserviceaccount.com","gcp-sa-cloudscheduler.iam.gserviceaccount.com"],"GOOGLE_SERVICE_ACCOUNT_KEY_FOR_URL_SIGNING_EXPIRES_IN":2592000,"GOOGLE_SERVICE_ACCOUNT_PREFIX":"","GOOGLE_USER_SERVICE_ACCOUNT_ACCESS_EXPIRES_IN":604800,"GUN_MAIL":{"datacommons.io":{"api_key":"","api_url":"https://api.mailgun.net/v3/mailgun.example.com","default_login":"postmaster@mailgun.example.com","smtp_hostname":"smtp.mailgun.org","smtp_password":""}},"HTTP_PROXY":{"host":null,"port":3128},"INDEXD":"http://indexd-service","INDEXD_PASSWORD":"","INDEXD_USERNAME":"fence","ITRUST_GLOBAL_LOGOUT":"https://auth.nih.gov/siteminderagent/smlogout.asp?mode=nih&AppReturnUrl=","LOGIN_OPTIONS":[{"desc":"description","idp":"google","name":"Login from Google"}],"LOGIN_REDIRECT_WHITELIST":[],"MAX_ACCESS_TOKEN_TTL":3600,"MAX_API_KEY_TTL":2592000,"MAX_PRESIGNED_URL_TTL":3600,"MAX_ROLE_SESSION_INCREASE":false,"MOCK_AUTH":false,"MOCK_GOOGLE_AUTH":false,"MOCK_STORAGE":false,"OAUTH2_JWT_ALG":"RS256","OAUTH2_JWT_ENABLED":true,"OAUTH2_JWT_ISS":"{{BASE_URL}}","OAUTH2_PROVIDER_ERROR_URI":"/api/oauth2/errors","OAUTH2_TOKEN_EXPIRES_IN":{"authorization_code":1200,"implicit":1200},"OPENID_CONNECT":{"cilogon":{"client_id":"","client_secret":"","discovery_url":"https://cilogon.org/.well-known/openid-configuration","mock":false,"mock_default_user":"http://cilogon.org/serverT/users/64703","redirect_url":"{{BASE_URL}}/login/cilogon/login/","scope":"openid email profile"},"cognito":{"client_id":"","client_secret":"","discovery_url":"https://cognito-idp.{REGION}.amazonaws.com/{USER-POOL-ID}/.well-known/openid-configuration","redirect_url":"{{BASE_URL}}/login/cognito/login/","scope":"openid email"},"fence":{"access_token_url":"{{api_base_url}}/oauth2/token","api_base_url":"","authorize_url":"{{api_base_url}}/oauth2/authorize","client_id":"","client_kwargs":{"redirect_uri":"{{BASE_URL}}/login/fence/login","scope":"openid"},"client_secret":"","mock":false,"mock_default_user":"test@example.com","name":"","refresh_token_url":"{{api_base_url}}/oauth2/token","shibboleth_discovery_url":"https://login.bionimbus.org/Shibboleth.sso/DiscoFeed"},"generic_oidc_idp":{"client_id":"","client_secret":"","discovery":{"authorization_endpoint":"","jwks_uri":"","token_endpoint":""},"discovery_url":"https://server.com/.well-known/openid-configuration","email_field":"","name":"some_idp","redirect_url":"{{BASE_URL}}/login/some_idp/login","scope":"","user_id_field":""},"google":{"client_id":"","client_secret":"","discovery_url":"https://accounts.google.com/.well-known/openid-configuration","mock":"","mock_default_user":"test@example.com","redirect_url":"{{BASE_URL}}/login/google/login/","scope":"openid email"},"microsoft":{"client_id":"","client_secret":"","discovery_url":"https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration","mock":false,"mock_default_user":"test@example.com","redirect_url":"{{BASE_URL}}/login/microsoft/login/","scope":"openid email"},"okta":{"client_id":"","client_secret":"","discovery_url":"","redirect_url":"{{BASE_URL}}/login/okta/login/","scope":"openid email"},"orcid":{"client_id":"","client_secret":"","discovery_url":"https://orcid.org/.well-known/openid-configuration","mock":false,"mock_default_user":"0000-0002-2601-8132","redirect_url":"{{BASE_URL}}/login/orcid/login/","scope":"openid"},"ras":{"client_id":"","client_secret":"","discovery_url":"https://sts.nih.gov/.well-known/openid-configuration","mock":false,"mock_default_user":"test@example.com","redirect_url":"{{BASE_URL}}/login/ras/callback","scope":"openid email profile ga4gh_passport_v1"},"shibboleth":{"client_id":"","client_secret":"","redirect_url":"{{BASE_URL}}/login/shib/login"},"synapse":{"client_id":"","client_secret":"","discovery_url":"","redirect_url":"","scope":"openid"}},"OVERRIDE_NGINX_RATE_LIMIT":18,"PRIVACY_POLICY_URL":null,"PROBLEM_USER_EMAIL_NOTIFICATION":{"admin":["admin@example.edu"],"content":"The Data Commons Framework utilizes dbGaP for data access authorization. Another member of a Google project you belong to ({}) is attempting to register a service account to the following additional datasets ({}). Please contact dbGaP to request access.\n","domain":"example.com","from":"do-not-reply@example.com","subject":"Account access error notification"},"PUSH_AUDIT_LOGS_CONFIG":{"aws_sqs_config":{"aws_cred":null,"region":null,"sqs_url":null},"type":"aws_sqs"},"RAS_REFRESH_EXPIRATION":1296000,"RAS_USERINFO_ENDPOINT":"/openid/connect/v1.1/userinfo","REFRESH_TOKEN_EXPIRES_IN":2592000,"REGISTERED_USERS_GROUP":"","REGISTER_USERS_ON":false,"REMOVE_SERVICE_ACCOUNT_EMAIL_NOTIFICATION":{"admin":["admin@example.edu"],"content":"Service accounts were removed from access control data because some users or service accounts of GCP Project {} are not authorized to access the data sets associated to the service accounts, or do not adhere to the security policies.\n","domain":"example.com","enable":false,"from":"do-not-reply@example.com","subject":"User service account removal notification"},"RENEW_ACCESS_TOKEN_BEFORE_EXPIRATION":false,"S3_BUCKETS":{},"SEND_FROM":"example@gmail.com","SEND_TO":"example@gmail.com","SERVICE_ACCOUNT_LIMIT":6,"SESSION_ALLOWED_SCOPES":["openid","user","credentials","data","admin","google_credentials","google_service_account","google_link","ga4gh_passport_v1"],"SESSION_COOKIE_DOMAIN":null,"SESSION_COOKIE_NAME":"fence","SESSION_COOKIE_SECURE":true,"SESSION_LIFETIME":28800,"SESSION_TIMEOUT":1800,"SHIBBOLETH_HEADER":"persistent_id","SSO_URL":"https://auth.nih.gov/affwebservices/public/saml2sso?SPID={{BASE_URL}}/shibboleth&RelayState=","STORAGE_CREDENTIALS":{},"SUPPORT_EMAIL_FOR_ERRORS":null,"SYNAPSE_AUTHZ_TTL":86400,"SYNAPSE_DISCOVERY_URL":null,"SYNAPSE_JWKS_URI":null,"SYNAPSE_URI":"https://repo-prod.prod.sagebase.org/auth/v1","TOKEN_PROJECTS_CUTOFF":10,"USERSYNC":{"fallback_to_dbgap_sftp":false,"sync_from_visas":false,"visa_types":{"ras":["https://ras.nih.gov/visas/v1","https://ras.nih.gov/visas/v1.1"]}},"USER_ALLOWED_SCOPES":["fence","openid","user","data","admin","google_credentials","google_service_account","google_link","ga4gh_passport_v1"],"WHITE_LISTED_GOOGLE_PARENT_ORGS":[],"WHITE_LISTED_SERVICE_ACCOUNT_EMAILS":[],"WTF_CSRF_SECRET_KEY":"{{ENCRYPTION_KEY}}","dbGaP":[{"decrypt_key":"","enable_common_exchange_area_access":false,"info":{"host":"","password":"","port":22,"proxy":"","username":""},"parse_consent_code":true,"protocol":"sftp","study_common_exchange_areas":{"example":"test_common_exchange_area"},"study_to_resource_namespaces":{"_default":["/"],"test_common_exchange_area":["/dbgap/"]}}]}` | Private configuration settings for Fence app | | FENCE_CONFIG.APP_NAME | string | `"Gen3 Data Commons"` | Name of the Fence app | | FENCE_CONFIG.AUTHLIB_INSECURE_TRANSPORT | bool | `true` | allow OIDC traffic on http for development. By default it requires https. WARNING: ONLY set to true when fence will be deployed in such a way that it will ONLY receive traffic from internal clients and can safely use HTTP. | | FENCE_CONFIG.CLIENT_ALLOWED_SCOPES | list | `["openid","user","data","google_credentials","google_service_account","google_link","ga4gh_passport_v1"]` | These are the *possible* scopes a client can be given, NOT scopes that are given to all clients. You can be more restrictive during client creation | @@ -69,7 +69,8 @@ A Helm chart for gen3 Fence | FENCE_CONFIG.SESSION_COOKIE_SECURE | bool | `true` | set if you want browsers to only send cookies with requests over HTTPS | | FENCE_CONFIG.USER_ALLOWED_SCOPES | list | `["fence","openid","user","data","admin","google_credentials","google_service_account","google_link","ga4gh_passport_v1"]` | these are the scopes that CAN be included in a user's own access_token | | FENCE_CONFIG.WTF_CSRF_SECRET_KEY | str | `"{{ENCRYPTION_KEY}}"` | signing key for WTForms to sign CSRF tokens with | -| USER_YAML | string | `"cloud_providers: {}\ngroups: {}\nauthz:\n # policies automatically given to anyone, even if they haven't authenticated\n anonymous_policies: ['open_data_reader', 'full_open_access']\n\n # policies automatically given to authenticated users (in addition to their other\n # policies)\n all_users_policies: ['open_data_reader', 'authn_open_access']\n\n user_project_to_resource:\n QA: /programs/QA\n DEV: /programs/DEV\n test: /programs/QA/projects/test\n jenkins: /programs/jnkns/projects/jenkins\n jenkins2: /programs/jnkns/projects/jenkins2\n jnkns: /programs/jnkns\n\n policies:\n # General Access\n - id: 'workspace'\n description: 'be able to use workspace'\n resource_paths: ['/workspace']\n role_ids: ['workspace_user']\n - id: 'dashboard'\n description: 'be able to use the commons dashboard'\n resource_paths: ['/dashboard']\n role_ids: ['dashboard_user']\n - id: 'prometheus'\n description: 'be able to use prometheus'\n resource_paths: ['/prometheus']\n role_ids: ['prometheus_user']\n - id: 'ttyadmin'\n description: 'be able to use the admin tty'\n resource_paths: ['/ttyadmin']\n role_ids: ['ttyadmin_user']\n - id: 'mds_admin'\n description: 'be able to use metadata service'\n resource_paths: ['/mds_gateway']\n role_ids: ['mds_user']\n - id: 'data_upload'\n description: 'upload raw data files to S3'\n role_ids: ['file_uploader']\n resource_paths: ['/data_file']\n - description: be able to use sower job\n id: sower\n resource_paths: [/sower]\n role_ids: [sower_user]\n - id: 'mariner_admin'\n description: 'full access to mariner API'\n resource_paths: ['/mariner']\n role_ids: ['mariner_admin']\n - id: audit_reader\n role_ids:\n - audit_reader\n resource_paths:\n - /services/audit\n - id: audit_login_reader\n role_ids:\n - audit_reader\n resource_paths:\n - /services/audit/login\n - id: audit_presigned_url_reader\n role_ids:\n - audit_reader\n resource_paths:\n - /services/audit/presigned_url\n - id: requestor_admin\n role_ids:\n - requestor_admin\n resource_paths:\n - /programs\n - id: requestor_reader\n role_ids:\n - requestor_reader\n resource_paths:\n - /programs\n - id: requestor_creator\n role_ids:\n - requestor_creator\n resource_paths:\n - /programs\n - id: requestor_updater\n role_ids:\n - requestor_updater\n resource_paths:\n - /programs\n - id: requestor_deleter\n role_ids:\n - requestor_deleter\n resource_paths:\n - /programs\n # Data Access\n\n # All programs policy\n - id: 'all_programs_reader'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths: ['/programs']\n\n # # example if need access to write to storage\n # - id: 'programs.jnkns-storage_writer'\n # description: ''\n # role_ids:\n # - 'storage_writer'\n # resource_paths: ['/programs/jnkns']\n\n - id: 'programs.jnkns-admin'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_reader'\n resource_paths:\n - '/programs/jnkns'\n - '/gen3/programs/jnkns'\n\n - id: 'programs.jnkns-viewer'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths:\n - '/programs/jnkns'\n - '/gen3/programs/jnkns'\n\n\n - id: 'programs.QA-admin'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_reader'\n resource_paths:\n - '/programs/QA'\n - '/gen3/programs/QA'\n\n - id: 'programs.QA-admin-no-storage'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n resource_paths:\n - '/programs/QA'\n - '/gen3/programs/QA'\n\n - id: 'programs.QA-viewer'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths:\n - '/programs/QA'\n - '/gen3/programs/QA'\n\n - id: 'programs.DEV-admin'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_reader'\n - 'storage_writer'\n resource_paths:\n - '/programs/DEV'\n - '/gen3/programs/DEV'\n\n - id: 'programs.DEV-storage_writer'\n description: ''\n role_ids:\n - 'storage_writer'\n resource_paths: ['/programs/DEV']\n\n - id: 'programs.DEV-viewer'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths:\n - '/programs/DEV'\n - '/gen3/programs/DEV'\n\n - id: 'programs.test-admin'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_reader'\n resource_paths:\n - '/programs/test'\n - '/gen3/programs/test'\n\n - id: 'programs.test-viewer'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths:\n - '/programs/test'\n - '/gen3/programs/test'\n\n - id: 'abc-admin'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_reader'\n resource_paths:\n - '/abc'\n\n - id: 'gen3-admin'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_reader'\n resource_paths:\n - '/gen3'\n\n - id: 'gen3-hmb-researcher'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_reader'\n resource_paths:\n - '/consents/NRES'\n - '/consents/GRU'\n - '/consents/GRU_CC'\n - '/consents/HMB'\n - '/gen3'\n\n - id: 'abc.programs.test_program.projects.test_project1-viewer'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths:\n - '/abc/programs/test_program/projects/test_project1'\n\n - id: 'abc.programs.test_program.projects.test_project2-viewer'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths:\n - '/abc/programs/test_program/projects/test_project2'\n\n - id: 'abc.programs.test_program2.projects.test_project3-viewer'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths:\n - '/abc/programs/test_program2/projects/test_project3'\n\n # Open data policies\n - id: 'authn_open_access'\n resource_paths: ['/programs/open/projects/authnRequired']\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n - id: 'full_open_access'\n resource_paths: ['/programs/open/projects/1000G']\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n - id: 'open_data_reader'\n description: ''\n role_ids:\n - 'reader'\n - 'storage_reader'\n resource_paths: ['/open']\n - id: 'open_data_admin'\n description: ''\n role_ids:\n - 'creator'\n - 'reader'\n - 'updater'\n - 'deleter'\n - 'storage_writer'\n - 'storage_reader'\n resource_paths: ['/open']\n\n # Consent Code Policies\n - id: 'not-for-profit-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/NPU'\n\n - id: 'publication-required-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/PUB'\n\n - id: 'gru-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/NRES'\n - '/consents/GRU'\n\n - id: 'gru-cc-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/NRES'\n - '/consents/GRU'\n - '/consents/GRU_CC'\n\n - id: 'hmb-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/NRES'\n - '/consents/GRU'\n - '/consents/GRU_CC'\n - '/consents/HMB'\n\n - id: 'poa-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/NRES'\n - '/consents/GRU'\n - '/consents/GRU_CC'\n - '/consents/POA'\n\n - id: 'ds-lung-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/NRES'\n - '/consents/GRU'\n - '/consents/GRU_CC'\n - '/consents/HMB'\n - '/consents/DS_LungDisease'\n\n - id: 'ds-chronic-obstructive-pulmonary-disease-researcher'\n description: ''\n role_ids:\n - 'admin'\n resource_paths:\n - '/consents/NRES'\n - '/consents/GRU'\n - '/consents/GRU_CC'\n - '/consents/HMB'\n - '/consents/DS_ChronicObstructivePulmonaryDisease'\n\n - id: 'services.sheepdog-admin'\n description: 'CRUD access to programs and projects'\n role_ids:\n - 'sheepdog_admin'\n resource_paths:\n - '/services/sheepdog/submission/program'\n - '/services/sheepdog/submission/project'\n\n # indexd\n - id: 'indexd_admin'\n description: 'full access to indexd API'\n role_ids:\n - 'indexd_admin'\n resource_paths:\n - '/programs'\n - '/services/indexd/admin'\n # # TODO resource path '/' is not valid right now in arborist, trying to decide\n # # how to handle all resources\n # - id: 'indexd_admin'\n # description: ''\n # role_ids:\n # - 'indexd_record_creator'\n # - 'indexd_record_reader'\n # - 'indexd_record_updater'\n # - 'indexd_delete_record'\n # - 'indexd_storage_reader'\n # - 'indexd_storage_writer'\n # resource_paths: ['/']\n # - id: 'indexd_record_reader'\n # description: ''\n # role_ids:\n # - 'indexd_record_reader'\n # resource_paths: ['/']\n # - id: 'indexd_record_editor'\n # description: ''\n # role_ids:\n # - 'indexd_record_creator'\n # - 'indexd_record_reader'\n # - 'indexd_record_updater'\n # - 'indexd_delete_record'\n # resource_paths: ['/']\n # - id: 'indexd_storage_reader'\n # description: ''\n # role_ids:\n # - 'indexd_storage_reader'\n # resource_paths: ['/']\n # - id: 'indexd_storage_editor'\n # description: ''\n # role_ids:\n # - 'indexd_storage_reader'\n # - 'indexd_storage_writer'\n # resource_paths: ['/']\n\n # argo\n - id: argo\n description: be able to use argo\n resource_paths: [/argo]\n role_ids: [argo_user]\n\n resources:\n # General Access\n - name: 'data_file'\n description: 'data files, stored in S3'\n - name: 'dashboard'\n description: 'commons /dashboard'\n - name: 'mds_gateway'\n description: 'commons /mds-admin'\n - name: 'prometheus'\n description: 'commons /prometheus and /grafana'\n - name: 'ttyadmin'\n description: 'commons /ttyadmin'\n - name: 'workspace'\n - name: \"sower\"\n - name: 'mariner'\n description: 'workflow execution service'\n - name: argo\n\n # OLD Data\n - name: 'programs'\n subresources:\n - name: 'open'\n subresources:\n - name: 'projects'\n subresources:\n - name: '1000G'\n - name: 'authnRequired'\n - name: 'QA'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test'\n - name: 'DEV'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test'\n - name: 'jnkns'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'jenkins'\n - name: 'jenkins2'\n - name: 'test'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test'\n\n # NEW Data WITH PREFIX\n - name: 'gen3'\n subresources:\n - name: 'programs'\n subresources:\n - name: 'QA'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test'\n - name: 'DEV'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test'\n - name: 'jnkns'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'jenkins'\n - name: 'jenkins2'\n - name: 'test'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test'\n\n # consents obtained from DUO and NIH\n # https://github.com/EBISPOT/DUO\n # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4721915/\n - name: 'consents'\n subresources:\n - name: 'NRES'\n description: 'no restriction'\n - name: 'GRU'\n description: 'general research use'\n - name: 'GRU_CC'\n description: 'general research use and clinical care'\n - name: 'HMB'\n description: 'health/medical/biomedical research'\n - name: 'POA'\n description: 'population origins or ancestry research'\n - name: 'NMDS'\n description: 'no general methods research'\n - name: 'NPU'\n description: 'not-for-profit use only'\n - name: 'PUB'\n description: 'publication required'\n - name: 'DS_LungDisease'\n description: 'disease-specific research for lung disease'\n - name: 'DS_ChronicObstructivePulmonaryDisease'\n description: 'disease-specific research for chronic obstructive pulmonary disease'\n\n - name: 'abc'\n subresources:\n - name: 'programs'\n subresources:\n - name: 'foo'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'bar'\n - name: 'test_program'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test_project1'\n - name: 'test_project2'\n - name: 'test_program2'\n subresources:\n - name: 'projects'\n subresources:\n - name: 'test_project3'\n\n\n # \"Sheepdog admin\" resources\n - name: 'services'\n subresources:\n - name: 'sheepdog'\n subresources:\n - name: 'submission'\n subresources:\n - name: 'program'\n - name: 'project'\n - name: 'indexd'\n subresources:\n - name: 'admin'\n - name: 'bundles'\n - name: audit\n subresources:\n - name: presigned_url\n - name: login\n\n\n - name: 'open'\n\n # action/methods:\n # create, read, update, delete, read-storage, write-storage,\n # file_upload, access\n roles:\n # General Access\n - id: 'file_uploader'\n description: 'can upload data files'\n permissions:\n - id: 'file_upload'\n action:\n service: '*'\n method: 'file_upload'\n - id: 'workspace_user'\n permissions:\n - id: 'workspace_access'\n action:\n service: 'jupyterhub'\n method: 'access'\n - id: 'dashboard_user'\n permissions:\n - id: 'dashboard_access'\n action:\n service: 'dashboard'\n method: 'access'\n - id: 'mds_user'\n permissions:\n - id: 'mds_access'\n action:\n service: 'mds_gateway'\n method: 'access'\n - id: 'prometheus_user'\n permissions:\n - id: 'prometheus_access'\n action:\n service: 'prometheus'\n method: 'access'\n - id: 'ttyadmin_user'\n permissions:\n - id: 'ttyadmin_access'\n action:\n service: 'ttyadmin'\n method: 'access'\n - id: 'sower_user'\n permissions:\n - id: 'sower_access'\n action:\n service: 'job'\n method: 'access'\n - id: 'mariner_admin'\n permissions:\n - id: 'mariner_access'\n action:\n service: 'mariner'\n method: 'access'\n - id: audit_reader\n permissions:\n - id: audit_reader_action\n action:\n service: audit\n method: read\n\n # All services\n - id: 'admin'\n description: ''\n permissions:\n - id: 'admin'\n action:\n service: '*'\n method: '*'\n - id: 'creator'\n description: ''\n permissions:\n - id: 'creator'\n action:\n service: '*'\n method: 'create'\n - id: 'reader'\n description: ''\n permissions:\n - id: 'reader'\n action:\n service: '*'\n method: 'read'\n - id: 'updater'\n description: ''\n permissions:\n - id: 'updater'\n action:\n service: '*'\n method: 'update'\n - id: 'deleter'\n description: ''\n permissions:\n - id: 'deleter'\n action:\n service: '*'\n method: 'delete'\n - id: 'storage_writer'\n description: ''\n permissions:\n - id: 'storage_writer'\n action:\n service: '*'\n method: 'write-storage'\n - id: 'storage_reader'\n description: ''\n permissions:\n - id: 'storage_reader'\n action:\n service: '*'\n method: 'read-storage'\n\n\n # Sheepdog admin role\n - id: 'sheepdog_admin'\n description: 'sheepdog admin role for program project crud'\n permissions:\n - id: 'sheepdog_admin_action'\n action:\n service: 'sheepdog'\n method: '*'\n\n\n # indexd\n - id: 'indexd_admin'\n # this only works if indexd.arborist is enabled in manifest!\n description: 'full access to indexd API'\n permissions:\n - id: 'indexd_admin'\n action:\n service: 'indexd'\n method: '*'\n - id: 'indexd_record_creator'\n description: ''\n permissions:\n - id: 'indexd_record_creator'\n action:\n service: 'indexd'\n method: 'create'\n - id: 'indexd_record_reader'\n description: ''\n permissions:\n - id: 'indexd_record_reader'\n action:\n service: 'indexd'\n method: 'read'\n - id: 'indexd_record_updater'\n description: ''\n permissions:\n - id: 'indexd_record_updater'\n action:\n service: 'indexd'\n method: 'update'\n - id: 'indexd_delete_record'\n description: ''\n permissions:\n - id: 'indexd_delete_record'\n action:\n service: 'indexd'\n method: 'delete'\n - id: 'indexd_storage_reader'\n description: ''\n permissions:\n - id: 'indexd_storage_reader'\n action:\n service: 'indexd'\n method: 'read-storage'\n - id: 'indexd_storage_writer'\n description: ''\n permissions:\n - id: 'indexd_storage_writer'\n action:\n service: 'indexd'\n method: 'write-storage'\n\n # arborist\n - id: 'arborist_creator'\n description: ''\n permissions:\n - id: 'arborist_creator'\n action:\n service: 'arborist'\n method: 'create'\n - id: 'arborist_reader'\n description: ''\n permissions:\n - id: 'arborist_reader'\n action:\n service: 'arborist'\n method: 'read'\n - id: 'arborist_updater'\n description: ''\n permissions:\n - id: 'arborist_updater'\n action:\n service: 'arborist'\n method: 'update'\n - id: 'arborist_deleter'\n description: ''\n permissions:\n - id: 'arborist_deleter'\n action:\n service: 'arborist'\n method: 'delete'\n\n # requestor\n - id: requestor_admin\n permissions:\n - id: requestor_admin_action\n action:\n service: requestor\n method: '*'\n - id: requestor_reader\n permissions:\n - id: requestor_reader_action\n action:\n service: requestor\n method: read\n - id: requestor_creator\n permissions:\n - id: requestor_creator_action\n action:\n service: requestor\n method: create\n - id: requestor_updater\n permissions:\n - id: requestor_updater_action\n action:\n service: requestor\n method: update\n - id: requestor_deleter\n permissions:\n - id: requestor_deleter_action\n action:\n service: requestor\n method: delete\n # argo\n - id: argo_user\n permissions:\n - id: argo_access\n action:\n service: argo\n method: access\n\nclients:\n basic-test-client:\n policies:\n - abc-admin\n - gen3-admin\n basic-test-abc-client:\n policies:\n - abc-admin\n wts:\n policies:\n - all_programs_reader\n - workspace\n\nusers:\n ### BEGIN INTERNS SECTION ###\n ### END INTERNS SECTION ###\n qureshi@uchicago.edu:\n admin: true\n policies:\n - data_upload\n - workspace\n - dashboard\n - mds_admin\n - prometheus\n - sower\n - services.sheepdog-admin\n - programs.QA-admin\n - programs.test-admin\n - programs.DEV-admin\n - programs.jnkns-admin\n - indexd_admin\n - ttyadmin\n projects:\n - auth_id: QA\n privilege: [create, read, update, delete, upload, read-storage]\n - auth_id: test\n privilege: [create, read, update, delete, upload, read-storage]\n - auth_id: DEV\n privilege: [create, read, update, delete, upload, read-storage]\n - auth_id: jenkins\n privilege: [create, read, update, delete, upload, read-storage]\n - auth_id: jenkins2\n privilege: [create, read, update, delete, upload, read-storage]\n - auth_id: jnkns\n privilege: [create, read, update, delete, upload, read-storage]\n"` | USER YAML. Passed in as a multiline string. | +| FENCE_CONFIG_PUBLIC | map | `{}` | Public configuration settings for Fence app | +| USER_YAML | string | `"cloud_providers: {}\nauthz:\n # policies automatically given to anyone, even if they are not authenticated\n anonymous_policies:\n - open_data_reader\n\n # policies automatically given to authenticated users (in addition to their other policies)\n all_users_policies: []\n\n groups:\n # can CRUD programs and projects and upload data files\n - name: data_submitters\n policies:\n - services.sheepdog-admin\n - data_upload\n - MyFirstProject_submitter\n users:\n - username1@gmail.com\n\n # can create/update/delete indexd records\n - name: indexd_admins\n policies:\n - indexd_admin\n users:\n - username1@gmail.com\n\n resources:\n - name: workspace\n - name: data_file\n - name: services\n subresources:\n - name: sheepdog\n subresources:\n - name: submission\n subresources:\n - name: program\n - name: project\n - name: 'indexd'\n subresources:\n - name: 'admin'\n - name: audit\n subresources:\n - name: presigned_url\n - name: login\n - name: open\n - name: programs\n subresources:\n - name: MyFirstProgram\n subresources:\n - name: projects\n subresources:\n - name: MyFirstProject\n\n policies:\n - id: workspace\n description: be able to use workspace\n resource_paths:\n - /workspace\n role_ids:\n - workspace_user\n - id: data_upload\n description: upload raw data files to S3\n role_ids:\n - file_uploader\n resource_paths:\n - /data_file\n - id: services.sheepdog-admin\n description: CRUD access to programs and projects\n role_ids:\n - sheepdog_admin\n resource_paths:\n - /services/sheepdog/submission/program\n - /services/sheepdog/submission/project\n - id: indexd_admin\n description: full access to indexd API\n role_ids:\n - indexd_admin\n resource_paths:\n - /programs\n - id: open_data_reader\n role_ids:\n - peregrine_reader\n - guppy_reader\n - fence_storage_reader\n resource_paths:\n - /open\n - id: all_programs_reader\n role_ids:\n - peregrine_reader\n - guppy_reader\n - fence_storage_reader\n resource_paths:\n - /programs\n - id: MyFirstProject_submitter\n role_ids:\n - reader\n - creator\n - updater\n - deleter\n - storage_reader\n - storage_writer\n resource_paths:\n - /programs/MyFirstProgram/projects/MyFirstProject\n\n roles:\n - id: file_uploader\n permissions:\n - id: file_upload\n action:\n service: fence\n method: file_upload\n - id: workspace_user\n permissions:\n - id: workspace_access\n action:\n service: jupyterhub\n method: access\n - id: sheepdog_admin\n description: CRUD access to programs and projects\n permissions:\n - id: sheepdog_admin_action\n action:\n service: sheepdog\n method: '*'\n - id: indexd_admin\n description: full access to indexd API\n permissions:\n - id: indexd_admin\n action:\n service: indexd\n method: '*'\n - id: admin\n permissions:\n - id: admin\n action:\n service: '*'\n method: '*'\n - id: creator\n permissions:\n - id: creator\n action:\n service: '*'\n method: create\n - id: reader\n permissions:\n - id: reader\n action:\n service: '*'\n method: read\n - id: updater\n permissions:\n - id: updater\n action:\n service: '*'\n method: update\n - id: deleter\n permissions:\n - id: deleter\n action:\n service: '*'\n method: delete\n - id: storage_writer\n permissions:\n - id: storage_creator\n action:\n service: '*'\n method: write-storage\n - id: storage_reader\n permissions:\n - id: storage_reader\n action:\n service: '*'\n method: read-storage\n - id: peregrine_reader\n permissions:\n - id: peregrine_reader\n action:\n method: read\n service: peregrine\n - id: guppy_reader\n permissions:\n - id: guppy_reader\n action:\n method: read\n service: guppy\n - id: fence_storage_reader\n permissions:\n - id: fence_storage_reader\n action:\n method: read-storage\n service: fence\n\nclients:\n wts:\n policies:\n - all_programs_reader\n - open_data_reader\n\nusers:\n username1@gmail.com: {}\n username2:\n tags:\n name: John Doe\n email: johndoe@gmail.com\n policies:\n - MyFirstProject_submitter\n\ncloud_providers: {}\ngroups: {}\n"` | USER YAML. Passed in as a multiline string. | | affinity | map | `{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["fence"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]}}` | Affinity to use for the deployment. | | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution | map | `[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["fence"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]` | Option for scheduling to be required or preferred. | | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0] | int | `{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["fence"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}` | Weight value for preferred scheduling. | @@ -83,21 +84,43 @@ A Helm chart for gen3 Fence | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage | | autoscaling.targetMemoryUtilizationPercentage | int | `80` | Target Memory utilization percentage | -| env | list | `[{"name":"DD_ENABLED","valueFrom":{"configMapKeyRef":{"key":"dd_enabled","name":"manifest-global","optional":true}}},{"name":"DD_ENV","valueFrom":{"fieldRef":{"fieldPath":"metadata.labels['tags.datadoghq.com/env']"}}},{"name":"DD_SERVICE","valueFrom":{"fieldRef":{"fieldPath":"metadata.labels['tags.datadoghq.com/service']"}}},{"name":"DD_VERSION","valueFrom":{"fieldRef":{"fieldPath":"metadata.labels['tags.datadoghq.com/version']"}}},{"name":"DD_LOGS_INJECTION","value":"true"},{"name":"DD_PROFILING_ENABLED","value":"true"},{"name":"DD_TRACE_SAMPLE_RATE","value":"1"},{"name":"GEN3_UWSGI_TIMEOUT","valueFrom":{"configMapKeyRef":{"key":"uwsgi-timeout","name":"manifest-global","optional":true}}},{"name":"DD_AGENT_HOST","valueFrom":{"fieldRef":{"fieldPath":"status.hostIP"}}},{"name":"AWS_STS_REGIONAL_ENDPOINTS","value":"regional"},{"name":"PYTHONPATH","value":"/var/www/fence"},{"name":"GEN3_DEBUG","value":"False"},{"name":"FENCE_PUBLIC_CONFIG","valueFrom":{"configMapKeyRef":{"key":"fence-config-public.yaml","name":"manifest-fence","optional":true}}},{"name":"PGHOST","valueFrom":{"secretKeyRef":{"key":"host","name":"fence-dbcreds","optional":false}}},{"name":"PGUSER","valueFrom":{"secretKeyRef":{"key":"username","name":"fence-dbcreds","optional":false}}},{"name":"PGPASSWORD","valueFrom":{"secretKeyRef":{"key":"password","name":"fence-dbcreds","optional":false}}},{"name":"PGDB","valueFrom":{"secretKeyRef":{"key":"database","name":"fence-dbcreds","optional":false}}},{"name":"DBREADY","valueFrom":{"secretKeyRef":{"key":"dbcreated","name":"fence-dbcreds","optional":false}}},{"name":"DB","value":"postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB)"}]` | Environment variables to pass to the container | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| env | list | `[{"name":"GEN3_UWSGI_TIMEOUT","valueFrom":{"configMapKeyRef":{"key":"uwsgi-timeout","name":"manifest-global","optional":true}}},{"name":"DD_AGENT_HOST","valueFrom":{"fieldRef":{"fieldPath":"status.hostIP"}}},{"name":"AWS_STS_REGIONAL_ENDPOINTS","value":"regional"},{"name":"PYTHONPATH","value":"/var/www/fence"},{"name":"GEN3_DEBUG","value":"False"},{"name":"PGHOST","valueFrom":{"secretKeyRef":{"key":"host","name":"fence-dbcreds","optional":false}}},{"name":"PGUSER","valueFrom":{"secretKeyRef":{"key":"username","name":"fence-dbcreds","optional":false}}},{"name":"PGPASSWORD","valueFrom":{"secretKeyRef":{"key":"password","name":"fence-dbcreds","optional":false}}},{"name":"PGDB","valueFrom":{"secretKeyRef":{"key":"database","name":"fence-dbcreds","optional":false}}},{"name":"DBREADY","valueFrom":{"secretKeyRef":{"key":"dbcreated","name":"fence-dbcreds","optional":false}}},{"name":"DB","value":"postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB)"},{"name":"INDEXD_PASSWORD","valueFrom":{"secretKeyRef":{"key":"fence","name":"indexd-service-creds"}}},{"name":"gen3Env","valueFrom":{"configMapKeyRef":{"key":"hostname","name":"manifest-global"}}}]` | Environment variables to pass to the container | +| externalSecrets | map | `{"createK8sFenceConfigSecret":false,"createK8sGoogleAppSecrets":false,"createK8sJwtKeysSecret":false,"dbcreds":null,"fenceConfig":null,"fenceGoogleAppCredsSecret":null,"fenceGoogleStorageCredsSecret":null,"fenceJwtKeys":null}` | External Secrets settings. | +| externalSecrets.createK8sFenceConfigSecret | string | `false` | Will create the Helm "fence-config" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.createK8sGoogleAppSecrets | string | `false` | Will create the Helm "fence-google-app-creds-secret" and "fence-google-storage-creds-secret" secrets even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.createK8sJwtKeysSecret | string | `false` | Will create the Helm "fence-jwt-keys" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | +| externalSecrets.fenceConfig | string | `nil` | Will override the name of the aws secrets manager secret. Default is "fence-config" | +| externalSecrets.fenceGoogleAppCredsSecret | string | `nil` | Will override the name of the aws secrets manager secret. Default is "fence-google-app-creds-secret" | +| externalSecrets.fenceGoogleStorageCredsSecret | string | `nil` | Will override the name of the aws secrets manager secret. Default is "fence-google-storage-creds-secret" | +| externalSecrets.fenceJwtKeys | string | `nil` | Will override the name of the aws secrets manager secret. Default is "fence-jwt-keys" | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| global | map | `{"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false,"useLocalSecret":{"enabled":false,"localSecretName":null,"localSecretNamespace":null}}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | +| global.aws.useLocalSecret | map | `{"enabled":false,"localSecretName":null,"localSecretNamespace":null}` | Local secret setting if using a pre-exising secret. | +| global.aws.useLocalSecret.enabled | bool | `false` | Set to true if you would like to use a secret that is already running on your cluster. | +| global.aws.useLocalSecret.localSecretName | string | `nil` | Name of the local secret. | +| global.aws.useLocalSecret.localSecretNamespace | string | `nil` | Namespace of the local secret. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any fence secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -107,23 +130,23 @@ A Helm chart for gen3 Fence | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | | global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | +| global.tierAccessLimit | int | `"1000"` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | | image.pullPolicy | string | `"Always"` | When to pull the image. This value should be "Always" to ensure the latest image is used. | | image.repository | string | `"quay.io/cdis/fence"` | The Docker image repository for the fence service | | image.tag | string | `"master"` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | -| initEnv | list | `[{"name":"PGHOST","valueFrom":{"secretKeyRef":{"key":"host","name":"fence-dbcreds","optional":false}}},{"name":"PGUSER","valueFrom":{"secretKeyRef":{"key":"username","name":"fence-dbcreds","optional":false}}},{"name":"PGPASSWORD","valueFrom":{"secretKeyRef":{"key":"password","name":"fence-dbcreds","optional":false}}},{"name":"PGDB","valueFrom":{"secretKeyRef":{"key":"database","name":"fence-dbcreds","optional":false}}},{"name":"DBREADY","valueFrom":{"secretKeyRef":{"key":"dbcreated","name":"fence-dbcreds","optional":false}}},{"name":"DB","value":"postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB)"},{"name":"PYTHONPATH","value":"/var/www/fence"},{"name":"FENCE_PUBLIC_CONFIG","valueFrom":{"configMapKeyRef":{"key":"fence-config-public.yaml","name":"manifest-fence","optional":true}}}]` | Volumes to attach to the init container. | -| initVolumeMounts | list | `[{"mountPath":"/var/www/fence/fence-config.yaml","name":"config-volume","readOnly":true,"subPath":"fence-config.yaml"},{"mountPath":"/var/www/fence/yaml_merge.py","name":"yaml-merge","readOnly":true,"subPath":"yaml_merge.py"},{"mountPath":"/var/www/fence/fence_google_app_creds_secret.json","name":"fence-google-app-creds-secret-volume","readOnly":true,"subPath":"fence_google_app_creds_secret.json"},{"mountPath":"/var/www/fence/fence_google_storage_creds_secret.json","name":"fence-google-storage-creds-secret-volume","readOnly":true,"subPath":"fence_google_storage_creds_secret.json"}]` | Volumes to mount to the init container. | -| labels | map | `{"app":"fence","authprovider":"yes","netnolimit":"yes","public":"yes","release":"production","tags.datadoghq.com/env":"anvilstaging","tags.datadoghq.com/service":"fence","tags.datadoghq.com/version":2021.12,"userhelper":"yes"}` | Labels to add to the pod. | -| labels.app | string | `"fence"` | Application name. | +| initEnv | list | `[{"name":"PGHOST","valueFrom":{"secretKeyRef":{"key":"host","name":"fence-dbcreds","optional":false}}},{"name":"PGUSER","valueFrom":{"secretKeyRef":{"key":"username","name":"fence-dbcreds","optional":false}}},{"name":"PGPASSWORD","valueFrom":{"secretKeyRef":{"key":"password","name":"fence-dbcreds","optional":false}}},{"name":"PGDB","valueFrom":{"secretKeyRef":{"key":"database","name":"fence-dbcreds","optional":false}}},{"name":"DBREADY","valueFrom":{"secretKeyRef":{"key":"dbcreated","name":"fence-dbcreds","optional":false}}},{"name":"DB","value":"postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB)"},{"name":"FENCE_DB","value":"postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB)"},{"name":"PYTHONPATH","value":"/var/www/fence"}]` | Volumes to attach to the init container. | +| initVolumeMounts | list | `[{"mountPath":"/var/www/fence/fence-config-secret.yaml","name":"config-volume","readOnly":true,"subPath":"fence-config.yaml"},{"mountPath":"/var/www/fence/fence-config-public.yaml","name":"config-volume-public","readOnly":true,"subPath":"fence-config-public.yaml"},{"mountPath":"/var/www/fence/yaml_merge.py","name":"yaml-merge","readOnly":true,"subPath":"yaml_merge.py"},{"mountPath":"/var/www/fence/fence_google_app_creds_secret.json","name":"fence-google-app-creds-secret-volume","readOnly":true,"subPath":"fence_google_app_creds_secret.json"},{"mountPath":"/var/www/fence/fence_google_storage_creds_secret.json","name":"fence-google-storage-creds-secret-volume","readOnly":true,"subPath":"fence_google_storage_creds_secret.json"}]` | Volumes to mount to the init container. | +| labels | map | `{"authprovider":"yes","netnolimit":"yes","public":"yes","userhelper":"yes"}` | Labels to add to the pod. | | labels.authprovider | string | `"yes"` | Grants egress from all pods to pods labeled with authrpovider=yes. For network policy selectors. | | labels.netnolimit | string | `"yes"` | Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs | | labels.public | string | `"yes"` | Grants ingress from the revproxy service for pods labeled with public=yes | -| labels.release | string | `"production"` | Release name. | | labels.userhelper | string | `"yes"` | Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes | | logo | string | `nil` | | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | | nodeSelector | map | `{}` | Node Selector for the pods | +| partOf | string | `"Authentication"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod | | podSecurityContext | map | `{"fsGroup":101}` | Security context for the pod | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | @@ -137,6 +160,8 @@ A Helm chart for gen3 Fence | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | | privacy_policy | string | `nil` | | +| projects | string | `nil` | | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of desired replicas | | resources | map | `{"limits":{"cpu":1,"memory":"2Gi"},"requests":{"cpu":0.3,"memory":"128Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"2Gi"}` | The maximum amount of resources that the container is allowed to use | @@ -145,8 +170,11 @@ A Helm chart for gen3 Fence | resources.requests | map | `{"cpu":0.3,"memory":"128Mi"}` | The amount of resources that the container requests | | resources.requests.cpu | string | `0.3` | The amount of CPU requested | | resources.requests.memory | string | `"128Mi"` | The amount of memory requested | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information for Usersync and External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS access key ID. Overrides global key. | | securityContext | map | `{}` | Security context for the containers in the pod | -| selectorLabels | map | `{"app":"fence","release":"production"}` | Labels to use for selecting the deployment. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -156,8 +184,15 @@ A Helm chart for gen3 Fence | serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | | serviceAccount.name | string | `"fence-sa"` | The name of the service account | | tolerations | list | `[]` | Tolerations for the pods | -| volumeMounts | list | `[{"mountPath":"/var/www/fence/local_settings.py","name":"old-config-volume","readOnly":true,"subPath":"local_settings.py"},{"mountPath":"/var/www/fence/fence_credentials.json","name":"json-secret-volume","readOnly":true,"subPath":"fence_credentials.json"},{"mountPath":"/var/www/fence/creds.json","name":"creds-volume","readOnly":true,"subPath":"creds.json"},{"mountPath":"/var/www/fence/config_helper.py","name":"config-helper","readOnly":true,"subPath":"config_helper.py"},{"mountPath":"/fence/fence/static/img/logo.svg","name":"logo-volume","readOnly":true,"subPath":"logo.svg"},{"mountPath":"/fence/fence/static/privacy_policy.md","name":"privacy-policy","readOnly":true,"subPath":"privacy_policy.md"},{"mountPath":"/var/www/fence/fence-config.yaml","name":"config-volume","readOnly":true,"subPath":"fence-config.yaml"},{"mountPath":"/var/www/fence/yaml_merge.py","name":"yaml-merge","readOnly":true,"subPath":"yaml_merge.py"},{"mountPath":"/var/www/fence/fence_google_app_creds_secret.json","name":"fence-google-app-creds-secret-volume","readOnly":true,"subPath":"fence_google_app_creds_secret.json"},{"mountPath":"/var/www/fence/fence_google_storage_creds_secret.json","name":"fence-google-storage-creds-secret-volume","readOnly":true,"subPath":"fence_google_storage_creds_secret.json"},{"mountPath":"/fence/keys/key/jwt_private_key.pem","name":"fence-jwt-keys","readOnly":true,"subPath":"jwt_private_key.pem"}]` | Volumes to mount to the container. | -| volumes | list | `[{"name":"old-config-volume","secret":{"secretName":"fence-secret"}},{"name":"json-secret-volume","secret":{"optional":true,"secretName":"fence-json-secret"}},{"name":"creds-volume","secret":{"secretName":"fence-creds"}},{"configMap":{"name":"config-helper","optional":true},"name":"config-helper"},{"configMap":{"name":"logo-config"},"name":"logo-volume"},{"name":"config-volume","secret":{"secretName":"fence-config"}},{"name":"fence-google-app-creds-secret-volume","secret":{"secretName":"fence-google-app-creds-secret"}},{"name":"fence-google-storage-creds-secret-volume","secret":{"secretName":"fence-google-storage-creds-secret"}},{"name":"fence-jwt-keys","secret":{"secretName":"fence-jwt-keys"}},{"configMap":{"name":"privacy-policy"},"name":"privacy-policy"},{"configMap":{"name":"fence-yaml-merge","optional":true},"name":"yaml-merge"}]` | Volumes to attach to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) +| usersync | map | `{"addDbgap":false,"custom_image":null,"onlyDbgap":false,"schedule":"*/30 * * * *","slack_send_dbgap":false,"slack_webhook":"None","syncFromDbgap":false,"userYamlS3Path":"s3://cdis-gen3-users/helm-test/user.yaml","usersync":false}` | Configuration options for usersync cronjob. | +| usersync.addDbgap | bool | `false` | Force attempting a dbgap sync if "true", falls back on user.yaml | +| usersync.custom_image | string | `nil` | To set a custom image for pulling the user.yaml file from S3. Default is the Gen3 Awshelper image. | +| usersync.onlyDbgap | bool | `false` | Forces ONLY a dbgap sync if "true", IGNORING user.yaml | +| usersync.schedule | string | `"*/30 * * * *"` | The cron schedule expression to use in the usersync cronjob. Runs every 30 minutes by default. | +| usersync.slack_send_dbgap | bool | `false` | Will echo what files we are seeing on dbgap ftp to Slack. | +| usersync.slack_webhook | string | `"None"` | Slack webhook endpoint used with certain jobs. | +| usersync.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | +| usersync.userYamlS3Path | string | `"s3://cdis-gen3-users/helm-test/user.yaml"` | Path to the user.yaml file in S3. | +| usersync.usersync | bool | `false` | Whether to run Fence usersync or not. | +| volumeMounts | list | `[{"mountPath":"/var/www/fence/local_settings.py","name":"old-config-volume","readOnly":true,"subPath":"local_settings.py"},{"mountPath":"/var/www/fence/fence_credentials.json","name":"json-secret-volume","readOnly":true,"subPath":"fence_credentials.json"},{"mountPath":"/var/www/fence/creds.json","name":"creds-volume","readOnly":true,"subPath":"creds.json"},{"mountPath":"/var/www/fence/config_helper.py","name":"config-helper","readOnly":true,"subPath":"config_helper.py"},{"mountPath":"/fence/fence/static/img/logo.svg","name":"logo-volume","readOnly":true,"subPath":"logo.svg"},{"mountPath":"/fence/fence/static/privacy_policy.md","name":"privacy-policy","readOnly":true,"subPath":"privacy_policy.md"},{"mountPath":"/var/www/fence/fence-config-secret.yaml","name":"config-volume","readOnly":true,"subPath":"fence-config.yaml"},{"mountPath":"/var/www/fence/yaml_merge.py","name":"yaml-merge","readOnly":true,"subPath":"yaml_merge.py"},{"mountPath":"/var/www/fence/fence_google_app_creds_secret.json","name":"fence-google-app-creds-secret-volume","readOnly":true,"subPath":"fence_google_app_creds_secret.json"},{"mountPath":"/var/www/fence/fence_google_storage_creds_secret.json","name":"fence-google-storage-creds-secret-volume","readOnly":true,"subPath":"fence_google_storage_creds_secret.json"},{"mountPath":"/fence/keys/key/jwt_private_key.pem","name":"fence-jwt-keys","readOnly":true,"subPath":"jwt_private_key.pem"},{"mountPath":"/var/www/fence/fence-config-public.yaml","name":"config-volume-public","readOnly":true,"subPath":"fence-config-public.yaml"}]` | Volumes to mount to the container. | +| volumes | list | `[{"name":"old-config-volume","secret":{"secretName":"fence-secret"}},{"name":"json-secret-volume","secret":{"optional":true,"secretName":"fence-json-secret"}},{"name":"creds-volume","secret":{"secretName":"fence-creds"}},{"configMap":{"name":"config-helper","optional":true},"name":"config-helper"},{"configMap":{"name":"logo-config"},"name":"logo-volume"},{"name":"config-volume","secret":{"secretName":"fence-config"}},{"name":"fence-google-app-creds-secret-volume","secret":{"secretName":"fence-google-app-creds-secret"}},{"name":"fence-google-storage-creds-secret-volume","secret":{"secretName":"fence-google-storage-creds-secret"}},{"name":"fence-jwt-keys","secret":{"secretName":"fence-jwt-keys"}},{"configMap":{"name":"privacy-policy"},"name":"privacy-policy"},{"configMap":{"name":"fence-yaml-merge","optional":false},"name":"yaml-merge"},{"configMap":{"name":"manifest-fence","optional":true},"name":"config-volume-public"}]` | Volumes to attach to the container. | diff --git a/helm/fence/fence-google-creds/fence_google_app_creds_secret.json b/helm/fence/fence-google-creds/fence_google_app_creds_secret.json new file mode 100644 index 00000000..e69de29b diff --git a/helm/fence/fence-google-creds/fence_google_storage_creds_secret.json b/helm/fence/fence-google-creds/fence_google_storage_creds_secret.json new file mode 100644 index 00000000..e69de29b diff --git a/helm/fence/projects/projects.yaml b/helm/fence/projects/projects.yaml new file mode 100644 index 00000000..e69de29b diff --git a/helm/fence/scripts/yaml_merge.py b/helm/fence/scripts/yaml_merge.py new file mode 100644 index 00000000..5223f9f4 --- /dev/null +++ b/helm/fence/scripts/yaml_merge.py @@ -0,0 +1,56 @@ +import sys +import yaml + +''' +Helper script to merge arbitraly number of yaml files + +Usage: python yaml_merge.py file1.yaml file2.yaml ... fence-config.yaml + +Example: python yaml_merge.py file1.yaml file2.yaml fence-config.yaml +file1.yaml key(s) will overriden by items in file2.yaml if they exist, + +''' +def merge_yaml_files(file_paths): + merged_data = {} + + for file_path in file_paths: + try: + with open(file_path, 'r') as file: + data = yaml.safe_load(file) + merged_data = merge_dicts(merged_data, data) + except FileNotFoundError as e: + print('WARNING! File not found: {}. Will be ignored!'.format(file_path)) + + return merged_data + +def merge_dicts(dict1, dict2): + if dict2 is not None: #Fix AttributeError + for key, value in dict2.items(): + if key in dict1 and isinstance(dict1[key], dict) and isinstance(value, dict): + dict1[key] = merge_dicts(dict1[key], value) + else: + dict1[key] = value + + return dict1 + +def save_merged_file(merged_data, output_file_path): + with open(output_file_path, 'w') as output_file: + yaml.dump(merged_data, output_file, default_flow_style=False) + +if __name__ == "__main__": + # Check if at least two arguments are provided (including the script name) + if len(sys.argv) < 3: + print("Usage: python yaml_merge.py config-file1.yaml config-file2.yaml ... fence-config.yaml") + sys.exit(1) + + # Extract input file paths and output file path + input_files = sys.argv[1:-1] + output_file = sys.argv[-1] + + # Merge YAML files + merged_data = merge_yaml_files(input_files) + + # Save the merged data to the output file + save_merged_file(merged_data, output_file) + + print(f"Merged Configuration saved to {output_file}") diff --git a/helm/fence/templates/_helpers.tpl b/helm/fence/templates/_helpers.tpl index 1eeaac06..c4a4aa77 100644 --- a/helm/fence/templates/_helpers.tpl +++ b/helm/fence/templates/_helpers.tpl @@ -5,6 +5,24 @@ Expand the name of the chart. {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} + + +{{/* +a function to generate or get the jwt keys +*/}} + +{{- define "getOrCreatePrivateKey" -}} +{{- $secretName := "fence-jwt-keys" }} +{{- $existingSecret := (lookup "v1" "Secret" .Release.Namespace $secretName) }} +{{- if $existingSecret }} +{{- index $existingSecret.data "jwt_private_key.pem" }} +{{- else }} +{{- genPrivateKey "rsa" | b64enc }} +{{- end }} +{{- end -}} + + + {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). @@ -34,20 +52,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "fence.labels" -}} -helm.sh/chart: {{ include "fence.chart" . }} -{{ include "fence.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "fence.selectorLabels" -}} -app.kubernetes.io/name: {{ include "fence.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -71,4 +95,33 @@ Create the name of the service account to use {{- else }} {{- default .Values.postgres.password }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} + + +{{/* + Fence JWT Keys Secrets Manager Name +*/}} +{{- define "fence-jwt-keys" -}} +{{- default "fence-jwt-keys" .Values.externalSecrets.fenceJwtKeys }} +{{- end }} + +{{/* + Fence Google App Creds Secrets Manager Name +*/}} +{{- define "fence-google-app-creds-secret" -}} +{{- default "fence-google-app-creds-secret" .Values.externalSecrets.fenceGoogleAppCredsSecret }} +{{- end }} + +{{/* + Fence Google Storage Creds Secrets Manager Name +*/}} +{{- define "fence-google-storage-creds-secret" -}} +{{- default "fence-google-storage-creds-secret" .Values.externalSecrets.fenceGoogleStorageCredsSecret }} +{{- end }} + +{{/* + Fence Config Secrets Manager Name +*/}} +{{- define "fence-config" -}} +{{- default "fence-config" .Values.externalSecrets.fenceConfig }} +{{- end }} diff --git a/helm/fence/templates/aws-config.yaml b/helm/fence/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/fence/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/fence/templates/db-init.yaml b/helm/fence/templates/db-init.yaml index abbefb6e..5ef14e87 100644 --- a/helm/fence/templates/db-init.yaml +++ b/helm/fence/templates/db-init.yaml @@ -1,6 +1,6 @@ -{{ include "common.db_setup_job" . }} ---- {{ include "common.db-secret" . }} --- -{{ include "common.db_setup_sa" . }} +{{ include "common.db_setup_job" . }} --- +{{ include "common.db_setup_sa" . }} +--- \ No newline at end of file diff --git a/helm/fence/templates/external-secret.yaml b/helm/fence/templates/external-secret.yaml new file mode 100644 index 00000000..27e5ebc3 --- /dev/null +++ b/helm/fence/templates/external-secret.yaml @@ -0,0 +1,75 @@ +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: fence-jwt-keys +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: fence-jwt-keys + creationPolicy: Owner + data: + - secretKey: jwt_private_key.pem + remoteRef: + #name of secret in secrets manager + key: {{include "fence-jwt-keys" .}} +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: fence-google-app-creds-secret +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: fence-google-app-creds-secret + creationPolicy: Owner + data: + - secretKey: fence_google_app_creds_secret.json + remoteRef: + #name of secret in secrets manager + key: {{include "fence-google-app-creds-secret" .}} +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: fence-google-storage-creds-secret +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: fence-google-storage-creds-secret + creationPolicy: Owner + data: + - secretKey: fence_google_storage_creds_secret.json + remoteRef: + #name of secret in secrets manager + key: {{include "fence-google-storage-creds-secret" .}} +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: fence-config +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: fence-config + creationPolicy: Owner + data: + - secretKey: fence-config.yaml + remoteRef: + #name of secret in secrets manager + key: {{include "fence-config" .}} +{{- end }} +--- +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/fence/templates/fence-config-public.yaml b/helm/fence/templates/fence-config-public.yaml new file mode 100644 index 00000000..4e18796a --- /dev/null +++ b/helm/fence/templates/fence-config-public.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: manifest-fence +data: + fence-config-public.yaml: | + {{- with .Values.FENCE_CONFIG_PUBLIC }} + {{- toYaml . | nindent 4 }} + {{ end }} + diff --git a/helm/fence/templates/fence-config.yaml b/helm/fence/templates/fence-config.yaml index 2ff0eadc..e594d072 100644 --- a/helm/fence/templates/fence-config.yaml +++ b/helm/fence/templates/fence-config.yaml @@ -1,3 +1,4 @@ +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sFenceConfigSecret) }} apiVersion: v1 kind: Secret metadata: @@ -8,4 +9,5 @@ stringData: {{- with .Values.FENCE_CONFIG }} {{- toYaml . | nindent 4 }} {{ end }} - \ No newline at end of file +--- +{{- end }} diff --git a/helm/fence/templates/fence-deployment.yaml b/helm/fence/templates/fence-deployment.yaml index 7df97d02..1b24c149 100644 --- a/helm/fence/templates/fence-deployment.yaml +++ b/helm/fence/templates/fence-deployment.yaml @@ -13,20 +13,30 @@ spec: {{- include "fence.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/publicconfig: {{ include (print $.Template.BasePath "/fence-config-public.yaml") . | sha256sum }} + checksum/secretconfig: {{ include (print $.Template.BasePath "/fence-config.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: - {{- include "fence.labels" . | nindent 8 }} + authprovider: "yes" + netnolimit: "yes" + userhelper: "yes" + {{- include "fence.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: + enableServiceLinks: false serviceAccountName: {{ include "fence.serviceAccountName" . }} volumes: {{- toYaml .Values.volumes | nindent 8 }} containers: - name: fence image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: Always + imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: 80 @@ -54,8 +64,7 @@ spec: args: - "-c" - | - echo "${FENCE_PUBLIC_CONFIG:-""}" > "/var/www/fence/fence-config-public.yaml" - python /var/www/fence/yaml_merge.py /var/www/fence/fence-config-public.yaml /var/www/fence/fence-config-secret.yaml > /var/www/fence/fence-config.yaml + python /var/www/fence/yaml_merge.py /var/www/fence/fence-config-public.yaml /var/www/fence/fence-config-secret.yaml /var/www/fence/fence-config.yaml if [[ -f /fence/keys/key/jwt_private_key.pem ]]; then openssl rsa -in /fence/keys/key/jwt_private_key.pem -pubout > /fence/keys/key/jwt_public_key.pem fi @@ -84,8 +93,7 @@ spec: args: - "-c" - | - # echo "${FENCE_PUBLIC_CONFIG:-""}" > "/var/www/fence/fence-config-public.yaml" - # python /var/www/fence/yaml_merge.py /var/www/fence/fence-config-public.yaml /var/www/fence/fence-config-secret.yaml > /var/www/fence/fence-config.yaml + python /var/www/fence/yaml_merge.py /var/www/fence/fence-config-public.yaml /var/www/fence/fence-config-secret.yaml /var/www/fence/fence-config.yaml if fence-create migrate --help > /dev/null 2>&1; then if ! grep -E 'ENABLE_DB_MIGRATION"?: *false' /var/www/fence/fence-config.yaml; then echo "Running db migration: fence-create migrate" @@ -112,4 +120,4 @@ spec: {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} \ No newline at end of file diff --git a/helm/fence/templates/fence-secret.yaml b/helm/fence/templates/fence-secret.yaml index 6ac60bde..7bd3675a 100644 --- a/helm/fence/templates/fence-secret.yaml +++ b/helm/fence/templates/fence-secret.yaml @@ -5,6 +5,7 @@ metadata: type: Opaque data: {{ (.Files.Glob "fence-secret/*").AsSecrets | indent 2 }} +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sGoogleAppSecrets) }} --- apiVersion: v1 kind: Secret @@ -21,3 +22,4 @@ metadata: type: Opaque data: {{ (.Files.Glob "fence-google-creds/*").AsSecrets | indent 2 }} +{{- end }} \ No newline at end of file diff --git a/helm/fence/templates/fence-yaml-merge.yaml b/helm/fence/templates/fence-yaml-merge.yaml new file mode 100644 index 00000000..4ec22b51 --- /dev/null +++ b/helm/fence/templates/fence-yaml-merge.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: fence-yaml-merge +data: +{{ (.Files.Glob "scripts/*").AsConfig | indent 2 }} \ No newline at end of file diff --git a/helm/fence/templates/jwt-keys.yaml b/helm/fence/templates/jwt-keys.yaml index fca35e12..06d10f28 100644 --- a/helm/fence/templates/jwt-keys.yaml +++ b/helm/fence/templates/jwt-keys.yaml @@ -1,7 +1,9 @@ +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sJwtKeysSecret) }} apiVersion: v1 kind: Secret metadata: name: fence-jwt-keys type: Opaque data: - jwt_private_key.pem: {{ genPrivateKey "rsa" | b64enc }} + jwt_private_key.pem: {{ include "getOrCreatePrivateKey" . }} +{{- end }} \ No newline at end of file diff --git a/helm/fence/templates/netpolicy.yaml b/helm/fence/templates/netpolicy.yaml new file mode 100644 index 00000000..70a5c3b5 --- /dev/null +++ b/helm/fence/templates/netpolicy.yaml @@ -0,0 +1 @@ +{{ include "common.db_netpolicy" . }} \ No newline at end of file diff --git a/helm/fence/templates/pdb.yaml b/helm/fence/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/fence/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/fence/templates/presigned-url-fence.yaml b/helm/fence/templates/presigned-url-fence.yaml index 534c81e9..0fc5342c 100644 --- a/helm/fence/templates/presigned-url-fence.yaml +++ b/helm/fence/templates/presigned-url-fence.yaml @@ -13,12 +13,21 @@ spec: app: presigned-url-fence template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/publicconfig: {{ include (print $.Template.BasePath "/fence-config-public.yaml") . | sha256sum }} + checksum/secretconfig: {{ include (print $.Template.BasePath "/fence-config.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: - app: presigned-url-fence + app: "presigned-url-fence" + authprovder: "yes" + netnolimit: "yes" + public: "yes" + userhelper: "yes" spec: serviceAccountName: {{ include "fence.serviceAccountName" . }} volumes: @@ -54,8 +63,7 @@ spec: args: - "-c" - | - echo "${FENCE_PUBLIC_CONFIG:-""}" > "/var/www/fence/fence-config-public.yaml" - python /var/www/fence/yaml_merge.py /var/www/fence/fence-config-public.yaml /var/www/fence/fence-config-secret.yaml > /var/www/fence/fence-config.yaml + python /var/www/fence/yaml_merge.py /var/www/fence/fence-config-public.yaml /var/www/fence/fence-config-secret.yaml /var/www/fence/fence-config.yaml if [[ -f /fence/keys/key/jwt_private_key.pem ]]; then openssl rsa -in /fence/keys/key/jwt_private_key.pem -pubout > /fence/keys/key/jwt_public_key.pem fi diff --git a/helm/fence/templates/projects-config.yaml b/helm/fence/templates/projects-config.yaml new file mode 100644 index 00000000..f963fb6d --- /dev/null +++ b/helm/fence/templates/projects-config.yaml @@ -0,0 +1,8 @@ +{{- with .Values.usersync.projects }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: projects +data: + projects.yaml: {{ . }} +{{- end }} diff --git a/helm/fence/templates/secret-store.yaml b/helm/fence/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/fence/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/fence/templates/serviceaccount.yaml b/helm/fence/templates/serviceaccount.yaml index 8193f704..48c24f36 100644 --- a/helm/fence/templates/serviceaccount.yaml +++ b/helm/fence/templates/serviceaccount.yaml @@ -9,4 +9,4 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} -{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/fence/templates/tests/test-connection.yaml b/helm/fence/templates/tests/test-connection.yaml index 2a65b670..7986768b 100644 --- a/helm/fence/templates/tests/test-connection.yaml +++ b/helm/fence/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "fence.fullname" . }}-test-connection" + name: "fence-test-connection" labels: {{- include "fence.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "fence.fullname" . }}:{{ .Values.service.port }}'] + args: ['fence-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/fence/templates/usersync-cron.yaml b/helm/fence/templates/usersync-cron.yaml index e69de29b..d10ad669 100644 --- a/helm/fence/templates/usersync-cron.yaml +++ b/helm/fence/templates/usersync-cron.yaml @@ -0,0 +1,218 @@ +{{- if .Values.usersync.usersync -}} +# +# run with: +# gen3 job run usersync +# +# Optional Arguments: +# ADD_DBGAP Force attempting a dbgap sync if "true", falls back on user.yaml +# by defualt. i.e. this isn't required for a dbGaP sync to happen +# default: "false" - fall back on user.yaml +# +# ONLY_DBGAP Forces ONLY a dbgap sync if "true", IGNORING user.yaml +# default: "false" +# +apiVersion: batch/v1 +kind: CronJob +metadata: + name: usersync +spec: + schedule: {{ .Values.usersync.schedule | quote }} + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + concurrencyPolicy: Forbid + jobTemplate: + spec: + backoffLimit: 4 + template: + metadata: + labels: + app: gen3job + spec: + serviceAccountName: usersync-job + volumes: + - name: user-yaml + configMap: + name: useryaml + items: + - key: useryaml + path: user.yaml + - name: config-volume + secret: + secretName: "fence-config" + - name: creds-volume + secret: + secretName: "fence-creds" + - name: projects + configMap: + name: "projects" + optional: true + - name: fence-google-app-creds-secret-volume + secret: + secretName: "fence-google-app-creds-secret" + - name: fence-google-storage-creds-secret-volume + secret: + secretName: "fence-google-storage-creds-secret" + - name: shared-data + emptyDir: {} + - name: cred-volume + secret: + {{- if .Values.global.aws.useLocalSecret.enabled }} + secretName: {{ .Values.global.aws.useLocalSecret.localSecretName }} + {{- else }} + secretName: {{.Chart.Name}}-aws-config + {{- end }} + initContainers: + - name: wait-for-fence + image: curlimages/curl:latest + command: ["/bin/sh","-c"] + args: ["while [ $(curl -sw '%{http_code}' http://fence-service -o /dev/null) -ne 200 ]; do sleep 5; echo 'Waiting for fence...'; done"] + - name: awshelper + image: {{ .Values.usersync.custom_image | default "quay.io/cdis/awshelper:master" }} + imagePullPolicy: Always + env: + - name: gen3Env + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname + - name: userYamlS3Path + value: {{ .Values.usersync.userYamlS3Path | quote }} + - name: slackWebHook + value: {{ .Values.usersync.slack_webhook | quote }} + volumeMounts: + - name: user-yaml + mountPath: /var/www/fence + - name: shared-data + mountPath: /mnt/shared + - name: cred-volume + mountPath: "/home/ubuntu/.aws/credentials" + subPath: credentials + command: ["/bin/bash" ] + args: + - "-c" + - | + GEN3_HOME=/home/ubuntu/cloud-automation + source "${GEN3_HOME}/gen3/lib/utils.sh" + gen3_load "gen3/gen3setup" + + if [ "${userYamlS3Path}" = 'none' ]; then + echo "using local user.yaml" + cp /var/www/fence/user.yaml /mnt/shared/user.yaml + else + # ----------------- + echo "awshelper downloading ${userYamlS3Path} to /mnt/shared/user.yaml" + n=0 + until [ $n -ge 5 ]; do + echo "Download attempt $n" + aws s3 cp "${userYamlS3Path}" /mnt/shared/user.yaml && break + n=$[$n+1] + sleep 2 + done + fi + if [[ ! -f /mnt/shared/user.yaml ]]; then + echo "awshelper failed to retrieve /mnt/shared/user.yaml" + exit 1 + fi + #----------- + echo "awshelper updating etl configmap" + if ! gen3 gitops etl-convert < /mnt/shared/user.yaml > /tmp/user.yaml; then + echo "ERROR: failed to generate ETL config" + exit 1 + fi + kubectl delete configmap fence > /dev/null 2>&1 + kubectl create configmap fence --from-file=/tmp/user.yaml + if [ "${slackWebHook}" != 'None' ]; then + curl -X POST --data-urlencode "payload={\"text\": \"AWSHelper: Syncing users on ${gen3Env}\"}" "${slackWebHook}" + fi + echo "Helper exit ok" + containers: + - name: usersync + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: Always + env: + - name: SYNC_FROM_DBGAP + value: {{ .Values.usersync.syncFromDbgap | quote }} + - name: ADD_DBGAP + value: {{ .Values.usersync.addDbgap | quote }} + - name: ONLY_DBGAP + value: {{ .Values.usersync.onlyDbgap | quote }} + - name: SLACK_SEND_DBGAP + value: {{ .Values.usersync.slack_send_dbgap | quote }} + - name: slackWebHook + value: {{ .Values.usersync.slack_webhook | quote }} + {{- toYaml .Values.env | nindent 12 }} + volumeMounts: + - name: shared-data + mountPath: /mnt/shared + - name: "config-volume" + readOnly: true + mountPath: "/var/www/fence/fence-config.yaml" + subPath: fence-config.yaml + - name: "creds-volume" + readOnly: true + mountPath: "/var/www/fence/creds.json" + subPath: creds.json + - name: "projects" + mountPath: "/var/www/fence/projects.yaml" + subPath: "projects.yaml" + - name: "fence-google-app-creds-secret-volume" + readOnly: true + mountPath: "/var/www/fence/fence_google_app_creds_secret.json" + subPath: fence_google_app_creds_secret.json + - name: "fence-google-storage-creds-secret-volume" + readOnly: true + mountPath: "/var/www/fence/fence_google_storage_creds_secret.json" + subPath: fence_google_storage_creds_secret.json + command: ["/bin/bash" ] + args: + - "-c" + # Script always succeeds if it runs (echo exits with 0) + - | + echo 'options use-vc' >> /etc/resolv.conf + # pip3 install SQLAlchemy==1.3.6 + # can be removed once this is merged: https://github.com/uc-cdis/fence/pull/1096 + if [[ "$SYNC_FROM_DBGAP" != "true" && "$ADD_DBGAP" != "true" ]]; then + if [[ -f /mnt/shared/user.yaml ]]; then + echo "running fence-create" + time fence-create sync --arborist http://arborist-service --yaml /mnt/shared/user.yaml + else + echo "/mnt/shared/user.yaml did not appear within timeout :-(" + false # non-zero exit code + fi + exitcode=$? + else + output=$(mktemp "/tmp/fence-create-output_XXXXXX") + if [[ -f /mnt/shared/user.yaml && "$ONLY_DBGAP" != "true" ]]; then + echo "Running fence-create dbgap-sync with user.yaml - see $output" + time fence-create sync --arborist http://arborist-service --sync_from_dbgap "True" --projects /var/www/fence/projects.yaml --yaml /mnt/shared/user.yaml 2>&1 | tee "$output" + else + echo "Running fence-create dbgap-sync without user.yaml - see $output" + time fence-create sync --arborist http://arborist-service --sync_from_dbgap "True" --projects /var/www/fence/projects.yaml 2>&1 | tee "$output" + fi + exitcode="${PIPESTATUS[0]}" + echo "$output" + # Echo what files we are seeing on dbgap ftp to Slack + # We only do this step every 12 hours and not on weekends to reduce noise + if [[ -n "$SLACK_SEND_DBGAP" && "$SLACK_SEND_DBGAP" = "true" ]]; then + files=$(grep "Reading file" "$output") + let hour=$(date -u +10#%H) + let dow=$(date -u +10#%u) + if ! (( hour % 12 )) && (( dow < 6 )); then + if [ "${slackWebHook}" != 'None' ]; then + curl -X POST --data-urlencode "payload={\"text\": \"FenceHelper: \n\`\`\`\n${files}\n\`\`\`\"}" "${slackWebHook}" + fi + fi + fi + fi + if [[ $exitcode -ne 0 && "${slackWebHook}" != 'None' ]]; then + emptyfile=$(grep "EnvironmentError:" "$output") + if [ ! -z "$emptyfile" ]; then + curl -X POST --data-urlencode "payload={\"text\": \"JOBSKIPPED: User sync skipped on ${gen3Env} ${emptyfile}\"}" "${slackWebHook}"; + else + curl -X POST --data-urlencode "payload={\"text\": \"JOBFAIL: User sync failed on ${gen3Env}\"}" "${slackWebHook}" + fi + fi + echo "Exit code: $exitcode" + exit "$exitcode" + restartPolicy: "Never" +{{- end }} \ No newline at end of file diff --git a/helm/fence/templates/usersync-sa.yaml b/helm/fence/templates/usersync-sa.yaml new file mode 100644 index 00000000..f86ff821 --- /dev/null +++ b/helm/fence/templates/usersync-sa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.usersync.usersync -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: usersync-job +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: usersync-job-role +rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch", "create", "update", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: usersync-job-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: usersync-job-role +subjects: + - kind: ServiceAccount + name: usersync-job + namespace: default +{{- end }} \ No newline at end of file diff --git a/helm/fence/templates/useryaml-job.yaml b/helm/fence/templates/useryaml-job.yaml index d7ec25f1..6fd72be7 100644 --- a/helm/fence/templates/useryaml-job.yaml +++ b/helm/fence/templates/useryaml-job.yaml @@ -5,6 +5,7 @@ metadata: data: useryaml: {{ .Values.USER_YAML | toYaml | nindent 4}} --- +{{ if not .Values.usersync.usersync }} apiVersion: batch/v1 kind: Job metadata: @@ -43,5 +44,8 @@ spec: - "-c" # Script always succeeds if it runs (echo exits with 0) - | + pip3 install SQLAlchemy==1.3.6 + # can be removed once this is merged: https://github.com/uc-cdis/fence/pull/1096 fence-create sync --arborist http://arborist-service --yaml /var/www/fence/user.yaml restartPolicy: OnFailure +{{ end }} \ No newline at end of file diff --git a/helm/fence/values.yaml b/helm/fence/values.yaml index d64e189a..8cd72005 100644 --- a/helm/fence/values.yaml +++ b/helm/fence/values.yaml @@ -2,14 +2,32 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: + # -- (map) AWS configuration + aws: + # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. + enabled: false + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: + # -- (map) Local secret setting if using a pre-exising secret. + useLocalSecret: + # -- (bool) Set to true if you would like to use a secret that is already running on your cluster. + enabled: false + # -- (string) Name of the local secret. + localSecretName: + # -- (string) Namespace of the local secret. + localSecretNamespace: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -36,18 +54,77 @@ global: logsBucket: logs-gen3 # -- (bool) Whether to sync data from dbGaP. syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. + tierAccessLimit: "1000" + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any fence secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will create the Helm "fence-config" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sFenceConfigSecret: false + # -- (string) Will create the Helm "fence-jwt-keys" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sJwtKeysSecret: false + # -- (string) Will create the Helm "fence-google-app-creds-secret" and "fence-google-storage-creds-secret" secrets even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sGoogleAppSecrets: false + # -- (string) Will override the name of the aws secrets manager secret. Default is "fence-jwt-keys" + fenceJwtKeys: + # -- (string) Will override the name of the aws secrets manager secret. Default is "fence-google-app-creds-secret" + fenceGoogleAppCredsSecret: + # -- (string) Will override the name of the aws secrets manager secret. Default is "fence-google-storage-creds-secret" + fenceGoogleStorageCredsSecret: + # -- (string) Will override the name of the aws secrets manager secret. Default is "fence-config" + fenceConfig: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: + +# -- (map) Configuration options for usersync cronjob. +usersync: + # -- (bool) Whether to run Fence usersync or not. + usersync: false + # -- (string) The cron schedule expression to use in the usersync cronjob. Runs every 30 minutes by default. + schedule: "*/30 * * * *" + # -- (string) To set a custom image for pulling the user.yaml file from S3. Default is the Gen3 Awshelper image. + custom_image: + # -- (bool) Whether to sync data from dbGaP. + syncFromDbgap: false + # -- (bool) Force attempting a dbgap sync if "true", falls back on user.yaml + addDbgap: false + # -- (bool) Forces ONLY a dbgap sync if "true", IGNORING user.yaml + onlyDbgap: false + # -- (string) Path to the user.yaml file in S3. + userYamlS3Path: s3://cdis-gen3-users/helm-test/user.yaml + # -- (string) Slack webhook endpoint used with certain jobs. + slack_webhook: None + # -- (bool) Will echo what files we are seeing on dbgap ftp to Slack. + slack_send_dbgap: false + +# -- (map) Secret information for Usersync and External Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS access key ID. Overrides global key. + awsSecretAccessKey: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -116,7 +193,8 @@ podSecurityContext: fsGroup: 101 # -- (map) Security context for the containers in the pod -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -146,7 +224,6 @@ resources: # -- (string) The maximum amount of memory the container can use memory: 2Gi - # -- (map) Configuration for autoscaling the number of replicas autoscaling: # -- (bool) Whether autoscaling is enabled @@ -168,10 +245,6 @@ tolerations: [] # -- (map) Labels to add to the pod. labels: - # -- (string) Application name. - app: fence - # -- (string) Release name. - release: production # -- (string) Grants egress from all pods to pods labeled with authrpovider=yes. For network policy selectors. authprovider: "yes" # -- (string) Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs @@ -180,56 +253,29 @@ labels: public: "yes" # -- (string) Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes userhelper: "yes" - tags.datadoghq.com/service: "fence" - tags.datadoghq.com/env: anvilstaging - tags.datadoghq.com/version: 2021.12 # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - fence - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - fence + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (list) Environment variables to pass to the container env: - - name: DD_ENABLED - valueFrom: - configMapKeyRef: - name: manifest-global - key: dd_enabled - optional: true - - name: DD_ENV - valueFrom: - fieldRef: - fieldPath: metadata.labels['tags.datadoghq.com/env'] - - name: DD_SERVICE - valueFrom: - fieldRef: - fieldPath: metadata.labels['tags.datadoghq.com/service'] - - name: DD_VERSION - valueFrom: - fieldRef: - fieldPath: metadata.labels['tags.datadoghq.com/version'] - - name: DD_LOGS_INJECTION - value: "true" - - name: DD_PROFILING_ENABLED - value: "true" - - name: DD_TRACE_SAMPLE_RATE - value: "1" - name: GEN3_UWSGI_TIMEOUT valueFrom: configMapKeyRef: @@ -246,12 +292,6 @@ env: value: /var/www/fence - name: GEN3_DEBUG value: "False" - - name: FENCE_PUBLIC_CONFIG - valueFrom: - configMapKeyRef: - name: manifest-fence - key: fence-config-public.yaml - optional: true - name: PGHOST valueFrom: secretKeyRef: @@ -284,6 +324,16 @@ env: optional: false - name: DB value: postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB) + - name: INDEXD_PASSWORD + valueFrom: + secretKeyRef: + name: indexd-service-creds + key: fence + - name: gen3Env + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname # -- (list) Volumes to attach to the container. volumes: @@ -322,6 +372,10 @@ volumes: - name: yaml-merge configMap: name: "fence-yaml-merge" + optional: false + - name: config-volume-public + configMap: + name: "manifest-fence" optional: true # -- (list) Volumes to mount to the container. @@ -352,7 +406,7 @@ volumeMounts: subPath: "privacy_policy.md" - name: "config-volume" readOnly: true - mountPath: "/var/www/fence/fence-config.yaml" + mountPath: "/var/www/fence/fence-config-secret.yaml" subPath: fence-config.yaml - name: "yaml-merge" readOnly: true @@ -370,13 +424,21 @@ volumeMounts: readOnly: true mountPath: "/fence/keys/key/jwt_private_key.pem" subPath: "jwt_private_key.pem" + - name: "config-volume-public" + readOnly: true + mountPath: "/var/www/fence/fence-config-public.yaml" + subPath: fence-config-public.yaml # -- (list) Volumes to mount to the init container. initVolumeMounts: - name: "config-volume" readOnly: true - mountPath: "/var/www/fence/fence-config.yaml" + mountPath: "/var/www/fence/fence-config-secret.yaml" subPath: fence-config.yaml + - name: "config-volume-public" + readOnly: true + mountPath: "/var/www/fence/fence-config-public.yaml" + subPath: fence-config-public.yaml - name: "yaml-merge" readOnly: true mountPath: "/var/www/fence/yaml_merge.py" @@ -424,23 +486,26 @@ initEnv: optional: false - name: DB value: postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB) + - name: FENCE_DB + value: postgresql://$(PGUSER):$(PGPASSWORD)@$(PGHOST):5432/$(PGDB) - name: PYTHONPATH value: /var/www/fence - - name: FENCE_PUBLIC_CONFIG - valueFrom: - configMapKeyRef: - name: manifest-fence - key: fence-config-public.yaml - optional: true -# -- (map) Labels to use for selecting the deployment. +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Authentication" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl selectorLabels: - app: fence - release: production +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: logo: privacy_policy: - +projects: # USER_SYNC_CRON: # LOCATION: @@ -451,881 +516,222 @@ privacy_policy: # -- (string) USER YAML. Passed in as a multiline string. USER_YAML: | cloud_providers: {} - groups: {} authz: - # policies automatically given to anyone, even if they haven't authenticated - anonymous_policies: ['open_data_reader', 'full_open_access'] + # policies automatically given to anyone, even if they are not authenticated + anonymous_policies: + - open_data_reader - # policies automatically given to authenticated users (in addition to their other - # policies) - all_users_policies: ['open_data_reader', 'authn_open_access'] + # policies automatically given to authenticated users (in addition to their other policies) + all_users_policies: [] - user_project_to_resource: - QA: /programs/QA - DEV: /programs/DEV - test: /programs/QA/projects/test - jenkins: /programs/jnkns/projects/jenkins - jenkins2: /programs/jnkns/projects/jenkins2 - jnkns: /programs/jnkns + groups: + # can CRUD programs and projects and upload data files + - name: data_submitters + policies: + - services.sheepdog-admin + - data_upload + - MyFirstProject_submitter + users: + - username1@gmail.com - policies: - # General Access - - id: 'workspace' - description: 'be able to use workspace' - resource_paths: ['/workspace'] - role_ids: ['workspace_user'] - - id: 'dashboard' - description: 'be able to use the commons dashboard' - resource_paths: ['/dashboard'] - role_ids: ['dashboard_user'] - - id: 'prometheus' - description: 'be able to use prometheus' - resource_paths: ['/prometheus'] - role_ids: ['prometheus_user'] - - id: 'ttyadmin' - description: 'be able to use the admin tty' - resource_paths: ['/ttyadmin'] - role_ids: ['ttyadmin_user'] - - id: 'mds_admin' - description: 'be able to use metadata service' - resource_paths: ['/mds_gateway'] - role_ids: ['mds_user'] - - id: 'data_upload' - description: 'upload raw data files to S3' - role_ids: ['file_uploader'] - resource_paths: ['/data_file'] - - description: be able to use sower job - id: sower - resource_paths: [/sower] - role_ids: [sower_user] - - id: 'mariner_admin' - description: 'full access to mariner API' - resource_paths: ['/mariner'] - role_ids: ['mariner_admin'] - - id: audit_reader - role_ids: - - audit_reader - resource_paths: - - /services/audit - - id: audit_login_reader - role_ids: - - audit_reader - resource_paths: - - /services/audit/login - - id: audit_presigned_url_reader - role_ids: - - audit_reader - resource_paths: - - /services/audit/presigned_url - - id: requestor_admin - role_ids: - - requestor_admin - resource_paths: - - /programs - - id: requestor_reader - role_ids: - - requestor_reader - resource_paths: - - /programs - - id: requestor_creator - role_ids: - - requestor_creator - resource_paths: - - /programs - - id: requestor_updater - role_ids: - - requestor_updater - resource_paths: - - /programs - - id: requestor_deleter - role_ids: - - requestor_deleter - resource_paths: - - /programs - # Data Access - - # All programs policy - - id: 'all_programs_reader' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: ['/programs'] - - # # example if need access to write to storage - # - id: 'programs.jnkns-storage_writer' - # description: '' - # role_ids: - # - 'storage_writer' - # resource_paths: ['/programs/jnkns'] - - - id: 'programs.jnkns-admin' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_reader' - resource_paths: - - '/programs/jnkns' - - '/gen3/programs/jnkns' - - - id: 'programs.jnkns-viewer' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: - - '/programs/jnkns' - - '/gen3/programs/jnkns' - - - - id: 'programs.QA-admin' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_reader' - resource_paths: - - '/programs/QA' - - '/gen3/programs/QA' - - - id: 'programs.QA-admin-no-storage' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - resource_paths: - - '/programs/QA' - - '/gen3/programs/QA' - - - id: 'programs.QA-viewer' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: - - '/programs/QA' - - '/gen3/programs/QA' - - - id: 'programs.DEV-admin' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_reader' - - 'storage_writer' - resource_paths: - - '/programs/DEV' - - '/gen3/programs/DEV' - - - id: 'programs.DEV-storage_writer' - description: '' - role_ids: - - 'storage_writer' - resource_paths: ['/programs/DEV'] - - - id: 'programs.DEV-viewer' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: - - '/programs/DEV' - - '/gen3/programs/DEV' - - - id: 'programs.test-admin' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_reader' - resource_paths: - - '/programs/test' - - '/gen3/programs/test' - - - id: 'programs.test-viewer' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: - - '/programs/test' - - '/gen3/programs/test' - - - id: 'abc-admin' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_reader' - resource_paths: - - '/abc' - - - id: 'gen3-admin' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_reader' - resource_paths: - - '/gen3' - - - id: 'gen3-hmb-researcher' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_reader' - resource_paths: - - '/consents/NRES' - - '/consents/GRU' - - '/consents/GRU_CC' - - '/consents/HMB' - - '/gen3' - - - id: 'abc.programs.test_program.projects.test_project1-viewer' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: - - '/abc/programs/test_program/projects/test_project1' - - - id: 'abc.programs.test_program.projects.test_project2-viewer' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: - - '/abc/programs/test_program/projects/test_project2' - - - id: 'abc.programs.test_program2.projects.test_project3-viewer' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: - - '/abc/programs/test_program2/projects/test_project3' - - # Open data policies - - id: 'authn_open_access' - resource_paths: ['/programs/open/projects/authnRequired'] - description: '' - role_ids: - - 'reader' - - 'storage_reader' - - id: 'full_open_access' - resource_paths: ['/programs/open/projects/1000G'] - description: '' - role_ids: - - 'reader' - - 'storage_reader' - - id: 'open_data_reader' - description: '' - role_ids: - - 'reader' - - 'storage_reader' - resource_paths: ['/open'] - - id: 'open_data_admin' - description: '' - role_ids: - - 'creator' - - 'reader' - - 'updater' - - 'deleter' - - 'storage_writer' - - 'storage_reader' - resource_paths: ['/open'] - - # Consent Code Policies - - id: 'not-for-profit-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/NPU' - - - id: 'publication-required-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/PUB' - - - id: 'gru-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/NRES' - - '/consents/GRU' - - - id: 'gru-cc-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/NRES' - - '/consents/GRU' - - '/consents/GRU_CC' - - - id: 'hmb-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/NRES' - - '/consents/GRU' - - '/consents/GRU_CC' - - '/consents/HMB' - - - id: 'poa-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/NRES' - - '/consents/GRU' - - '/consents/GRU_CC' - - '/consents/POA' - - - id: 'ds-lung-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/NRES' - - '/consents/GRU' - - '/consents/GRU_CC' - - '/consents/HMB' - - '/consents/DS_LungDisease' - - - id: 'ds-chronic-obstructive-pulmonary-disease-researcher' - description: '' - role_ids: - - 'admin' - resource_paths: - - '/consents/NRES' - - '/consents/GRU' - - '/consents/GRU_CC' - - '/consents/HMB' - - '/consents/DS_ChronicObstructivePulmonaryDisease' - - - id: 'services.sheepdog-admin' - description: 'CRUD access to programs and projects' - role_ids: - - 'sheepdog_admin' - resource_paths: - - '/services/sheepdog/submission/program' - - '/services/sheepdog/submission/project' - - # indexd - - id: 'indexd_admin' - description: 'full access to indexd API' - role_ids: - - 'indexd_admin' - resource_paths: - - '/programs' - - '/services/indexd/admin' - # # TODO resource path '/' is not valid right now in arborist, trying to decide - # # how to handle all resources - # - id: 'indexd_admin' - # description: '' - # role_ids: - # - 'indexd_record_creator' - # - 'indexd_record_reader' - # - 'indexd_record_updater' - # - 'indexd_delete_record' - # - 'indexd_storage_reader' - # - 'indexd_storage_writer' - # resource_paths: ['/'] - # - id: 'indexd_record_reader' - # description: '' - # role_ids: - # - 'indexd_record_reader' - # resource_paths: ['/'] - # - id: 'indexd_record_editor' - # description: '' - # role_ids: - # - 'indexd_record_creator' - # - 'indexd_record_reader' - # - 'indexd_record_updater' - # - 'indexd_delete_record' - # resource_paths: ['/'] - # - id: 'indexd_storage_reader' - # description: '' - # role_ids: - # - 'indexd_storage_reader' - # resource_paths: ['/'] - # - id: 'indexd_storage_editor' - # description: '' - # role_ids: - # - 'indexd_storage_reader' - # - 'indexd_storage_writer' - # resource_paths: ['/'] - - # argo - - id: argo - description: be able to use argo - resource_paths: [/argo] - role_ids: [argo_user] + # can create/update/delete indexd records + - name: indexd_admins + policies: + - indexd_admin + users: + - username1@gmail.com resources: - # General Access - - name: 'data_file' - description: 'data files, stored in S3' - - name: 'dashboard' - description: 'commons /dashboard' - - name: 'mds_gateway' - description: 'commons /mds-admin' - - name: 'prometheus' - description: 'commons /prometheus and /grafana' - - name: 'ttyadmin' - description: 'commons /ttyadmin' - - name: 'workspace' - - name: "sower" - - name: 'mariner' - description: 'workflow execution service' - - name: argo - - # OLD Data - - name: 'programs' + - name: workspace + - name: data_file + - name: services + subresources: + - name: sheepdog subresources: - - name: 'open' - subresources: - - name: 'projects' - subresources: - - name: '1000G' - - name: 'authnRequired' - - name: 'QA' - subresources: - - name: 'projects' - subresources: - - name: 'test' - - name: 'DEV' - subresources: - - name: 'projects' - subresources: - - name: 'test' - - name: 'jnkns' - subresources: - - name: 'projects' - subresources: - - name: 'jenkins' - - name: 'jenkins2' - - name: 'test' - subresources: - - name: 'projects' - subresources: - - name: 'test' - - # NEW Data WITH PREFIX - - name: 'gen3' + - name: submission + subresources: + - name: program + - name: project + - name: 'indexd' subresources: - - name: 'programs' - subresources: - - name: 'QA' - subresources: - - name: 'projects' - subresources: - - name: 'test' - - name: 'DEV' - subresources: - - name: 'projects' - subresources: - - name: 'test' - - name: 'jnkns' - subresources: - - name: 'projects' - subresources: - - name: 'jenkins' - - name: 'jenkins2' - - name: 'test' - subresources: - - name: 'projects' - subresources: - - name: 'test' - - # consents obtained from DUO and NIH - # https://github.com/EBISPOT/DUO - # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4721915/ - - name: 'consents' + - name: 'admin' + - name: audit subresources: - - name: 'NRES' - description: 'no restriction' - - name: 'GRU' - description: 'general research use' - - name: 'GRU_CC' - description: 'general research use and clinical care' - - name: 'HMB' - description: 'health/medical/biomedical research' - - name: 'POA' - description: 'population origins or ancestry research' - - name: 'NMDS' - description: 'no general methods research' - - name: 'NPU' - description: 'not-for-profit use only' - - name: 'PUB' - description: 'publication required' - - name: 'DS_LungDisease' - description: 'disease-specific research for lung disease' - - name: 'DS_ChronicObstructivePulmonaryDisease' - description: 'disease-specific research for chronic obstructive pulmonary disease' - - - name: 'abc' + - name: presigned_url + - name: login + - name: open + - name: programs + subresources: + - name: MyFirstProgram subresources: - - name: 'programs' - subresources: - - name: 'foo' - subresources: - - name: 'projects' - subresources: - - name: 'bar' - - name: 'test_program' - subresources: - - name: 'projects' - subresources: - - name: 'test_project1' - - name: 'test_project2' - - name: 'test_program2' - subresources: - - name: 'projects' - subresources: - - name: 'test_project3' - - - # "Sheepdog admin" resources - - name: 'services' - subresources: - - name: 'sheepdog' - subresources: - - name: 'submission' - subresources: - - name: 'program' - - name: 'project' - - name: 'indexd' - subresources: - - name: 'admin' - - name: 'bundles' - - name: audit - subresources: - - name: presigned_url - - name: login - - - - name: 'open' - - # action/methods: - # create, read, update, delete, read-storage, write-storage, - # file_upload, access + - name: projects + subresources: + - name: MyFirstProject + + policies: + - id: workspace + description: be able to use workspace + resource_paths: + - /workspace + role_ids: + - workspace_user + - id: data_upload + description: upload raw data files to S3 + role_ids: + - file_uploader + resource_paths: + - /data_file + - id: services.sheepdog-admin + description: CRUD access to programs and projects + role_ids: + - sheepdog_admin + resource_paths: + - /services/sheepdog/submission/program + - /services/sheepdog/submission/project + - id: indexd_admin + description: full access to indexd API + role_ids: + - indexd_admin + resource_paths: + - /programs + - id: open_data_reader + role_ids: + - peregrine_reader + - guppy_reader + - fence_storage_reader + resource_paths: + - /open + - id: all_programs_reader + role_ids: + - peregrine_reader + - guppy_reader + - fence_storage_reader + resource_paths: + - /programs + - id: MyFirstProject_submitter + role_ids: + - reader + - creator + - updater + - deleter + - storage_reader + - storage_writer + resource_paths: + - /programs/MyFirstProgram/projects/MyFirstProject + roles: - # General Access - - id: 'file_uploader' - description: 'can upload data files' - permissions: - - id: 'file_upload' - action: - service: '*' - method: 'file_upload' - - id: 'workspace_user' - permissions: - - id: 'workspace_access' - action: - service: 'jupyterhub' - method: 'access' - - id: 'dashboard_user' - permissions: - - id: 'dashboard_access' - action: - service: 'dashboard' - method: 'access' - - id: 'mds_user' - permissions: - - id: 'mds_access' - action: - service: 'mds_gateway' - method: 'access' - - id: 'prometheus_user' - permissions: - - id: 'prometheus_access' - action: - service: 'prometheus' - method: 'access' - - id: 'ttyadmin_user' - permissions: - - id: 'ttyadmin_access' - action: - service: 'ttyadmin' - method: 'access' - - id: 'sower_user' - permissions: - - id: 'sower_access' - action: - service: 'job' - method: 'access' - - id: 'mariner_admin' - permissions: - - id: 'mariner_access' - action: - service: 'mariner' - method: 'access' - - id: audit_reader - permissions: - - id: audit_reader_action - action: - service: audit - method: read - - # All services - - id: 'admin' - description: '' - permissions: - - id: 'admin' - action: - service: '*' - method: '*' - - id: 'creator' - description: '' - permissions: - - id: 'creator' - action: - service: '*' - method: 'create' - - id: 'reader' - description: '' - permissions: - - id: 'reader' - action: - service: '*' - method: 'read' - - id: 'updater' - description: '' - permissions: - - id: 'updater' - action: - service: '*' - method: 'update' - - id: 'deleter' - description: '' - permissions: - - id: 'deleter' - action: - service: '*' - method: 'delete' - - id: 'storage_writer' - description: '' - permissions: - - id: 'storage_writer' - action: - service: '*' - method: 'write-storage' - - id: 'storage_reader' - description: '' - permissions: - - id: 'storage_reader' - action: - service: '*' - method: 'read-storage' - - - # Sheepdog admin role - - id: 'sheepdog_admin' - description: 'sheepdog admin role for program project crud' - permissions: - - id: 'sheepdog_admin_action' - action: - service: 'sheepdog' - method: '*' - - - # indexd - - id: 'indexd_admin' - # this only works if indexd.arborist is enabled in manifest! - description: 'full access to indexd API' - permissions: - - id: 'indexd_admin' - action: - service: 'indexd' - method: '*' - - id: 'indexd_record_creator' - description: '' - permissions: - - id: 'indexd_record_creator' - action: - service: 'indexd' - method: 'create' - - id: 'indexd_record_reader' - description: '' - permissions: - - id: 'indexd_record_reader' - action: - service: 'indexd' - method: 'read' - - id: 'indexd_record_updater' - description: '' - permissions: - - id: 'indexd_record_updater' - action: - service: 'indexd' - method: 'update' - - id: 'indexd_delete_record' - description: '' - permissions: - - id: 'indexd_delete_record' - action: - service: 'indexd' - method: 'delete' - - id: 'indexd_storage_reader' - description: '' - permissions: - - id: 'indexd_storage_reader' - action: - service: 'indexd' - method: 'read-storage' - - id: 'indexd_storage_writer' - description: '' - permissions: - - id: 'indexd_storage_writer' - action: - service: 'indexd' - method: 'write-storage' - - # arborist - - id: 'arborist_creator' - description: '' - permissions: - - id: 'arborist_creator' - action: - service: 'arborist' - method: 'create' - - id: 'arborist_reader' - description: '' - permissions: - - id: 'arborist_reader' - action: - service: 'arborist' - method: 'read' - - id: 'arborist_updater' - description: '' - permissions: - - id: 'arborist_updater' - action: - service: 'arborist' - method: 'update' - - id: 'arborist_deleter' - description: '' - permissions: - - id: 'arborist_deleter' - action: - service: 'arborist' - method: 'delete' - - # requestor - - id: requestor_admin - permissions: - - id: requestor_admin_action + - id: file_uploader + permissions: + - id: file_upload + action: + service: fence + method: file_upload + - id: workspace_user + permissions: + - id: workspace_access + action: + service: jupyterhub + method: access + - id: sheepdog_admin + description: CRUD access to programs and projects + permissions: + - id: sheepdog_admin_action + action: + service: sheepdog + method: '*' + - id: indexd_admin + description: full access to indexd API + permissions: + - id: indexd_admin + action: + service: indexd + method: '*' + - id: admin + permissions: + - id: admin action: - service: requestor + service: '*' method: '*' - - id: requestor_reader - permissions: - - id: requestor_reader_action + - id: creator + permissions: + - id: creator action: - service: requestor - method: read - - id: requestor_creator - permissions: - - id: requestor_creator_action - action: - service: requestor + service: '*' method: create - - id: requestor_updater - permissions: - - id: requestor_updater_action + - id: reader + permissions: + - id: reader + action: + service: '*' + method: read + - id: updater + permissions: + - id: updater action: - service: requestor + service: '*' method: update - - id: requestor_deleter - permissions: - - id: requestor_deleter_action + - id: deleter + permissions: + - id: deleter action: - service: requestor + service: '*' method: delete - # argo - - id: argo_user - permissions: - - id: argo_access - action: - service: argo - method: access + - id: storage_writer + permissions: + - id: storage_creator + action: + service: '*' + method: write-storage + - id: storage_reader + permissions: + - id: storage_reader + action: + service: '*' + method: read-storage + - id: peregrine_reader + permissions: + - id: peregrine_reader + action: + method: read + service: peregrine + - id: guppy_reader + permissions: + - id: guppy_reader + action: + method: read + service: guppy + - id: fence_storage_reader + permissions: + - id: fence_storage_reader + action: + method: read-storage + service: fence clients: - basic-test-client: - policies: - - abc-admin - - gen3-admin - basic-test-abc-client: - policies: - - abc-admin wts: policies: - all_programs_reader - - workspace + - open_data_reader users: - ### BEGIN INTERNS SECTION ### - ### END INTERNS SECTION ### - qureshi@uchicago.edu: - admin: true + username1@gmail.com: {} + username2: + tags: + name: John Doe + email: johndoe@gmail.com policies: - - data_upload - - workspace - - dashboard - - mds_admin - - prometheus - - sower - - services.sheepdog-admin - - programs.QA-admin - - programs.test-admin - - programs.DEV-admin - - programs.jnkns-admin - - indexd_admin - - ttyadmin - projects: - - auth_id: QA - privilege: [create, read, update, delete, upload, read-storage] - - auth_id: test - privilege: [create, read, update, delete, upload, read-storage] - - auth_id: DEV - privilege: [create, read, update, delete, upload, read-storage] - - auth_id: jenkins - privilege: [create, read, update, delete, upload, read-storage] - - auth_id: jenkins2 - privilege: [create, read, update, delete, upload, read-storage] - - auth_id: jnkns - privilege: [create, read, update, delete, upload, read-storage] - - -# -- (map) Configuration settings for Fence app + - MyFirstProject_submitter + + cloud_providers: {} + groups: {} + +# -- (map) Public configuration settings for Fence app +FENCE_CONFIG_PUBLIC: {} + +# -- (map) Private configuration settings for Fence app FENCE_CONFIG: # -- (string) Name of the Fence app - APP_NAME: 'Gen3 Data Commons' + APP_NAME: "Gen3 Data Commons" # -- (string) A URL-safe base64-encoded 32-byte key for encrypting keys in db # in python you can use the following script to generate one: @@ -1373,7 +779,7 @@ FENCE_CONFIG: ENABLE_CSRF_PROTECTION: true # -- (str) signing key for WTForms to sign CSRF tokens with - WTF_CSRF_SECRET_KEY: '{{ENCRYPTION_KEY}}' + WTF_CSRF_SECRET_KEY: "{{ENCRYPTION_KEY}}" # -- (bool) fence (at the moment) attempts a migration on startup. setting this to false will disable that # WARNING: ONLY set to false if you do NOT want to automatically migrate your database. @@ -1383,7 +789,6 @@ FENCE_CONFIG: # NOTE: We are working to improve the migration process in the near future ENABLE_DB_MIGRATION: true - # -- (dict) Configurations for OpenID Connect (OIDC) authentication # - Fully configure at least one client so login works # - WARNING: Be careful changing the *_ALLOWED_SCOPES as you can break basic @@ -1391,32 +796,32 @@ FENCE_CONFIG: OPENID_CONNECT: # any OIDC IDP that does not differ from the generic implementation can be # configured without code changes - generic_oidc_idp: # choose a unique ID and replace this key + generic_oidc_idp: # choose a unique ID and replace this key # -- (str) Optional; display name for this IDP - name: 'some_idp' + name: "some_idp" # -- (str) Client ID - client_id: '' + client_id: "" # -- (str) Client secret - client_secret: '' + client_secret: "" # -- (str) Redirect URL for this IDP - redirect_url: '{{BASE_URL}}/login/some_idp/login' # replace IDP name + redirect_url: "{{BASE_URL}}/login/some_idp/login" # replace IDP name # use `discovery` to configure IDPs that do not expose a discovery # endpoint. One of `discovery_url` or `discovery` should be configured # -- (str) URL of the OIDC discovery endpoint for the IDP - discovery_url: 'https://server.com/.well-known/openid-configuration' + discovery_url: "https://server.com/.well-known/openid-configuration" discovery: # -- (str) Authorization endpoint URL - authorization_endpoint: '' + authorization_endpoint: "" # -- (str) Token endpoint URL - token_endpoint: '' + token_endpoint: "" # -- (str) JSON Web Key Set (JWKS) URI - jwks_uri: '' + jwks_uri: "" # -- (str) Optional; claims field to get the user_id from (default "sub") - user_id_field: '' + user_id_field: "" # -- (str) Optional; claims field to get the user email from (default "email") - email_field: '' + email_field: "" # -- (str) Optional; default is "openid" - scope: '' + scope: "" # These Google values must be obtained from Google's Cloud Console # Follow: https://developers.google.com/identity/protocols/OpenIDConnect # @@ -1426,24 +831,24 @@ FENCE_CONFIG: # -- (dict) Configuration for Google authentication provider google: # -- (str) URL of the OIDC discovery endpoint for Google - discovery_url: 'https://accounts.google.com/.well-known/openid-configuration' + discovery_url: "https://accounts.google.com/.well-known/openid-configuration" # -- (str) Client ID - client_id: '' + client_id: "" # -- (str) Client secret - client_secret: '' + client_secret: "" # -- (str) The allowed redirect back to fence, should not need to change - redirect_url: '{{BASE_URL}}/login/google/login/' + redirect_url: "{{BASE_URL}}/login/google/login/" # -- (str) The scope to request from Google (default "openid email") - scope: 'openid email' + scope: "openid email" # if mock is true, will fake a successful login response from Google in /login/google # NOTE: this will also modify the behavior of /link/google endpoints # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) # will login as the username set in cookie DEV_LOGIN_COOKIE_NAME or default provided # here # -- (str) Optional; defaults to '{{MOCK_GOOGLE_AUTH}}' for backwards compatibility with older cfg files - mock: '' + mock: "" # -- (str) Optional; defaults to 'test@example.com' - mock_default_user: 'test@example.com' + mock_default_user: "test@example.com" # -- (dict): Contains multi-tenant Fence configuration # Support for multi-tenant fence (another fence is this fence's IDP) @@ -1453,40 +858,40 @@ FENCE_CONFIG: # -- (str): Root URL for the other fence # this api_base_url should be the root url for the OTHER fence # something like: https://example.com - api_base_url: '' + api_base_url: "" # -- (str): ID of the client of this fence on the other fence # this client_id and client_secret should be obtained by registering THIS fence as # a new client of the OTHER fence - client_id: '' + client_id: "" # -- (str): Secret of the client of this fence on the other fence - client_secret: '' + client_secret: "" # -- dict: Additional client parameters client_kwargs: # -- (str): Space-separated string of scopes # openid is required to use OIDC flow - scope: 'openid' + scope: "openid" # -- (str): The URL to which the other fence will redirect after logging in - redirect_uri: '{{BASE_URL}}/login/fence/login' + redirect_uri: "{{BASE_URL}}/login/fence/login" # -- (str): URL for authorization endpoint of the other fence # The next 3 should not need to be changed if the provider is following # Oauth2 endpoint naming conventions - authorize_url: '{{api_base_url}}/oauth2/authorize' + authorize_url: "{{api_base_url}}/oauth2/authorize" # -- (str): URL for access token endpoint of the other fence - access_token_url: '{{api_base_url}}/oauth2/token' + access_token_url: "{{api_base_url}}/oauth2/token" # -- (str): URL for refresh token endpoint of the other fence - refresh_token_url: '{{api_base_url}}/oauth2/token' + refresh_token_url: "{{api_base_url}}/oauth2/token" # -- (str): Name of the provider for consent screens # Custom name to display for consent screens. If not provided, will use `fence`. # If the other fence is using NIH Login, you should make name: `NIH Login` - name: '' + name: "" # -- (bool): Whether to mock a successful login response for testing purposes # if mock is true, will fake a successful login response for login @@ -1494,72 +899,72 @@ FENCE_CONFIG: mock: false # -- (str): Default user for mock login - mock_default_user: 'test@example.com' + mock_default_user: "test@example.com" # -- (str): URL of the shibboleth discovery endpoint if needed for InCommon login # this is needed to enable InCommon login, if some LOGIN_OPTIONS are configured with idp=fence and a list of shib_idps: - shibboleth_discovery_url: 'https://login.bionimbus.org/Shibboleth.sso/DiscoFeed' + shibboleth_discovery_url: "https://login.bionimbus.org/Shibboleth.sso/DiscoFeed" orcid: - discovery_url: 'https://orcid.org/.well-known/openid-configuration' - client_id: '' - client_secret: '' + discovery_url: "https://orcid.org/.well-known/openid-configuration" + client_id: "" + client_secret: "" # make sure you put the FULL url for this deployment in the allowed redirects in # ORCID.org. DO NOT include {{BASE_URL}} at ORCID.org, you need to actually put the # full url - redirect_url: '{{BASE_URL}}/login/orcid/login/' - scope: 'openid' + redirect_url: "{{BASE_URL}}/login/orcid/login/" + scope: "openid" # if mock is true, will fake a successful login response for login # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) mock: false - mock_default_user: '0000-0002-2601-8132' + mock_default_user: "0000-0002-2601-8132" ras: - discovery_url: 'https://sts.nih.gov/.well-known/openid-configuration' - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/ras/callback' - scope: 'openid email profile ga4gh_passport_v1' + discovery_url: "https://sts.nih.gov/.well-known/openid-configuration" + client_id: "" + client_secret: "" + redirect_url: "{{BASE_URL}}/login/ras/callback" + scope: "openid email profile ga4gh_passport_v1" # if mock is true, will fake a successful login response for login # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) mock: false - mock_default_user: 'test@example.com' + mock_default_user: "test@example.com" # Create a client in Azure here: # https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview # Currently supports organizational account only, so when registering a new App in # Azure, make sure to select the `Accounts in any organizational directory` for # supported account types. microsoft: - discovery_url: 'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration' + discovery_url: "https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration" # after registering a new appl, client_id can be found as # "APPLICATION (CLIENT) ID" in Microsoft Azure - client_id: '' + client_id: "" # You have a generate a secret in Azure for this app, there should be a # "Certificates & secrets" section where you can create a "New client secret" - client_secret: '' + client_secret: "" # make sure you put the FULL url for this deployment in the allowed redirects in # your app in Azure. DO NOT include {{BASE_URL}} in Azure, you need to actually put the # full url - redirect_url: '{{BASE_URL}}/login/microsoft/login/' - scope: 'openid email' + redirect_url: "{{BASE_URL}}/login/microsoft/login/" + scope: "openid email" # if mock is true, will fake a successful login response for login # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) mock: false - mock_default_user: 'test@example.com' + mock_default_user: "test@example.com" # For information on configuring an Okta tenant as an OIDC IdP refer to Okta documentation at: # https://developer.okta.com/docs/reference/api/oidc/#2-okta-as-the-identity-platform-for-your-app-or-api okta: - discovery_url: '' - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/okta/login/' - scope: 'openid email' + discovery_url: "" + client_id: "" + client_secret: "" + redirect_url: "{{BASE_URL}}/login/okta/login/" + scope: "openid email" cognito: # You must create a user pool in order to have a discovery url - discovery_url: 'https://cognito-idp.{REGION}.amazonaws.com/{USER-POOL-ID}/.well-known/openid-configuration' - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/cognito/login/' - scope: 'openid email' + discovery_url: "https://cognito-idp.{REGION}.amazonaws.com/{USER-POOL-ID}/.well-known/openid-configuration" + client_id: "" + client_secret: "" + redirect_url: "{{BASE_URL}}/login/cognito/login/" + scope: "openid email" # In the case where Cognito is being used solely as an intermediary to a single IdP, # and that IdP is a SAML IdP with no 'email_verified' outgoing claim, but it is safe # to assume all emails from this SAML IdP are in fact verified, we may set this to True @@ -1567,28 +972,28 @@ FENCE_CONFIG: # CILogon subscribers can create and manage OIDC clients using COmanage Registry. # Free tier users may request OIDC clients at https://cilogon.org/oauth2/register cilogon: - discovery_url: 'https://cilogon.org/.well-known/openid-configuration' - client_id: '' - client_secret: '' + discovery_url: "https://cilogon.org/.well-known/openid-configuration" + client_id: "" + client_secret: "" # When registering the Callback URLs for your CILogon OIDC client be # sure to include the FULL url for this deployment, including the https:// scheme # and server FQDN. - redirect_url: '{{BASE_URL}}/login/cilogon/login/' - scope: 'openid email profile' + redirect_url: "{{BASE_URL}}/login/cilogon/login/" + scope: "openid email profile" # if mock is true, will fake a successful login response for login # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) mock: false - mock_default_user: 'http://cilogon.org/serverT/users/64703' + mock_default_user: "http://cilogon.org/serverT/users/64703" synapse: - discovery_url: '' - client_id: '' - client_secret: '' - redirect_url: '' - scope: 'openid' + discovery_url: "" + client_id: "" + client_secret: "" + redirect_url: "" + scope: "openid" shibboleth: - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/shib/login' + client_id: "" + client_secret: "" + redirect_url: "{{BASE_URL}}/login/shib/login" # these are the *possible* scopes a client can be given, NOT scopes that are # given to all clients. You can be more restrictive during client creation @@ -1602,7 +1007,6 @@ FENCE_CONFIG: - "google_link" - "ga4gh_passport_v1" - # -- (list) these are the scopes that CAN be included in a user's own access_token USER_ALLOWED_SCOPES: - "fence" @@ -1615,7 +1019,6 @@ FENCE_CONFIG: - "google_link" - "ga4gh_passport_v1" - # -- (list) these are the scopes that a browser session can create for a user (very similar to USER_ALLOWED_SCOPES, as the session will actually create access_tokens for an actively logged in user) SESSION_ALLOWED_SCOPES: - "openid" @@ -1648,15 +1051,15 @@ FENCE_CONFIG: # be used by the frontend to display secondary buttons differently). # -- (list) List of enabled login options (used by data-portal to display login buttons). LOGIN_OPTIONS: - - name: 'Login from Google' - desc: 'description' + - name: "Login from Google" + desc: "description" idp: google # -- (string) Default login provider. - must be configured in LOGIN_OPTIONS and OPENID_CONNECT - - if several options in LOGIN_OPTIONS are defined for this IDP, will default to the first one DEFAULT_LOGIN_IDP: google # -- (string) Default login URL: DEPRECATED and replaced by LOGIN_OPTIONS + DEFAULT_LOGIN_IDP configs - DEFAULT_LOGIN_URL: '{{BASE_URL}}/login/google' + DEFAULT_LOGIN_URL: "{{BASE_URL}}/login/google" # `LOGIN_REDIRECT_WHITELIST` is a list of extra whitelisted URLs which can be redirected # to by the `/login/*` endpoints. Fence automatically populates this with the redirect @@ -1668,7 +1071,6 @@ FENCE_CONFIG: ### DEPRECATED and replaced by OPENID_CONNECT + LOGIN_OPTIONS configs ENABLED_IDENTITY_PROVIDERS: {} - # ////////////////////////////////////////////////////////////////////////////////////// # LIBRARY CONFIGURATION (authlib & flask) # - Already contains reasonable defaults @@ -1676,16 +1078,15 @@ FENCE_CONFIG: # authlib-specific configs for OIDC flow and JWTs # NOTE: the OAUTH2_JWT_KEY cfg gets set automatically by fence if keys are setup # correctly - OAUTH2_JWT_ALG: 'RS256' + OAUTH2_JWT_ALG: "RS256" OAUTH2_JWT_ENABLED: true - OAUTH2_JWT_ISS: '{{BASE_URL}}' - OAUTH2_PROVIDER_ERROR_URI: '/api/oauth2/errors' + OAUTH2_JWT_ISS: "{{BASE_URL}}" + OAUTH2_PROVIDER_ERROR_URI: "/api/oauth2/errors" # used for flask, "path mounted under by the application / web server" # since we deploy as microservices, fence is typically under {{base}}/user # this is also why our BASE_URL default ends in /user - APPLICATION_ROOT: '/user' - + APPLICATION_ROOT: "/user" # ////////////////////////////////////////////////////////////////////////////////////// # Tokens, Lifetimes, & Expirations @@ -1783,9 +1184,9 @@ FENCE_CONFIG: # - Contains defaults for using NIH's Login. # ////////////////////////////////////////////////////////////////////////////////////// # assumes shibboleth is deployed under {{BASE_URL}}/shibboleth - SHIBBOLETH_HEADER: 'persistent_id' - SSO_URL: 'https://auth.nih.gov/affwebservices/public/saml2sso?SPID={{BASE_URL}}/shibboleth&RelayState=' - ITRUST_GLOBAL_LOGOUT: 'https://auth.nih.gov/siteminderagent/smlogout.asp?mode=nih&AppReturnUrl=' + SHIBBOLETH_HEADER: "persistent_id" + SSO_URL: "https://auth.nih.gov/affwebservices/public/saml2sso?SPID={{BASE_URL}}/shibboleth&RelayState=" + ITRUST_GLOBAL_LOGOUT: "https://auth.nih.gov/siteminderagent/smlogout.asp?mode=nih&AppReturnUrl=" # ////////////////////////////////////////////////////////////////////////////////////// # dbGaP USER SYNCING SUPPORT @@ -1798,14 +1199,13 @@ FENCE_CONFIG: # fence's README for more information dbGaP: - info: - host: '' - username: '' - password: '' + host: "" + username: "" + password: "" port: 22 - proxy: '' - proxy_user: '' - protocol: 'sftp' - decrypt_key: '' + proxy: "" + protocol: "sftp" + decrypt_key: "" # parse out the consent from the dbgap accession number such that something # like "phs000123.v1.p1.c2" becomes "phs000123.c2". # @@ -1825,7 +1225,7 @@ FENCE_CONFIG: # subsequently gives access to an Arborist resource representing this common area # as well) study_common_exchange_areas: - 'example': 'test_common_exchange_area' + "example": "test_common_exchange_area" # 'studyX': 'test_common_exchange_area' # 'studyY': 'test_common_exchange_area' # 'studyZ': 'test_common_exchange_area' @@ -1833,8 +1233,8 @@ FENCE_CONFIG: # actual data lives in. For example, `studyX` data may exist in multiple organizations, so # we need to know how to map authorization to all orgs resources study_to_resource_namespaces: - '_default': ['/'] - 'test_common_exchange_area': ['/dbgap/'] + "_default": ["/"] + "test_common_exchange_area": ["/dbgap/"] # above are for default support and exchange area support # below are further examples # @@ -1849,7 +1249,7 @@ FENCE_CONFIG: # Will NOT MATCH forms like: phs000123 # # WARNING: Do not change this without consulting the code that uses it - DBGAP_ACCESSION_WITH_CONSENT_REGEX: '(?Pphs[0-9]+)(.(?Pv[0-9]+)){0,1}(.(?Pp[0-9]+)){0,1}.(?Pc[0-9]+)' + DBGAP_ACCESSION_WITH_CONSENT_REGEX: "(?Pphs[0-9]+)(.(?Pv[0-9]+)){0,1}(.(?Pp[0-9]+)){0,1}.(?Pc[0-9]+)" # ////////////////////////////////////////////////////////////////////////////////////// # STORAGE BACKENDS AND CREDENTIALS @@ -1927,7 +1327,7 @@ FENCE_CONFIG: # `DATA_UPLOAD_BUCKET` specifies an S3 bucket to which data files are uploaded, # using the `/data/upload` endpoint. This must be one of the first keys under # `S3_BUCKETS` (since these are the buckets fence has credentials for). - DATA_UPLOAD_BUCKET: 'bucket1' + DATA_UPLOAD_BUCKET: "bucket1" # ////////////////////////////////////////////////////////////////////////////////////// # PROXY @@ -1946,12 +1346,12 @@ FENCE_CONFIG: # url where indexd microservice is running (for signed urls primarily) # NOTE: Leaving as null will force fence to default to {{BASE_URL}}/index # example value: 'https://example.com/index' - INDEXD: null + INDEXD: http://indexd-service # this is the username which fence uses to make authenticated requests to indexd - INDEXD_USERNAME: 'fence' + INDEXD_USERNAME: "fence" # this is the password which fence uses to make authenticated requests to indexd - INDEXD_PASSWORD: '' + INDEXD_PASSWORD: "" # ////////////////////////////////////////////////////////////////////////////////////// # AZURE STORAGE BLOB CONFIGURATION @@ -1965,13 +1365,13 @@ FENCE_CONFIG: # AZ_BLOB_CONTAINER_URL: 'https://storageaccount.blob.core.windows.net/container/' # this is the container used for uploading, and should match the storage account # used in the connection string for AZ_BLOB_CREDENTIALS - AZ_BLOB_CONTAINER_URL: 'https://myfakeblob.blob.core.windows.net/my-fake-container/' + AZ_BLOB_CONTAINER_URL: "https://myfakeblob.blob.core.windows.net/my-fake-container/" # url where authz microservice is running - ARBORIST: null + ARBORIST: http://arborist-service # url where the audit-service is running - AUDIT_SERVICE: 'http://audit-service' + AUDIT_SERVICE: "http://audit-service" ENABLE_AUDIT_LOGS: presigned_url: false login: false @@ -1994,20 +1394,20 @@ FENCE_CONFIG: # Setting this up allows fence to create buckets, manage Google groups, etc. # See directions here for setting up cirrus: https://github.com/uc-cdis/cirrus CIRRUS_CFG: - GOOGLE_API_KEY: '' - GOOGLE_PROJECT_ID: '' - GOOGLE_APPLICATION_CREDENTIALS: '' - GOOGLE_STORAGE_CREDS: '' - GOOGLE_ADMIN_EMAIL: '' - GOOGLE_IDENTITY_DOMAIN: '' - GOOGLE_CLOUD_IDENTITY_ADMIN_EMAIL: '' + GOOGLE_API_KEY: "" + GOOGLE_PROJECT_ID: "" + GOOGLE_APPLICATION_CREDENTIALS: "" + GOOGLE_STORAGE_CREDS: "" + GOOGLE_ADMIN_EMAIL: "" + GOOGLE_IDENTITY_DOMAIN: "" + GOOGLE_CLOUD_IDENTITY_ADMIN_EMAIL: "" # Prefix to namespace Google Groups on a single Cloud Identity (see cirrus # setup for more info on Cloud Identity) # # NOTE: Make this short! Less than 8 characters if possible. Google has # length restrictions on group names. - GOOGLE_GROUP_PREFIX: '' + GOOGLE_GROUP_PREFIX: "" # Prefix to namespace Google Service Accounts in a single Google Cloud Platform Project. # This is primarily to support multiple instances of fence references the same Google @@ -2015,7 +1415,7 @@ FENCE_CONFIG: # # NOTE: Make this short! Less than 8 characters if possible. Google has # length restrictions on service account names. - GOOGLE_SERVICE_ACCOUNT_PREFIX: '' + GOOGLE_SERVICE_ACCOUNT_PREFIX: "" # A Google Project identitifier representing the default project to bill to for # accessing Google Requester Pays buckets (for signed urls and/or temporary service account @@ -2054,17 +1454,17 @@ FENCE_CONFIG: # # NOTE: Example in comments below GUN_MAIL: - 'datacommons.io': - smtp_hostname: 'smtp.mailgun.org' - api_key: '' - default_login: 'postmaster@mailgun.example.com' - api_url: 'https://api.mailgun.net/v3/mailgun.example.com' - smtp_password: '' + "datacommons.io": + smtp_hostname: "smtp.mailgun.org" + api_key: "" + default_login: "postmaster@mailgun.example.com" + api_url: "https://api.mailgun.net/v3/mailgun.example.com" + smtp_password: "" # For emails regarding users certificates - EMAIL_SERVER: 'localhost' - SEND_FROM: 'example@gmail.com' - SEND_TO: 'example@gmail.com' + EMAIL_SERVER: "localhost" + SEND_FROM: "example@gmail.com" + SEND_TO: "example@gmail.com" # ////////////////////////////////////////////////////////////////////////////////////// # DATA ACCESS: GOOGLE LINKING & SERVICE ACCOUNT REGISTRATION @@ -2095,9 +1495,9 @@ FENCE_CONFIG: REMOVE_SERVICE_ACCOUNT_EMAIL_NOTIFICATION: enable: false # this domain MUST exist in GUN_MAIL config - domain: 'example.com' - from: 'do-not-reply@example.com' - subject: 'User service account removal notification' + domain: "example.com" + from: "do-not-reply@example.com" + subject: "User service account removal notification" # the {} gets replaced dynamically in the Python code to be the Project ID content: > Service accounts were removed from access control data because some users or @@ -2108,13 +1508,13 @@ FENCE_CONFIG: # # WARNING: This is NOT a bcc so the email is visible to the end-user admin: - - 'admin@example.edu' + - "admin@example.edu" PROBLEM_USER_EMAIL_NOTIFICATION: # this domain MUST exist in GUN_MAIL config - domain: 'example.com' - from: 'do-not-reply@example.com' - subject: 'Account access error notification' + domain: "example.com" + from: "do-not-reply@example.com" + subject: "Account access error notification" # the {} gets replaced dynamically in the Python code to be the Project ID content: > The Data Commons Framework utilizes dbGaP for data access authorization. @@ -2126,55 +1526,55 @@ FENCE_CONFIG: # # WARNING: This is NOT a bcc so the email is visible to the end-user admin: - - 'admin@example.edu' + - "admin@example.edu" # Service account email domains that represent a service account that Google owns. # These are usually created when a sepcific GCP service is enabled. # This is used for Service Account Validation for Data Access. GOOGLE_MANAGED_SERVICE_ACCOUNT_DOMAINS: - - 'dataflow-service-producer-prod.iam.gserviceaccount.com' - - 'cloudbuild.gserviceaccount.com' - - 'cloud-ml.google.com.iam.gserviceaccount.com' - - 'container-engine-robot.iam.gserviceaccount.com' - - 'dataflow-service-producer-prod.iam.gserviceaccount.com' - - 'sourcerepo-service-accounts.iam.gserviceaccount.com' - - 'dataproc-accounts.iam.gserviceaccount.com' - - 'gae-api-prod.google.com.iam.gserviceaccount.com' - - 'genomics-api.google.com.iam.gserviceaccount.com' - - 'containerregistry.iam.gserviceaccount.com' - - 'container-analysis.iam.gserviceaccount.com' - - 'cloudservices.gserviceaccount.com' - - 'stackdriver-service.iam.gserviceaccount.com' - - 'appspot.gserviceaccount.com' - - 'partnercontent.gserviceaccount.com' - - 'trifacta-gcloud-prod.iam.gserviceaccount.com' - - 'gcf-admin-robot.iam.gserviceaccount.com' - - 'compute-system.iam.gserviceaccount.com' - - 'gcp-sa-websecurityscanner.iam.gserviceaccount.com' - - 'storage-transfer-service.iam.gserviceaccount.com' - - 'firebase-sa-management.iam.gserviceaccount.com' - - 'firebase-rules.iam.gserviceaccount.com' - - 'gcp-sa-cloudbuild.iam.gserviceaccount.com' - - 'gcp-sa-automl.iam.gserviceaccount.com' - - 'gcp-sa-datalabeling.iam.gserviceaccount.com' - - 'gcp-sa-cloudscheduler.iam.gserviceaccount.com' + - "dataflow-service-producer-prod.iam.gserviceaccount.com" + - "cloudbuild.gserviceaccount.com" + - "cloud-ml.google.com.iam.gserviceaccount.com" + - "container-engine-robot.iam.gserviceaccount.com" + - "dataflow-service-producer-prod.iam.gserviceaccount.com" + - "sourcerepo-service-accounts.iam.gserviceaccount.com" + - "dataproc-accounts.iam.gserviceaccount.com" + - "gae-api-prod.google.com.iam.gserviceaccount.com" + - "genomics-api.google.com.iam.gserviceaccount.com" + - "containerregistry.iam.gserviceaccount.com" + - "container-analysis.iam.gserviceaccount.com" + - "cloudservices.gserviceaccount.com" + - "stackdriver-service.iam.gserviceaccount.com" + - "appspot.gserviceaccount.com" + - "partnercontent.gserviceaccount.com" + - "trifacta-gcloud-prod.iam.gserviceaccount.com" + - "gcf-admin-robot.iam.gserviceaccount.com" + - "compute-system.iam.gserviceaccount.com" + - "gcp-sa-websecurityscanner.iam.gserviceaccount.com" + - "storage-transfer-service.iam.gserviceaccount.com" + - "firebase-sa-management.iam.gserviceaccount.com" + - "firebase-rules.iam.gserviceaccount.com" + - "gcp-sa-cloudbuild.iam.gserviceaccount.com" + - "gcp-sa-automl.iam.gserviceaccount.com" + - "gcp-sa-datalabeling.iam.gserviceaccount.com" + - "gcp-sa-cloudscheduler.iam.gserviceaccount.com" # The types of service accounts that are allowed to be registered at # /google/service_accounts endpoints ALLOWED_USER_SERVICE_ACCOUNT_DOMAINS: # compute engine default service account - - 'developer.gserviceaccount.com' + - "developer.gserviceaccount.com" # app engine default service account - - 'appspot.gserviceaccount.com' + - "appspot.gserviceaccount.com" # user-managed service account - - 'iam.gserviceaccount.com' + - "iam.gserviceaccount.com" # Synapse integration and DREAM challenge mapping. Team is from Synapse, and group is # providing the actual permission in Arborist. User will be added to the group for TTL # seconds if the team matches. - DREAM_CHALLENGE_TEAM: 'DREAM' - DREAM_CHALLENGE_GROUP: 'DREAM' - SYNAPSE_URI: 'https://repo-prod.prod.sagebase.org/auth/v1' + DREAM_CHALLENGE_TEAM: "DREAM" + DREAM_CHALLENGE_GROUP: "DREAM" + SYNAPSE_URI: "https://repo-prod.prod.sagebase.org/auth/v1" SYNAPSE_JWKS_URI: # deprecated, use the discovery_url in the OPENID_CONNECT block for the synapse client SYNAPSE_DISCOVERY_URL: @@ -2189,14 +1589,14 @@ FENCE_CONFIG: # If user registers, add them to configured Arborist group; idea is that the Arborist group # will have access to download data. REGISTER_USERS_ON: false - REGISTERED_USERS_GROUP: '' + REGISTERED_USERS_GROUP: "" # RAS refresh_tokens expire in 15 days RAS_REFRESH_EXPIRATION: 1296000 # List of JWT issuers from which Fence will accept GA4GH visas GA4GH_VISA_ISSUER_ALLOWLIST: - - '{{BASE_URL}}' - - 'https://sts.nih.gov' - - 'https://stsstg.nih.gov' + - "{{BASE_URL}}" + - "https://sts.nih.gov" + - "https://stsstg.nih.gov" # Number of projects that can be registered to a Google Service Accont SERVICE_ACCOUNT_LIMIT: 6 @@ -2204,7 +1604,7 @@ FENCE_CONFIG: # None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint # True: Parse for all clients i.e. a fence client will always sync their visas during login # False: Parse for no clients i.e. a fence client will not be able to sync visas during login even with parse_visas param - GLOBAL_PARSE_VISAS_ON_LOGIN: + GLOBAL_PARSE_VISAS_ON_LOGIN: false # Settings for usersync with visas USERSYNC: sync_from_visas: false @@ -2212,4 +1612,4 @@ FENCE_CONFIG: fallback_to_dbgap_sftp: false visa_types: ras: ["https://ras.nih.gov/visas/v1", "https://ras.nih.gov/visas/v1.1"] - RAS_USERINFO_ENDPOINT: '/openid/connect/v1.1/userinfo' + RAS_USERINFO_ENDPOINT: "/openid/connect/v1.1/userinfo" diff --git a/helm/frontend-framework/Chart.yaml b/helm/frontend-framework/Chart.yaml new file mode 100644 index 00000000..b885da7f --- /dev/null +++ b/helm/frontend-framework/Chart.yaml @@ -0,0 +1,29 @@ +apiVersion: v2 +name: frontend-framework +description: A Helm chart for the gen3 frontend framework + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and, therefore, cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.6 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "develop" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/frontend-framework/README.md b/helm/frontend-framework/README.md new file mode 100644 index 00000000..e33ae46f --- /dev/null +++ b/helm/frontend-framework/README.md @@ -0,0 +1,93 @@ +# frontend-framework + +![Version: 0.1.6](https://img.shields.io/badge/Version-0.1.6-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: develop](https://img.shields.io/badge/AppVersion-develop-informational?style=flat-square) + +A Helm chart for the gen3 frontend framework + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | map | `{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["frontend-framework"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]}}` | Affinity to use for the deployment. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution | map | `[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["frontend-framework"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]` | Option for scheduling to be required or preferred. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0] | int | `{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["frontend-framework"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}` | Weight value for preferred scheduling. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0] | list | `{"key":"app","operator":"In","values":["frontend-framework"]}` | Label key for match expression. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].operator | string | `"In"` | Operation type for the match expression. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values | list | `["frontend-framework"]` | Value for the match expression key. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey | string | `"kubernetes.io/hostname"` | Value for topology key label. | +| automountServiceAccountToken | bool | `false` | Automount the default service account token | +| autoscaling | map | `{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Configuration for autoscaling the number of replicas | +| autoscaling.enabled | bool | `false` | Whether autoscaling is enabled | +| autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | +| autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| env | list | `[]` | List of environment variables to add to the deployment. | +| fullnameOverride | string | `""` | Override the full name of the deployment. | +| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":{"enabled":false},"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | +| global.dev | bool | `true` | Whether the deployment is for development purposes. | +| global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | +| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.hostname | string | `"localhost"` | Hostname for the deployment. | +| global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | +| global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.portalApp | string | `"gitops"` | Portal application name. | +| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | +| global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | +| global.postgres.master.host | string | `nil` | hostname of postgres server | +| global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | +| global.postgres.master.port | string | `"5432"` | Port for Postgres. | +| global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | +| global.publicDataSets | bool | `true` | Whether public datasets are enabled. | +| global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | +| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | +| global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private`. | +| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | +| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/frontend-framework","tag":"develop"}` | Docker image information. | +| image.pullPolicy | string | `"Always"` | Docker pull policy. | +| image.repository | string | `"quay.io/cdis/frontend-framework"` | Docker repository. | +| image.tag | string | `"develop"` | Overrides the image tag whose default is the chart appVersion. | +| imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| nameOverride | string | `""` | Override the name of the chart. | +| nodeSelector | map | `{}` | Node selector to apply to the pod | +| partOf | string | `"Front-End"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | +| podAnnotations | map | `{}` | Annotations to add to the pod | +| podSecurityContext | map | `{}` | Security context to apply to the pod | +| port | int | `3000` | | +| release | string | `"dev"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | +| replicaCount | int | `1` | Number of replicas for the deployment. | +| resources | map | `{"limits":{"cpu":0.6,"memory":"4096Mi"},"requests":{"cpu":0.6,"memory":"512Mi"}}` | Resource requests and limits for the containers in the pod | +| resources.limits | map | `{"cpu":0.6,"memory":"4096Mi"}` | The maximum amount of resources that the container is allowed to use | +| resources.limits.cpu | string | `0.6` | The maximum amount of memory the container can use | +| resources.requests | map | `{"cpu":0.6,"memory":"512Mi"}` | The amount of resources that the container requests | +| resources.requests.cpu | string | `0.6` | The amount of CPU requested | +| resources.requests.memory | string | `"512Mi"` | The amount of memory requested | +| revisionHistoryLimit | int | `2` | Number of old revisions to retain | +| securityContext | map | `{}` | Security context to apply to the container | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | +| service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | +| service.port | int | `80` | The port number that the service exposes. | +| service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | +| serviceAccount | map | `{"annotations":{},"create":true,"name":""}` | Service account to use or create. | +| serviceAccount.annotations | map | `{}` | Annotations to add to the service account. | +| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | +| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | +| strategy | map | `{"rollingUpdate":{"maxSurge":2,"maxUnavailable":"25%"},"type":"RollingUpdate"}` | Rolling update deployment strategy | +| strategy.rollingUpdate.maxSurge | int | `2` | Number of additional replicas to add during rollout. | +| strategy.rollingUpdate.maxUnavailable | int | `"25%"` | Maximum amount of pods that can be unavailable during the update. | +| tolerations | list | `[]` | Tolerations to apply to the pod | diff --git a/helm/pidgin/templates/NOTES.txt b/helm/frontend-framework/templates/NOTES.txt similarity index 100% rename from helm/pidgin/templates/NOTES.txt rename to helm/frontend-framework/templates/NOTES.txt diff --git a/helm/frontend-framework/templates/_helpers.tpl b/helm/frontend-framework/templates/_helpers.tpl new file mode 100644 index 00000000..851e1655 --- /dev/null +++ b/helm/frontend-framework/templates/_helpers.tpl @@ -0,0 +1,79 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "frontend-framework.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "frontend-framework.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "frontend-framework.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "frontend-framework.labels" -}} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "frontend-framework.selectorLabels" -}} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "frontend-framework.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "frontend-framework.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Define tierAccessLevel +*/}} +{{- define "frontend-framework.tierAccessLevel" -}} +{{- if .Values.global }} +{{- .Values.global.tierAccessLevel }} +{{- else}} +{{- .Values.tierAccessLevel }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/frontend-framework/templates/configMap.yaml b/helm/frontend-framework/templates/configMap.yaml new file mode 100644 index 00000000..f8cd1c23 --- /dev/null +++ b/helm/frontend-framework/templates/configMap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "frontend-framework.fullname" . }}-configmap + labels: + {{- include "frontend-framework.labels" . | nindent 4 }} +data: + {{- range $key, $val := .Values.env }} + {{ $key }}: {{ $val | quote }} + {{- end }} \ No newline at end of file diff --git a/helm/frontend-framework/templates/deployment.yaml b/helm/frontend-framework/templates/deployment.yaml new file mode 100644 index 00000000..f8a65135 --- /dev/null +++ b/helm/frontend-framework/templates/deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-framework-deployment + labels: + {{- include "frontend-framework.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "frontend-framework.selectorLabels" . | nindent 6 }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + strategy: + {{- toYaml .Values.strategy | nindent 8 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configMap.yaml") . | sha256sum }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "frontend-framework.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} + public: "yes" + spec: + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.automountServiceAccountToken}} + containers: + - name: frontend-framework + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + livenessProbe: + httpGet: + {{- if eq "portal" .Values.global.frontendRoot }} + path: /ff + {{- else }} + path: / + {{- end }} + port: {{ .Values.port }} + initialDelaySeconds: 3 + periodSeconds: 10 + timeoutSeconds: 30 + readinessProbe: + httpGet: + {{- if eq "portal" .Values.global.frontendRoot }} + path: /ff + {{- else }} + path: / + {{- end }} + port: {{ .Values.port }} + initialDelaySeconds: 3 + periodSeconds: 10 + timeoutSeconds: 30 + resources: + {{- toYaml .Values.resources | nindent 12 }} + ports: + - containerPort: {{ .Values.port }} + name: http + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "frontend-framework.fullname" . }}-configmap + optional: true + env: + - name: PORT + value: {{ .Values.port | quote }} + - name: HOSTNAME + value: revproxy-service + {{- if eq "portal" .Values.global.frontendRoot }} + - name: BASE_PATH + value: /ff + {{- else}} + - name: NEXT_PUBLIC_PORTAL_BASENAME + value: /portal + {{- end }} + diff --git a/helm/frontend-framework/templates/service.yaml b/helm/frontend-framework/templates/service.yaml new file mode 100644 index 00000000..4d20da96 --- /dev/null +++ b/helm/frontend-framework/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: "frontend-framework-service" + labels: + {{- include "frontend-framework.labels" . | nindent 4 }} +spec: + ports: + - protocol: TCP + port: 80 + targetPort: {{ .Values.port }} + name: http + - protocol: TCP + port: 443 + targetPort: {{ .Values.port }} + name: https + type: ClusterIP + selector: + {{- include "frontend-framework.selectorLabels" . | nindent 4 }} diff --git a/helm/elasticsearch/templates/serviceaccount.yaml b/helm/frontend-framework/templates/serviceaccount.yaml similarity index 63% rename from helm/elasticsearch/templates/serviceaccount.yaml rename to helm/frontend-framework/templates/serviceaccount.yaml index 1f191c55..5357e89e 100644 --- a/helm/elasticsearch/templates/serviceaccount.yaml +++ b/helm/frontend-framework/templates/serviceaccount.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "elasticsearch.serviceAccountName" . }} + name: {{ include "frontend-framework.serviceAccountName" . }} labels: - {{- include "elasticsearch.labels" . | nindent 4 }} + {{- include "frontend-framework.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} diff --git a/helm/pidgin/values.yaml b/helm/frontend-framework/values.yaml similarity index 52% rename from helm/pidgin/values.yaml rename to helm/frontend-framework/values.yaml index 95e1eb84..3f7fb150 100644 --- a/helm/pidgin/values.yaml +++ b/helm/frontend-framework/values.yaml @@ -1,7 +1,6 @@ -# Default values for pidgin. +# Default values for frontend-framework. # This is a YAML-formatted file. # Declare variables to be passed into your templates. - # -- (map) Global configuration options. global: # -- (map) AWS configuration @@ -48,33 +47,73 @@ global: userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true - # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` + # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private`. tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false - -# -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you -postgres: - # (bool) Whether the database should be restored from s3. Default to global.postgres.dbRestore - dbRestore: false - # -- (bool) Whether the database should be created. Default to global.postgres.dbCreate - dbCreate: - # -- (string) Hostname for postgres server. This is a service override, defaults to global.postgres.host - host: - # -- (string) Database name for postgres. This is a service override, defaults to - - database: - # -- (string) Username for postgres. This is a service override, defaults to - - username: - # -- (string) Port for Postgres. - port: "5432" - # -- (string) Password for Postgres. Will be autogenerated if left empty. - password: - -# Deployment + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (int) Number of replicas for the deployment. +replicaCount: 1 + +# -- (map) Docker image information. +image: + # -- (string) Docker repository. + repository: quay.io/cdis/frontend-framework + # -- (string) Docker pull policy. + pullPolicy: Always + # -- (string) Overrides the image tag whose default is the chart appVersion. + tag: "develop" + +# -- (list) Docker image pull secrets. +imagePullSecrets: [] + +# -- (string) Override the name of the chart. +nameOverride: "" + +# -- (string) Override the full name of the deployment. +fullnameOverride: "" + +# -- (map) Service account to use or create. +serviceAccount: + # -- (bool) Specifies whether a service account should be created. + create: true + # -- (map) Annotations to add to the service account. + annotations: {} + # -- (string) The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# -- (map) Annotations to add to the pod +podAnnotations: {} + +# -- (map) Security context to apply to the pod +podSecurityContext: + {} + # fsGroup: 2000 + +# -- (map) Security context to apply to the container +securityContext: + {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# -- (map) Kubernetes service information. +service: + # -- (string) Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". + type: ClusterIP + # -- (int) The port number that the service exposes. + port: 80 + # -- (map) Configuration for autoscaling the number of replicas autoscaling: # -- (bool) Whether autoscaling is enabled @@ -85,9 +124,18 @@ autoscaling: maxReplicas: 100 # -- (int) The target CPU utilization percentage for autoscaling targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 -# -- (int) Number of desired replicas -replicaCount: 1 +port: 3000 + +# -- (list) List of environment variables to add to the deployment. +env: [] + +# -- (map) Node selector to apply to the pod +nodeSelector: {} + +# -- (list) Tolerations to apply to the pod +tolerations: [] # -- (int) Number of old revisions to retain revisionHistoryLimit: 2 @@ -97,64 +145,55 @@ strategy: type: RollingUpdate rollingUpdate: # -- (int) Number of additional replicas to add during rollout. - maxSurge: 1 + maxSurge: 2 # -- (int) Maximum amount of pods that can be unavailable during the update. - maxUnavailable: 0 - -# -- (bool) Whether Datadog is enabled. -dataDog: - enabled: false - env: dev + maxUnavailable: 25% # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - pidgin - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - frontend-framework + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (bool) Automount the default service account token automountServiceAccountToken: false -image: - # -- (string) The Docker image repository for the fence service - repository: quay.io/cdis/pidgin - # -- (string) When to pull the image. - pullPolicy: Always - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: "" - # -- (map) Resource requests and limits for the containers in the pod resources: - # limits: - # cpu: 1 - # memory: 512Mi + # -- (map) The amount of resources that the container requests + requests: + # -- (string) The amount of CPU requested + cpu: 0.6 + # -- (string) The amount of memory requested + memory: 512Mi + # -- (map) The maximum amount of resources that the container is allowed to use + limits: + # -- (string) The maximum amount of memory the container can use + cpu: 0.6 + memory: 4096Mi -# Service and Pod -# -- (map) Kubernetes service information. -service: - # -- (string) Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". - type: ClusterIP - # -- (list) The port numbers that the service exposes. - port: - - protocol: TCP - port: 80 - targetPort: 80 - name: http - - protocol: TCP - port: 443 - targetPort: 443 - name: https +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "dev" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Front-End" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/elasticsearch/.helmignore b/helm/gen3-network-policies/.helmignore similarity index 100% rename from helm/elasticsearch/.helmignore rename to helm/gen3-network-policies/.helmignore diff --git a/helm/gen3-network-policies/Chart.yaml b/helm/gen3-network-policies/Chart.yaml new file mode 100644 index 00000000..c1a81dfc --- /dev/null +++ b/helm/gen3-network-policies/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: gen3-network-policies +description: A Helm chart that holds network policies needed to run Gen3 + +type: application + +version: 0.1.2 + +appVersion: "0.1.2" diff --git a/helm/gen3-network-policies/README.md b/helm/gen3-network-policies/README.md new file mode 100644 index 00000000..be9c4d42 --- /dev/null +++ b/helm/gen3-network-policies/README.md @@ -0,0 +1,57 @@ +# gen3-network-policies + +![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.2](https://img.shields.io/badge/AppVersion-0.1.2-informational?style=flat-square) + +A Helm chart that holds network policies needed to run Gen3 + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| argo-workflows.enabled | bool | `true` | | +| argocd.enabled | bool | `true` | | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false,"region":"us-east-1","secretStoreServiceAccount":{"enabled":false,"name":"secret-store-sa","roleArn":null},"useLocalSecret":{"enabled":false,"localSecretName":null}}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | +| global.aws.region | string | `"us-east-1"` | AWS region for this deployment | +| global.aws.secretStoreServiceAccount | map | `{"enabled":false,"name":"secret-store-sa","roleArn":null}` | Service account and AWS role for authentication to AWS Secrets Manager | +| global.aws.secretStoreServiceAccount.enabled | bool | `false` | Set true if deploying to AWS and want to use service account and IAM role instead of aws keys. Must provide role-arn. | +| global.aws.secretStoreServiceAccount.name | string | `"secret-store-sa"` | Name of the service account to create | +| global.aws.secretStoreServiceAccount.roleArn | string | `nil` | AWS Role ARN for Secret Store to use | +| global.aws.useLocalSecret | map | `{"enabled":false,"localSecretName":null}` | Local secret setting if using a pre-exising secret. | +| global.aws.useLocalSecret.enabled | bool | `false` | Set to true if you would like to use a secret that is already running on your cluster. | +| global.aws.useLocalSecret.localSecretName | string | `nil` | Name of the local secret. | +| global.dev | bool | `true` | Deploys postgres/elasticsearch for dev | +| global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces in same cluster. | +| global.externalSecrets | map | `{"dbCreate":false,"deploy":false}` | External Secrets settings. | +| global.externalSecrets.dbCreate | bool | `false` | Will create the databases and store the creds in Kubernetes Secrets even if externalSecrets is deployed. Useful if you want to use ExternalSecrets for other secrets besides db secrets. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override secrets you have deployed. | +| global.frontendRoot | string | `"portal"` | Which app will be served on /. Needs be set to portal for portal, or "gen3ff" for frontendframework. | +| global.hostname | string | `"localhost"` | Hostname for the deployment. | +| global.manifestGlobalExtraValues | map | `{}` | If you would like to add any extra values to the manifest-global configmap. | +| global.netPolicy | bool | `{"dbSubnet":"","enabled":false}` | Global flags to control and manage network policies for a Gen3 installation NOTE: Network policies are currently a beta feature. Use with caution! | +| global.netPolicy.dbSubnet | array | `""` | A CIDR range representing a database subnet, that services with a database need access to | +| global.netPolicy.enabled | bool | `false` | Whether network policies are enabled | +| global.portalApp | string | `"gitops"` | Portal application name. | +| global.postgres.dbCreate | bool | `true` | Whether the database create job should run. | +| global.postgres.master.host | string | `nil` | global postgres master host | +| global.postgres.master.password | string | `nil` | global postgres master password | +| global.postgres.master.port | string | `"5432"` | global postgres master port | +| global.postgres.master.username | string | `"postgres"` | global postgres master username | +| global.publicDataSets | bool | `true` | Whether public datasets are enabled. | +| global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | +| global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | +| global.tierAccessLimit | int | `"1000"` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | +| s3CidrRanges[0].ipBlock.cidr | string | `"18.34.0.0/19"` | | +| s3CidrRanges[1].ipBlock.cidr | string | `"16.15.192.0/18"` | | +| s3CidrRanges[2].ipBlock.cidr | string | `"54.231.0.0/16"` | | +| s3CidrRanges[3].ipBlock.cidr | string | `"52.216.0.0/15"` | | +| s3CidrRanges[4].ipBlock.cidr | string | `"18.34.232.0/21"` | | +| s3CidrRanges[5].ipBlock.cidr | string | `"16.15.176.0/20"` | | +| s3CidrRanges[6].ipBlock.cidr | string | `"16.182.0.0/16"` | | +| s3CidrRanges[7].ipBlock.cidr | string | `"3.5.0.0/19"` | | +| s3CidrRanges[8].ipBlock.cidr | string | `"44.192.134.240/28"` | | +| s3CidrRanges[9].ipBlock.cidr | string | `"44.192.140.64/28"` | | diff --git a/helm/pidgin/templates/_helpers.tpl b/helm/gen3-network-policies/templates/_helpers.tpl similarity index 68% rename from helm/pidgin/templates/_helpers.tpl rename to helm/gen3-network-policies/templates/_helpers.tpl index 388e3197..344fd593 100644 --- a/helm/pidgin/templates/_helpers.tpl +++ b/helm/gen3-network-policies/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "pidgin.name" -}} +{{- define "gen3-network-policies.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} @@ -10,7 +10,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "pidgin.fullname" -}} +{{- define "gen3-network-policies.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "pidgin.chart" -}} +{{- define "gen3-network-policies.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "pidgin.labels" -}} -helm.sh/chart: {{ include "pidgin.chart" . }} -{{ include "pidgin.selectorLabels" . }} +{{- define "gen3-network-policies.labels" -}} +helm.sh/chart: {{ include "gen3-network-policies.chart" . }} +{{ include "gen3-network-policies.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -45,30 +45,18 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "pidgin.selectorLabels" -}} -app.kubernetes.io/name: {{ include "pidgin.name" . }} +{{- define "gen3-network-policies.selectorLabels" -}} +app.kubernetes.io/name: {{ include "gen3-network-policies.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ include "pidgin.name" . }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "pidgin.serviceAccountName" -}} +{{- define "gen3-network-policies.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "pidgin.fullname" .) .Values.serviceAccount.name }} +{{- default (include "gen3-network-policies.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} - -{{/* -Define ddEnabled -*/}} -{{- define "pidgin.ddEnabled" -}} -{{- if .Values.global }} -{{- .Values.global.ddEnabled }} -{{- else}} -{{- .Values.dataDog.enabled }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/allow_nothing_netpolicy.yaml b/helm/gen3-network-policies/templates/allow_nothing_netpolicy.yaml new file mode 100644 index 00000000..387cac04 --- /dev/null +++ b/helm/gen3-network-policies/templates/allow_nothing_netpolicy.yaml @@ -0,0 +1,11 @@ +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: allow-nothing-netpolicy +spec: + policyTypes: + - Ingress + - Egress + podSelector: {} + ingress: [] + egress: [] diff --git a/helm/gen3-network-policies/templates/allowdns_netpolicy.yaml b/helm/gen3-network-policies/templates/allowdns_netpolicy.yaml new file mode 100644 index 00000000..5fd50016 --- /dev/null +++ b/helm/gen3-network-policies/templates/allowdns_netpolicy.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allowdns-netpolicy +spec: + podSelector: + matchLabels: {} + egress: + - to: + - namespaceSelector: + {} + ports: + - protocol: TCP + port: 53 + - protocol: UDP + port: 53 + + policyTypes: + - Egress diff --git a/helm/gen3-network-policies/templates/argo_workflows_netpolicy.yaml b/helm/gen3-network-policies/templates/argo_workflows_netpolicy.yaml new file mode 100644 index 00000000..1760ddb7 --- /dev/null +++ b/helm/gen3-network-policies/templates/argo_workflows_netpolicy.yaml @@ -0,0 +1,35 @@ +{{- if index .Values "argo-workflows" "enabled" }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: argo-workflows-netpolicy +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - revproxy + - cohort-middleware + - wts + - indexd + ingress: + - from: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - port: 80 + - port: 4000 + - port: 8080 + - port: 81 + - port: 82 + - port: 443 + egress: + - to: + - namespaceSelector: + matchLabels: + app: argo + policyTypes: + - Ingress + - Egress +{{- end }} \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/argocd_netpolicy.yaml b/helm/gen3-network-policies/templates/argocd_netpolicy.yaml new file mode 100644 index 00000000..a3861e5e --- /dev/null +++ b/helm/gen3-network-policies/templates/argocd_netpolicy.yaml @@ -0,0 +1,34 @@ +{{- if .Values.argocd.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: argocd-netpolicy +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - revproxy + - cohort-middleware + - wts + ingress: + - from: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - port: 80 + - port: 4000 + - port: 8080 + - port: 81 + - port: 82 + - port: 443 + egress: + - to: + - namespaceSelector: + matchLabels: + app: argocd + policyTypes: + - Ingress + - Egress +{{- end }} \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/auth_netpolicy.yaml b/helm/gen3-network-policies/templates/auth_netpolicy.yaml new file mode 100644 index 00000000..28f96a83 --- /dev/null +++ b/helm/gen3-network-policies/templates/auth_netpolicy.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: auth-egress-netpolicy +spec: + egress: + - to: + - podSelector: + matchLabels: + authprovider: "yes" + podSelector: {} + policyTypes: + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: auth-ingress-netpolicy +spec: + ingress: + - from: + - podSelector: {} + podSelector: + matchLabels: + authprovider: "yes" + policyTypes: + - Ingress \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/dev_elasticsearch_netpolicy.yaml b/helm/gen3-network-policies/templates/dev_elasticsearch_netpolicy.yaml new file mode 100644 index 00000000..0b544b8e --- /dev/null +++ b/helm/gen3-network-policies/templates/dev_elasticsearch_netpolicy.yaml @@ -0,0 +1,29 @@ +{{- if .Values.global.dev }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: dev-elasticsearch-ingress-netpolicy +spec: + podSelector: + matchLabels: + app: gen3-elasticsearch-master + policyTypes: + - Ingress + ingress: + - from: + - podSelector: {} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: dev-elasticsearch-egress-netpolicy +spec: + podSelector: {} + policyTypes: + - Egress + egress: + - to: + - podSelector: + matchLabels: + app: gen3-elasticsearch-master +{{- end }} \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/dev_kube_api_netpolicy.yaml b/helm/gen3-network-policies/templates/dev_kube_api_netpolicy.yaml new file mode 100644 index 00000000..85c13912 --- /dev/null +++ b/helm/gen3-network-policies/templates/dev_kube_api_netpolicy.yaml @@ -0,0 +1,18 @@ +# This one needs some explanation. As far as we can tell, on AWS, other network policies allow traffic to reach the +# Kubernetes API. If you're running in another env (such as a local deployment), you'll need this policy to allow access +# to the API server, without knowing ahead of time what the IP address is (since selectors won't work) + +{{- if .Values.global.dev }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: dev-allow-ambassador-egress-netpolicy +spec: + podSelector: + matchLabels: + app: ambassador + policyTypes: + - Egress + egress: + - {} +{{- end }} \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/dev_postgres_netpolicy.yaml b/helm/gen3-network-policies/templates/dev_postgres_netpolicy.yaml new file mode 100644 index 00000000..3ea5de58 --- /dev/null +++ b/helm/gen3-network-policies/templates/dev_postgres_netpolicy.yaml @@ -0,0 +1,29 @@ +{{- if .Values.global.dev }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: dev-postgres-ingress-netpolicy +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: postgresql + policyTypes: + - Ingress + ingress: + - from: + - podSelector: {} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: dev-postgres-egress-netpolicy +spec: + podSelector: {} + policyTypes: + - Egress + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: postgresql +{{- end }} \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/external_egress_netpolicy.yaml b/helm/gen3-network-policies/templates/external_egress_netpolicy.yaml new file mode 100644 index 00000000..5d17ce5a --- /dev/null +++ b/helm/gen3-network-policies/templates/external_egress_netpolicy.yaml @@ -0,0 +1,22 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: external-egress-netpolicy +spec: + egress: + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 169.254.0.0/16 + - 172.16.0.0/12 + - 10.0.0.0/8 + # - to: + # - ipBlock: + # # TODO this looks like squid, do we need it? + # cidr: 172.26.225.72/32 + podSelector: + matchLabels: + internet: "yes" + policyTypes: + - Egress \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/gen3job_netpolicy.yaml b/helm/gen3-network-policies/templates/gen3job_netpolicy.yaml new file mode 100644 index 00000000..f4d7317d --- /dev/null +++ b/helm/gen3-network-policies/templates/gen3job_netpolicy.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: gen3job-netpolicy +spec: + podSelector: + matchLabels: + app: gen3job + egress: + - {} + policyTypes: + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: gen3job-in-netpolicy +spec: + podSelector: {} + ingress: + - from: + - podSelector: + matchLabels: + app: gen3job + policyTypes: + - Ingress diff --git a/helm/gen3-network-policies/templates/jh_netpolicy.yaml b/helm/gen3-network-policies/templates/jh_netpolicy.yaml new file mode 100644 index 00000000..82c02509 --- /dev/null +++ b/helm/gen3-network-policies/templates/jh_netpolicy.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: jupyter-hub-netpolicy +spec: + podSelector: + matchLabels: + app: jupyter-hub + ingress: + - from: + - podSelector: + matchLabels: + app: revproxy + ports: + - port: 3838 + egress: + - {} + policyTypes: + - Ingress + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: jupyter-hub-nb-netpolicy +spec: + podSelector: + matchLabels: + app: jupyterhub + ingress: + - from: + - podSelector: + matchLabels: + app: jupyter-hub + ports: + - {} + egress: + - {} + policyTypes: + - Ingress + - Egress diff --git a/helm/gen3-network-policies/templates/linklocal_netpolicy.yaml b/helm/gen3-network-policies/templates/linklocal_netpolicy.yaml new file mode 100644 index 00000000..aacb0e72 --- /dev/null +++ b/helm/gen3-network-policies/templates/linklocal_netpolicy.yaml @@ -0,0 +1,14 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: linklocal-netpolicy +spec: + egress: + - to: + - ipBlock: + cidr: 169.254.0.0/16 + podSelector: + matchLabels: + linklocal: "yes" + policyTypes: + - Egress \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/nolimit_netpolicy.yaml b/helm/gen3-network-policies/templates/nolimit_netpolicy.yaml new file mode 100644 index 00000000..b0ab7f61 --- /dev/null +++ b/helm/gen3-network-policies/templates/nolimit_netpolicy.yaml @@ -0,0 +1,12 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: nolimit-netpolicy +spec: + egress: + - {} + podSelector: + matchLabels: + netnolimit: "yes" + policyTypes: + - Egress \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/public_netpolicy.yaml b/helm/gen3-network-policies/templates/public_netpolicy.yaml new file mode 100644 index 00000000..a6364e7c --- /dev/null +++ b/helm/gen3-network-policies/templates/public_netpolicy.yaml @@ -0,0 +1,38 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: public-ingress-to-netpolicy +spec: + podSelector: + matchLabels: + public: "yes" + ingress: + - from: + - podSelector: + matchLabels: + app: revproxy + - podSelector: + matchLabels: + app: ambassador-gen3 + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: public-egress-to-netpolicy +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - revproxy + - ambassador-gen3 + egress: + - to: + - podSelector: + matchLabels: + public: "yes" + policyTypes: + - Egress diff --git a/helm/gen3-network-policies/templates/s3_netpolicy.yaml b/helm/gen3-network-policies/templates/s3_netpolicy.yaml new file mode 100644 index 00000000..b7ed1303 --- /dev/null +++ b/helm/gen3-network-policies/templates/s3_netpolicy.yaml @@ -0,0 +1,12 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: s3-netpolicy +spec: + egress: + - to: {{ toYaml .Values.s3CidrRanges | nindent 4}} + podSelector: + matchLabels: + s3: "yes" + policyTypes: + - Egress \ No newline at end of file diff --git a/helm/gen3-network-policies/templates/vpc_netpolicy.yaml b/helm/gen3-network-policies/templates/vpc_netpolicy.yaml new file mode 100644 index 00000000..9d214618 --- /dev/null +++ b/helm/gen3-network-policies/templates/vpc_netpolicy.yaml @@ -0,0 +1,14 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: vpc-netpolicy +spec: + egress: + - to: + - ipBlock: + cidr: 172.0.0.0/8 + podSelector: + matchLabels: + netvpc: "yes" + policyTypes: + - Egress \ No newline at end of file diff --git a/helm/gen3-network-policies/values.yaml b/helm/gen3-network-policies/values.yaml new file mode 100644 index 00000000..0492a5be --- /dev/null +++ b/helm/gen3-network-policies/values.yaml @@ -0,0 +1,109 @@ +# Global configuration +global: + # -- (map) AWS configuration + aws: + # -- (string) AWS region for this deployment + region: us-east-1 + # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. + enabled: false + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: + # -- (map) Service account and AWS role for authentication to AWS Secrets Manager + secretStoreServiceAccount: + # -- (bool) Set true if deploying to AWS and want to use service account and IAM role instead of aws keys. Must provide role-arn. + enabled: false + # -- (string) Name of the service account to create + name: secret-store-sa + # -- (string) AWS Role ARN for Secret Store to use + roleArn: + # -- (map) Local secret setting if using a pre-exising secret. + useLocalSecret: + # -- (bool) Set to true if you would like to use a secret that is already running on your cluster. + enabled: false + # -- (string) Name of the local secret. + localSecretName: + # -- (bool) Deploys postgres/elasticsearch for dev + dev: true + postgres: + # -- (bool) Whether the database create job should run. + dbCreate: true + master: + # -- global postgres master username + username: postgres + # -- global postgres master password + password: + # -- global postgres master host + host: + # -- global postgres master port + port: "5432" + # -- (string) Environment name. + # This should be the same as vpcname if you're doing an AWS deployment. + # Currently this is being used to share ALB's if you have multiple namespaces in same cluster. + environment: default + # -- (string) Hostname for the deployment. + hostname: localhost + # -- (string) ARN of the reverse proxy certificate. + revproxyArn: arn:aws:acm:us-east-1:123456:certificate + # -- (string) URL of the data dictionary. + dictionaryUrl: https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json + # -- (string) Portal application name. + portalApp: gitops + # -- (bool) Whether public datasets are enabled. + publicDataSets: true + # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` + tierAccessLevel: libre + # -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. + tierAccessLimit: "1000" + # -- (bool) Global flags to control and manage network policies for a Gen3 installation + # NOTE: Network policies are currently a beta feature. Use with caution! + netPolicy: + # -- (bool) Whether network policies are enabled + enabled: false + + # -- (array) A CIDR range representing a database subnet, that services with a database need access to + dbSubnet: "" + # -- (int) Number of dispatcher jobs. + dispatcherJobNum: "10" + # -- (map) If you would like to add any extra values to the manifest-global configmap. + manifestGlobalExtraValues: {} + # -- (string) Which app will be served on /. Needs be set to portal for portal, or "gen3ff" for frontendframework. + frontendRoot: "portal" + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override secrets you have deployed. + deploy: false + # -- (bool) Will create the databases and store the creds in Kubernetes Secrets even if externalSecrets is deployed. Useful if you want to use ExternalSecrets for other secrets besides db secrets. + dbCreate: false + + +argo-workflows: + enabled: true + +argocd: + enabled: true + +# This is a list of CIDR ranges that may be used by AWS. This is needed +# to allow egress to S3 for services that need it +s3CidrRanges: + - ipBlock: + cidr: 18.34.0.0/19 + - ipBlock: + cidr: 16.15.192.0/18 + - ipBlock: + cidr: 54.231.0.0/16 + - ipBlock: + cidr: 52.216.0.0/15 + - ipBlock: + cidr: 18.34.232.0/21 + - ipBlock: + cidr: 16.15.176.0/20 + - ipBlock: + cidr: 16.182.0.0/16 + - ipBlock: + cidr: 3.5.0.0/19 + - ipBlock: + cidr: 44.192.134.240/28 + - ipBlock: + cidr: 44.192.140.64/28 diff --git a/helm/gen3/Chart.yaml b/helm/gen3/Chart.yaml index 7db78746..84b9ccd7 100644 --- a/helm/gen3/Chart.yaml +++ b/helm/gen3/Chart.yaml @@ -2,97 +2,118 @@ apiVersion: v2 name: gen3 description: Helm chart to deploy Gen3 Data Commons -# Dependancies +# Dependencies dependencies: -- name: ambassador - version: "0.1.4" - repository: "file://../ambassador" - condition: ambassador.enabled -- name: arborist - version: "0.1.5" - repository: "file://../arborist" - condition: arborist.enabled -- name: argo-wrapper - version: "0.1.1" - repository: "file://../argo-wrapper" - condition: argo-wrapper.enabled -- name: audit - version: "0.1.5" - repository: "file://../audit" - condition: audit.enabled -- name: aws-es-proxy - version: "0.1.3" - repository: "file://../aws-es-proxy" - condition: aws-es-proxy.enabled -- name: common - version: "0.1.4" - repository: file://../common -- name: fence - version: "0.1.5" - repository: "file://../fence" - condition: fence.enabled -- name: guppy - version: "0.1.4" - repository: "file://../guppy" - condition: guppy.enabled -- name: hatchery - version: "0.1.3" - repository: "file://../hatchery" - condition: hatchery.enabled -- name: indexd - version: "0.1.5" - repository: "file://../indexd" - condition: indexd.enabled -- name: manifestservice - version: "0.1.4" - repository: "file://../manifestservice" - condition: manifestservice.enabled -- name: metadata - version: "0.1.5" - repository: "file://../metadata" - condition: metadata.enabled -- name: peregrine - version: "0.1.6" - repository: "file://../peregrine" - condition: peregrine.enabled -- name: pidgin - version: "0.1.4" - repository: "file://../pidgin" - condition: pidgin.enabled -- name: portal - version: "0.1.3" - repository: "file://../portal" - condition: portal.enabled -- name: requestor - version: "0.1.5" - repository: "file://../requestor" - condition: requestor.enabled -- name: revproxy - version: "0.1.5" - repository: "file://../revproxy" - condition: revproxy.enabled -- name: sheepdog - version: "0.1.6" - repository: "file://../sheepdog" - condition: sheepdog.enabled -- name: ssjdispatcher - version: "0.1.2" - repository: "file://../ssjdispatcher" - condition: ssjdispatcher.enabled -- name: wts - version: "0.1.6" - repository: "file://../wts" - condition: wts.enabled + - name: ambassador + version: 0.1.16 + repository: "file://../ambassador" + condition: ambassador.enabled + - name: arborist + version: 0.1.14 + repository: "file://../arborist" + condition: arborist.enabled + - name: argo-wrapper + version: 0.1.10 + repository: "file://../argo-wrapper" + condition: argo-wrapper.enabled + - name: audit + version: 0.1.16 + repository: "file://../audit" + condition: audit.enabled + - name: aws-es-proxy + version: 0.1.13 + repository: "file://../aws-es-proxy" + condition: aws-es-proxy.enabled + - name: common + version: 0.1.16 + repository: file://../common + - name: etl + version: 0.1.7 + repository: file://../etl + condition: etl.enabled + - name: frontend-framework + version: 0.1.6 + repository: "file://../frontend-framework" + condition: frontend-framework.enabled + - name: fence + version: 0.1.27 + repository: "file://../fence" + condition: fence.enabled + - name: guppy + version: 0.1.16 + repository: "file://../guppy" + condition: guppy.enabled + - name: hatchery + version: 0.1.12 + repository: "file://../hatchery" + condition: hatchery.enabled + - name: indexd + version: 0.1.18 + repository: "file://../indexd" + condition: indexd.enabled + - name: manifestservice + version: 0.1.18 + repository: "file://../manifestservice" + condition: manifestservice.enabled + - name: metadata + version: 0.1.17 + repository: "file://../metadata" + condition: metadata.enabled + - name: peregrine + version: 0.1.17 + repository: "file://../peregrine" + condition: peregrine.enabled + - name: portal + version: 0.1.22 + repository: "file://../portal" + condition: portal.enabled + - name: requestor + version: 0.1.16 + repository: "file://../requestor" + condition: requestor.enabled + - name: revproxy + version: 0.1.19 + repository: "file://../revproxy" + condition: revproxy.enabled + - name: sheepdog + version: 0.1.20 + repository: "file://../sheepdog" + condition: sheepdog.enabled + - name: ssjdispatcher + version: 0.1.15 + repository: "file://../ssjdispatcher" + condition: ssjdispatcher.enabled + - name: sower + version: 0.1.16 + condition: sower.enabled + repository: "file://../sower" + - name: wts + version: 0.1.18 + repository: "file://../wts" + condition: wts.enabled + - name: gen3-network-policies + version: 0.1.2 + repository: "file://../gen3-network-policies" + condition: global.netPolicy.enabled + - name: elasticsearch + version: 7.10.2 + repository: "https://helm.elastic.co" + condition: global.dev + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: global.dev -- name: elasticsearch - version: "0.1.2" - repository: "file://../elasticsearch" - condition: global.dev -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: global.dev + # (optional) NeuVector Kubernetes Security Policy templates to protect Gen3 + # NeuVector must be installed separately. + # Reference: https://open-docs.neuvector.com/basics/overview + # Reference: https://github.com/neuvector/neuvector-helm + # For more information, please use the Gen3 community Slack. + - name: neuvector + version: "0.1.2" + repository: "file://../neuvector" + condition: neuvector.enabled # A chart can be either an 'application' or a 'library' chart. # @@ -107,7 +128,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.8 +version: 0.1.52 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm/gen3/README.md b/helm/gen3/README.md index bdd7d2ba..a39e4aed 100644 --- a/helm/gen3/README.md +++ b/helm/gen3/README.md @@ -1,6 +1,6 @@ # gen3 -![Version: 0.1.8](https://img.shields.io/badge/Version-0.1.8-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.52](https://img.shields.io/badge/Version-0.1.52-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) Helm chart to deploy Gen3 Data Commons @@ -18,157 +18,167 @@ Helm chart to deploy Gen3 Data Commons | Repository | Name | Version | |------------|------|---------| -| file://../ambassador | ambassador | 0.1.4 | -| file://../arborist | arborist | 0.1.5 | -| file://../argo-wrapper | argo-wrapper | 0.1.1 | -| file://../audit | audit | 0.1.5 | -| file://../aws-es-proxy | aws-es-proxy | 0.1.3 | -| file://../common | common | 0.1.4 | -| file://../elasticsearch | elasticsearch | 0.1.2 | -| file://../fence | fence | 0.1.5 | -| file://../guppy | guppy | 0.1.4 | -| file://../hatchery | hatchery | 0.1.3 | -| file://../indexd | indexd | 0.1.5 | -| file://../manifestservice | manifestservice | 0.1.4 | -| file://../metadata | metadata | 0.1.5 | -| file://../peregrine | peregrine | 0.1.6 | -| file://../pidgin | pidgin | 0.1.4 | -| file://../portal | portal | 0.1.3 | -| file://../requestor | requestor | 0.1.5 | -| file://../revproxy | revproxy | 0.1.5 | -| file://../sheepdog | sheepdog | 0.1.6 | -| file://../ssjdispatcher | ssjdispatcher | 0.1.2 | -| file://../wts | wts | 0.1.6 | +| file://../ambassador | ambassador | 0.1.16 | +| file://../arborist | arborist | 0.1.14 | +| file://../argo-wrapper | argo-wrapper | 0.1.10 | +| file://../audit | audit | 0.1.16 | +| file://../aws-es-proxy | aws-es-proxy | 0.1.13 | +| file://../common | common | 0.1.16 | +| file://../etl | etl | 0.1.7 | +| file://../fence | fence | 0.1.27 | +| file://../frontend-framework | frontend-framework | 0.1.6 | +| file://../gen3-network-policies | gen3-network-policies | 0.1.2 | +| file://../guppy | guppy | 0.1.16 | +| file://../hatchery | hatchery | 0.1.12 | +| file://../indexd | indexd | 0.1.18 | +| file://../manifestservice | manifestservice | 0.1.18 | +| file://../metadata | metadata | 0.1.17 | +| file://../neuvector | neuvector | 0.1.2 | +| file://../peregrine | peregrine | 0.1.17 | +| file://../portal | portal | 0.1.22 | +| file://../requestor | requestor | 0.1.16 | +| file://../revproxy | revproxy | 0.1.19 | +| file://../sheepdog | sheepdog | 0.1.20 | +| file://../sower | sower | 0.1.16 | +| file://../ssjdispatcher | ssjdispatcher | 0.1.15 | +| file://../wts | wts | 0.1.18 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | +| https://helm.elastic.co | elasticsearch | 7.10.2 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| -| ambassador | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for ambassador chart. | | ambassador.enabled | bool | `true` | Whether to deploy the ambassador subchart. | -| ambassador.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| ambassador.image.repository | string | `nil` | The Docker image repository for the ambassador service. | -| ambassador.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| arborist | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for arborist chart. | | arborist.enabled | bool | `true` | Whether to deploy the arborist subchart. | -| arborist.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| arborist.image.repository | string | `nil` | The Docker image repository for the arborist service. | -| arborist.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| argo-wrapper | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for argo-wrapper chart. | -| argo-wrapper.enabled | bool | `true` | Whether to deploy the argo-wrapper subchart. | -| argo-wrapper.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| argo-wrapper.image.repository | string | `nil` | The Docker image repository for the argo-wrapper service. | -| argo-wrapper.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| audit | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for audit chart. | +| argo-wrapper.enabled | bool | `false` | Whether to deploy the argo-wrapper subchart. | | audit.enabled | bool | `true` | Whether to deploy the audit subchart. | -| audit.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| audit.image.repository | string | `nil` | The Docker image repository for the audit service. | -| audit.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| aws-es-proxy | map | `{"enabled":false}` | Configurations for aws-es-proxy chart. | | aws-es-proxy.enabled | bool | `false` | Whether to deploy the aws-es-proxy subchart. | -| fence | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for fence chart. | +| aws-es-proxy.esEndpoint | str | `"test.us-east-1.es.amazonaws.com"` | Elasticsearch endpoint in AWS | +| aws-es-proxy.secrets | map | `{"awsAccessKeyId":"","awsSecretAccessKey":""}` | Secret information | +| aws-es-proxy.secrets.awsAccessKeyId | str | `""` | AWS access key ID for aws-es-proxy | +| aws-es-proxy.secrets.awsSecretAccessKey | str | `""` | AWS secret access key for aws-es-proxy | +| elasticsearch.clusterHealthCheckParams | string | `"wait_for_status=yellow&timeout=1s"` | | +| elasticsearch.clusterName | string | `"gen3-elasticsearch"` | | +| elasticsearch.esConfig."elasticsearch.yml" | string | `"# Here we can add elasticsearch config\n"` | | +| elasticsearch.maxUnavailable | int | `0` | | +| elasticsearch.replicas | int | `1` | | +| elasticsearch.singleNode | bool | `true` | | +| etl.enabled | bool | `true` | Whether to deploy the etl subchart. | | fence.enabled | bool | `true` | Whether to deploy the fence subchart. | -| fence.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| fence.image.repository | string | `nil` | The Docker image repository for the fence service. | -| fence.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| global | map | `{"aws":{"account":{"aws_access_key_id":null,"aws_secret_access_key":null},"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","gcp":true,"hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","tls":{"cert":null,"key":null},"userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | -| global.aws.account | map | `{"aws_access_key_id":null,"aws_secret_access_key":null}` | Credentials for AWS | +| fence.usersync | map | `{"addDbgap":false,"onlyDbgap":false,"schedule":"*/30 * * * *","slack_send_dbgap":false,"slack_webhook":"None","syncFromDbgap":false,"userYamlS3Path":"s3://cdis-gen3-users/helm-test/user.yaml","usersync":false}` | Configuration options for usersync cronjob. | +| fence.usersync.addDbgap | bool | `false` | Force attempting a dbgap sync if "true", falls back on user.yaml | +| fence.usersync.onlyDbgap | bool | `false` | Forces ONLY a dbgap sync if "true", IGNORING user.yaml | +| fence.usersync.schedule | string | `"*/30 * * * *"` | The cron schedule expression to use in the usersync cronjob. Runs every 30 minutes by default. | +| fence.usersync.slack_send_dbgap | bool | `false` | Will echo what files we are seeing on dbgap ftp to Slack. | +| fence.usersync.slack_webhook | string | `"None"` | Slack webhook endpoint used with certain jobs. | +| fence.usersync.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | +| fence.usersync.userYamlS3Path | string | `"s3://cdis-gen3-users/helm-test/user.yaml"` | Path to the user.yaml file in S3. | +| fence.usersync.usersync | bool | `false` | Whether to run Fence usersync or not. | +| frontend-framework | map | `{"enabled":false,"image":{"repository":"quay.io/cdis/frontend-framework","tag":"develop"}}` | Configurations for frontend-framework chart. | +| frontend-framework.enabled | bool | `false` | Whether to deploy the frontend-framework subchart. | +| frontend-framework.image | map | `{"repository":"quay.io/cdis/frontend-framework","tag":"develop"}` | Docker image information. | +| frontend-framework.image.repository | string | `"quay.io/cdis/frontend-framework"` | The Docker image repository for the frontend-framework. | +| frontend-framework.image.tag | string | `"develop"` | Overrides the image tag whose default is the chart appVersion. | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false,"region":"us-east-1","secretStoreServiceAccount":{"enabled":false,"name":"secret-store-sa","roleArn":null},"useLocalSecret":{"enabled":false,"localSecretName":null}}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | -| global.dev | bool | `true` | Whether the deployment is for development purposes. | +| global.aws.region | string | `"us-east-1"` | AWS region for this deployment | +| global.aws.secretStoreServiceAccount | map | `{"enabled":false,"name":"secret-store-sa","roleArn":null}` | Service account and AWS role for authentication to AWS Secrets Manager | +| global.aws.secretStoreServiceAccount.enabled | bool | `false` | Set true if deploying to AWS and want to use service account and IAM role instead of aws keys. Must provide role-arn. | +| global.aws.secretStoreServiceAccount.name | string | `"secret-store-sa"` | Name of the service account to create | +| global.aws.secretStoreServiceAccount.roleArn | string | `nil` | AWS Role ARN for Secret Store to use | +| global.aws.useLocalSecret | map | `{"enabled":false,"localSecretName":null}` | Local secret setting if using a pre-exising secret. | +| global.aws.useLocalSecret.enabled | bool | `false` | Set to true if you would like to use a secret that is already running on your cluster. | +| global.aws.useLocalSecret.localSecretName | string | `nil` | Name of the local secret. | +| global.dev | bool | `true` | Deploys postgres/elasticsearch for dev | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | -| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | -| global.gcp | map | `true` | AWS configuration | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces in same cluster. | +| global.externalSecrets | map | `{"dbCreate":false,"deploy":false}` | External Secrets settings. | +| global.externalSecrets.dbCreate | bool | `false` | Will create the databases and store the creds in Kubernetes Secrets even if externalSecrets is deployed. Useful if you want to use ExternalSecrets for other secrets besides db secrets. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override secrets you have deployed. | +| global.frontendRoot | string | `"portal"` | Which app will be served on /. Needs be set to portal for portal, or "gen3ff" for frontendframework. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | -| global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | -| global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.manifestGlobalExtraValues | map | `{}` | If you would like to add any extra values to the manifest-global configmap. | +| global.netPolicy | bool | `{"dbSubnet":"","enabled":false}` | Global flags to control and manage network policies for a Gen3 installation NOTE: Network policies are currently a beta feature. Use with caution! | +| global.netPolicy.dbSubnet | array | `""` | A CIDR range representing a database subnet, that services with a database need access to | +| global.netPolicy.enabled | bool | `false` | Whether network policies are enabled | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | -| global.postgres.dbCreate | bool | `true` | Whether the database should be created. | -| global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | -| global.postgres.master.host | string | `nil` | hostname of postgres server | -| global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | -| global.postgres.master.port | string | `"5432"` | Port for Postgres. | -| global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | +| global.postgres.dbCreate | bool | `true` | Whether the database create job should run. | +| global.postgres.master.host | string | `nil` | global postgres master host | +| global.postgres.master.password | string | `nil` | global postgres master password | +| global.postgres.master.port | string | `"5432"` | global postgres master port | +| global.postgres.master.username | string | `"postgres"` | global postgres master username | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | -| guppy | map | `{"enabled":false,"image":{"repository":null,"tag":null}}` | Configurations for guppy chart. | +| global.tierAccessLimit | int | `"1000"` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | +| guppy | map | `{"enabled":false}` | Configurations for guppy chart. | | guppy.enabled | bool | `false` | Whether to deploy the guppy subchart. | -| guppy.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| guppy.image.repository | string | `nil` | The Docker image repository for the guppy service. | -| guppy.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| hatchery | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for hatchery chart. | | hatchery.enabled | bool | `true` | Whether to deploy the hatchery subchart. | -| hatchery.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| hatchery.image.repository | string | `nil` | The Docker image repository for the hatchery service. | -| hatchery.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| indexd | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for indexd chart. | +| hatchery.hatchery.containers[0].args[0] | string | `"--NotebookApp.base_url=/lw-workspace/proxy/"` | | +| hatchery.hatchery.containers[0].args[1] | string | `"--NotebookApp.default_url=/lab"` | | +| hatchery.hatchery.containers[0].args[2] | string | `"--NotebookApp.password=''"` | | +| hatchery.hatchery.containers[0].args[3] | string | `"--NotebookApp.token=''"` | | +| hatchery.hatchery.containers[0].args[4] | string | `"--NotebookApp.shutdown_no_activity_timeout=5400"` | | +| hatchery.hatchery.containers[0].args[5] | string | `"--NotebookApp.quit_button=False"` | | +| hatchery.hatchery.containers[0].command[0] | string | `"start-notebook.sh"` | | +| hatchery.hatchery.containers[0].cpu-limit | string | `"1.0"` | cpu limit of workspace container | +| hatchery.hatchery.containers[0].env | object | `{"FRAME_ANCESTORS":"https://{{ .Values.global.hostname }}"}` | environment variables for workspace container | +| hatchery.hatchery.containers[0].fs-gid | int | `100` | | +| hatchery.hatchery.containers[0].gen3-volume-location | string | `"/home/jovyan/.gen3"` | | +| hatchery.hatchery.containers[0].image | string | `"quay.io/cdis/heal-notebooks:combined_tutorials__latest"` | docker image for workspace | +| hatchery.hatchery.containers[0].lifecycle-post-start[0] | string | `"/bin/sh"` | | +| hatchery.hatchery.containers[0].lifecycle-post-start[1] | string | `"-c"` | | +| hatchery.hatchery.containers[0].lifecycle-post-start[2] | string | `"export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; ln -s /data /home/$IAM/pd/; true"` | | +| hatchery.hatchery.containers[0].memory-limit | string | `"2Gi"` | memory limit of workspace container | +| hatchery.hatchery.containers[0].name | string | `"(Tutorials) Example Analysis Jupyter Lab Notebooks"` | name of workspace | +| hatchery.hatchery.containers[0].path-rewrite | string | `"/lw-workspace/proxy/"` | | +| hatchery.hatchery.containers[0].ready-probe | string | `"/lw-workspace/proxy/"` | | +| hatchery.hatchery.containers[0].target-port | int | `8888` | port to proxy traffic to in docker contaniner | +| hatchery.hatchery.containers[0].use-tls | string | `"false"` | | +| hatchery.hatchery.containers[0].user-uid | int | `1000` | | +| hatchery.hatchery.containers[0].user-volume-location | string | `"/home/jovyan/pd"` | | +| hatchery.hatchery.sidecarContainer.args | list | `[]` | Arguments to pass to the sidecare container. | +| hatchery.hatchery.sidecarContainer.command | list | `["/bin/bash","./sidecar.sh"]` | Commands to run for the sidecar container. | +| hatchery.hatchery.sidecarContainer.cpu-limit | string | `"0.1"` | The maximum amount of CPU the sidecar container can use | +| hatchery.hatchery.sidecarContainer.env | map | `{"HOSTNAME":"{{ .Values.global.hostname }}","NAMESPACE":"{{ .Release.Namespace }}"}` | Environment variables to pass to the sidecar container | +| hatchery.hatchery.sidecarContainer.image | string | `"quay.io/cdis/ecs-ws-sidecar:master"` | The sidecar image. | +| hatchery.hatchery.sidecarContainer.lifecycle-pre-stop[0] | string | `"su"` | | +| hatchery.hatchery.sidecarContainer.lifecycle-pre-stop[1] | string | `"-c"` | | +| hatchery.hatchery.sidecarContainer.lifecycle-pre-stop[2] | string | `"echo test"` | | +| hatchery.hatchery.sidecarContainer.lifecycle-pre-stop[3] | string | `"-s"` | | +| hatchery.hatchery.sidecarContainer.lifecycle-pre-stop[4] | string | `"/bin/sh"` | | +| hatchery.hatchery.sidecarContainer.lifecycle-pre-stop[5] | string | `"root"` | | +| hatchery.hatchery.sidecarContainer.memory-limit | string | `"256Mi"` | The maximum amount of memory the sidecar container can use | +| indexd.defaultPrefix | string | `"PREFIX/"` | the default prefix for indexd records | | indexd.enabled | bool | `true` | Whether to deploy the indexd subchart. | -| indexd.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| indexd.image.repository | string | `nil` | The Docker image repository for the indexd service. | -| indexd.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| manifestservice | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for manifest service chart. | | manifestservice.enabled | bool | `true` | Whether to deploy the manifest service subchart. | -| manifestservice.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| manifestservice.image.repository | string | `nil` | The Docker image repository for the manifest service service. | -| manifestservice.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| metadata | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for metadata chart. | | metadata.enabled | bool | `true` | Whether to deploy the metadata subchart. | -| metadata.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| metadata.image.repository | string | `nil` | The Docker image repository for the metadata service. | -| metadata.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| peregrine | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for peregrine chart. | +| neuvector.DB_HOST | string | `"development-gen3-postgresql"` | | +| neuvector.ES_HOST | string | `"gen3-elasticsearch-master"` | | +| neuvector.enabled | bool | `false` | | +| neuvector.ingress.class | string | `"nginx"` | | +| neuvector.ingress.controller | string | `"nginx-ingress-controller"` | | +| neuvector.ingress.namespace | string | `"nginx"` | | +| neuvector.policies.include | bool | `false` | | +| neuvector.policies.policyMode | string | `"Monitor"` | | | peregrine.enabled | bool | `true` | Whether to deploy the peregrine subchart. | -| peregrine.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| peregrine.image.repository | string | `nil` | The Docker image repository for the peregrine service. | -| peregrine.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| pidgin | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for pidgin chart. | -| pidgin.enabled | bool | `true` | Whether to deploy the pidgin subchart. | -| pidgin.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| pidgin.image.repository | string | `nil` | The Docker image repository for the pidgin service. | -| pidgin.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| portal | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for portal chart. | +| pidgin.enabled | bool | `false` | Whether to deploy the pidgin subchart. | | portal.enabled | bool | `true` | Whether to deploy the portal subchart. | -| portal.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| portal.image.repository | string | `nil` | The Docker image repository for the portal service. | -| portal.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | To configure postgresql subchart Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | -| requestor | map | `{"enabled":false,"image":{"repository":null,"tag":null}}` | Configurations for requestor chart. | | requestor.enabled | bool | `false` | Whether to deploy the requestor subchart. | -| requestor.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| requestor.image.repository | string | `nil` | The Docker image repository for the requestor service. | -| requestor.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| revproxy | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for revproxy chart. | | revproxy.enabled | bool | `true` | Whether to deploy the revproxy subchart. | -| revproxy.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| revproxy.image.repository | string | `nil` | The Docker image repository for the revproxy service. | -| revproxy.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| secrets | map | `{"awsAccessKeyId":"test","awsSecretAccessKey":"test"}` | AWS credentials to access the db restore job S3 bucket | -| secrets.awsAccessKeyId | string | `"test"` | AWS access key. | -| secrets.awsSecretAccessKey | string | `"test"` | AWS secret access key. | -| sheepdog | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for sheepdog chart. | +| revproxy.ingress.annotations | map | `{}` | Annotations to add to the ingress. | +| revproxy.ingress.enabled | bool | `false` | Whether to create the custom revproxy ingress | +| revproxy.ingress.hosts | list | `[{"host":"chart-example.local"}]` | Where to route the traffic. | +| revproxy.ingress.tls | list | `[]` | To secure an Ingress by specifying a secret that contains a TLS private key and certificate. | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information for External Secrets and DB Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | | sheepdog.enabled | bool | `true` | Whether to deploy the sheepdog subchart. | -| sheepdog.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| sheepdog.image.repository | string | `nil` | The Docker image repository for the sheepdog service. | -| sheepdog.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| ssjdispatcher | map | `{"enabled":false,"image":{"repository":null,"tag":null}}` | Configurations for ssjdispatcher chart. | | ssjdispatcher.enabled | bool | `false` | Whether to deploy the ssjdispatcher subchart. | -| ssjdispatcher.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| ssjdispatcher.image.repository | string | `nil` | The Docker image repository for the ssjdispatcher service. | -| ssjdispatcher.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | -| tags.dev | bool | `false` | | -| wts | map | `{"enabled":true,"image":{"repository":null,"tag":null}}` | Configurations for wts chart. | | wts.enabled | bool | `true` | Whether to deploy the wts subchart. | -| wts.image | map | `{"repository":null,"tag":null}` | Docker image information. | -| wts.image.repository | string | `nil` | The Docker image repository for the wts service. | -| wts.image.tag | string | `nil` | Overrides the image tag whose default is the chart appVersion. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/gen3/ci/portal-values.yaml b/helm/gen3/ci/portal-values.yaml new file mode 100644 index 00000000..7b5bee17 --- /dev/null +++ b/helm/gen3/ci/portal-values.yaml @@ -0,0 +1,9 @@ +portal: + image: + repository: quay.io/cdis/data-portal-prebuilt + tag: "toxcommons.com-master" + + resources: + requests: + cpu: "0.2" + memory: 100Mi diff --git a/helm/gen3/templates/aws-config.yaml b/helm/gen3/templates/aws-config.yaml new file mode 100644 index 00000000..bec10059 --- /dev/null +++ b/helm/gen3/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.aws.awsSecretAccessKey (not .Values.global.aws.useLocalSecret.enabled) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/gen3/templates/aws_config.yaml b/helm/gen3/templates/aws_config.yaml deleted file mode 100644 index 3b51159c..00000000 --- a/helm/gen3/templates/aws_config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: aws-config -type: Opaque -stringData: - credentials: | - [default] - aws_access_key_id={{.Values.secrets.awsAccessKeyId}} - aws_secret_access_key={{ .Values.secrets.awsSecretAccessKey}} \ No newline at end of file diff --git a/helm/gen3/templates/cluster-secret-store.yaml b/helm/gen3/templates/cluster-secret-store.yaml new file mode 100644 index 00000000..38650a4c --- /dev/null +++ b/helm/gen3/templates/cluster-secret-store.yaml @@ -0,0 +1,27 @@ +{{- if and .Values.global.externalSecrets.deploy (not .Values.global.aws.useLocalSecret.enabled) }} +{{ include "common.secretstore" . }} +{{- else if .Values.global.aws.useLocalSecret.enabled }} +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: {{.Chart.Name}}-secret-store +spec: + provider: + aws: + service: SecretsManager + region: {{ .Values.global.aws.region }} + auth: + secretRef: + {{- if .Values.global.aws.useLocalSecret.localSecretName }} + accessKeyIDSecretRef: + name: {{ .Values.global.aws.useLocalSecret.localSecretName }} + key: access-key + secretAccessKeySecretRef: + name: {{ .Values.global.aws.useLocalSecret.localSecretName }} + key: secret-access-key + {{- else }} + jwt: + serviceAccountRef: + name: {{ .Values.global.aws.secretStoreServiceAccount.name }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/gen3/templates/global-manifest.yaml b/helm/gen3/templates/global-manifest.yaml index bdc3950e..7a1ac1d9 100644 --- a/helm/gen3/templates/global-manifest.yaml +++ b/helm/gen3/templates/global-manifest.yaml @@ -8,15 +8,17 @@ data: "revproxy_arn": {{ .Values.global.revproxyArn | quote }} "dictionary_url": {{ .Values.global.dictionaryUrl | quote }} "portal_app": {{ .Values.global.portalApp | quote }} - "kube_bucket": {{ .Values.global.kubeBucket | quote }} - "logs_bucket": {{ .Values.global.logsBucket | quote }} - "sync_from_dbgap": {{ .Values.global.syncFromDbgap | quote }} - "useryaml_s3path": {{ .Values.global.userYamlS3Path | quote }} "public_datasets": {{ .Values.global.publicDataSets | quote }} "tier_access_level": {{ .Values.global.tierAccessLevel | quote }} + "tier_access_limit": {{ .Values.global.tierAccessLimit | quote }} "netpolicy": {{ .Values.global.netPolicy | quote }} "dispatcher_job_num": {{ .Values.global.dispatcherJobNum | quote }} - "dd_enabled": {{ .Values.global.ddEnabled | quote }} + "frontend_root": {{ .Values.global.frontendRoot | quote }} {{- with .Values.global.origins_allow_credentials }} "origins_allow_credentials": {{ . | toJson | quote }} - {{- end -}} \ No newline at end of file + {{- end -}} + {{- with .Values.global.manifestGlobalExtraValues }} + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} \ No newline at end of file diff --git a/helm/gen3/templates/secret-store-service-account.yaml b/helm/gen3/templates/secret-store-service-account.yaml new file mode 100644 index 00000000..0284bac4 --- /dev/null +++ b/helm/gen3/templates/secret-store-service-account.yaml @@ -0,0 +1,29 @@ +{{- if .Values.global.aws.secretStoreServiceAccount.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.global.aws.secretStoreServiceAccount.name }} + annotations: + eks.amazonaws.com/role-arn: {{ .Values.global.aws.secretStoreServiceAccount.roleArn }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: external-secrets-role +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: external-secrets-rolebinding +subjects: +- kind: ServiceAccount + name: {{ .Values.global.aws.secretStoreServiceAccount.name }} +roleRef: + kind: Role + name: external-secrets-role + apiGroup: rbac.authorization.k8s.io +{{- end }} \ No newline at end of file diff --git a/helm/gen3/values.yaml b/helm/gen3/values.yaml index ae8164e6..05519614 100644 --- a/helm/gen3/values.yaml +++ b/helm/gen3/values.yaml @@ -2,38 +2,49 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration - gcp: true - tls: - cert: - key: aws: + # -- (string) AWS region for this deployment + region: us-east-1 # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. enabled: false - # -- (map) Credentials for AWS - account: - # Prep move of these keys here. - aws_access_key_id: - aws_secret_access_key: - # -- (bool) Whether the deployment is for development purposes. + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: + # -- (map) Service account and AWS role for authentication to AWS Secrets Manager + secretStoreServiceAccount: + # -- (bool) Set true if deploying to AWS and want to use service account and IAM role instead of aws keys. Must provide role-arn. + enabled: false + # -- (string) Name of the service account to create + name: secret-store-sa + # -- (string) AWS Role ARN for Secret Store to use + roleArn: + # -- (map) Local secret setting if using a pre-exising secret. + useLocalSecret: + # -- (bool) Set to true if you would like to use a secret that is already running on your cluster. + enabled: false + # -- (string) Name of the local secret. + localSecretName: + # -- (bool) Deploys postgres/elasticsearch for dev dev: true - # -- (map) Postgres database configuration. postgres: - # -- (bool) Whether the database should be created. + # -- (bool) Whether the database create job should run. dbCreate: true - # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: - # -- (string) hostname of postgres server - host: - # -- (string) username of superuser in postgres. This is used to create or restore databases + # -- global postgres master username username: postgres - # -- (string) password for superuser in postgres. This is used to create or restore databases + # -- global postgres master password password: - # -- (string) Port for Postgres. + # -- global postgres master host + host: + # -- global postgres master port port: "5432" - # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + # -- (string) Environment name. + # This should be the same as vpcname if you're doing an AWS deployment. + # Currently this is being used to share ALB's if you have multiple namespaces in same cluster. environment: default # -- (string) Hostname for the deployment. hostname: localhost @@ -43,241 +54,229 @@ global: dictionaryUrl: https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json # -- (string) Portal application name. portalApp: gitops - # -- (string) S3 bucket name for Kubernetes manifest files. - kubeBucket: kube-gen3 - # -- (string) S3 bucket name for log files. - logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true - # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false - + # -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. + tierAccessLimit: "1000" + # -- (bool) Global flags to control and manage network policies for a Gen3 installation + # NOTE: Network policies are currently a beta feature. Use with caution! + netPolicy: + # -- (bool) Whether network policies are enabled + enabled: false -tags: - # controls whether or not to deploy postgres/ elasticsearch as local services. - dev: false + # -- (array) A CIDR range representing a database subnet, that services with a database need access to + dbSubnet: "" + # -- (int) Number of dispatcher jobs. + dispatcherJobNum: "10" + # -- (map) If you would like to add any extra values to the manifest-global configmap. + manifestGlobalExtraValues: {} + # -- (string) Which app will be served on /. Needs be set to portal for portal, or "gen3ff" for frontendframework. + frontendRoot: "portal" + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override secrets you have deployed. + deploy: false + # -- (bool) Will create the databases and store the creds in Kubernetes Secrets even if externalSecrets is deployed. Useful if you want to use ExternalSecrets for other secrets besides db secrets. + dbCreate: false # Dependancy Charts -# -- (map) Configurations for ambassador chart. ambassador: # -- (bool) Whether to deploy the ambassador subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the ambassador service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for arborist chart. arborist: # -- (bool) Whether to deploy the arborist subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the arborist service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for argo-wrapper chart. argo-wrapper: # -- (bool) Whether to deploy the argo-wrapper subchart. - enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the argo-wrapper service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: + enabled: false -# -- (map) Configurations for audit chart. audit: # -- (bool) Whether to deploy the audit subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the audit service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for aws-es-proxy chart. aws-es-proxy: # -- (bool) Whether to deploy the aws-es-proxy subchart. enabled: false + # -- (str) Elasticsearch endpoint in AWS + esEndpoint: test.us-east-1.es.amazonaws.com + # -- (map) Secret information + secrets: + # -- (str) AWS access key ID for aws-es-proxy + awsAccessKeyId: "" + # -- (str) AWS secret access key for aws-es-proxy + awsSecretAccessKey: "" + +etl: + # -- (bool) Whether to deploy the etl subchart. + enabled: true -# -- (map) Configurations for fence chart. fence: # -- (bool) Whether to deploy the fence subchart. enabled: true + # -- (map) Configuration options for usersync cronjob. + usersync: + # -- (bool) Whether to run Fence usersync or not. + usersync: false + # -- (string) The cron schedule expression to use in the usersync cronjob. Runs every 30 minutes by default. + schedule: "*/30 * * * *" + # -- (bool) Whether to sync data from dbGaP. + syncFromDbgap: false + # -- (bool) Force attempting a dbgap sync if "true", falls back on user.yaml + addDbgap: false + # -- (bool) Forces ONLY a dbgap sync if "true", IGNORING user.yaml + onlyDbgap: false + # -- (string) Path to the user.yaml file in S3. + userYamlS3Path: s3://cdis-gen3-users/helm-test/user.yaml + # -- (string) Slack webhook endpoint used with certain jobs. + slack_webhook: None + # -- (bool) Will echo what files we are seeing on dbgap ftp to Slack. + slack_send_dbgap: false + +# -- (map) Configurations for frontend-framework chart. +frontend-framework: + # -- (bool) Whether to deploy the frontend-framework subchart. + enabled: false # -- (map) Docker image information. image: - # -- (string) The Docker image repository for the fence service. - repository: + # -- (string) The Docker image repository for the frontend-framework. + repository: "quay.io/cdis/frontend-framework" # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: + tag: "develop" # -- (map) Configurations for guppy chart. guppy: # -- (bool) Whether to deploy the guppy subchart. enabled: false - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the guppy service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for hatchery chart. hatchery: # -- (bool) Whether to deploy the hatchery subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the hatchery service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for indexd chart. + hatchery: + sidecarContainer: + # -- (string) The maximum amount of CPU the sidecar container can use + cpu-limit: "0.1" + # -- (string) The maximum amount of memory the sidecar container can use + memory-limit: 256Mi + # -- (string) The sidecar image. + image: quay.io/cdis/ecs-ws-sidecar:master + # -- (map) Environment variables to pass to the sidecar container + env: + NAMESPACE: "{{ .Release.Namespace }}" + HOSTNAME: "{{ .Values.global.hostname }}" + # -- (list) Arguments to pass to the sidecare container. + args: [] + # -- (list) Commands to run for the sidecar container. + command: + - "/bin/bash" + - "./sidecar.sh" + lifecycle-pre-stop: + - su + - "-c" + - echo test + - "-s" + - "/bin/sh" + - root + containers: + - # -- (int) port to proxy traffic to in docker contaniner + target-port: 8888 + # -- (string) cpu limit of workspace container + cpu-limit: "1.0" + # -- (string) memory limit of workspace container + memory-limit: 2Gi + # -- (string) name of workspace + name: "(Tutorials) Example Analysis Jupyter Lab Notebooks" + # -- (string) docker image for workspace + image: quay.io/cdis/heal-notebooks:combined_tutorials__latest + # -- environment variables for workspace container + env: + FRAME_ANCESTORS: https://{{ .Values.global.hostname }} + args: + - "--NotebookApp.base_url=/lw-workspace/proxy/" + - "--NotebookApp.default_url=/lab" + - "--NotebookApp.password=''" + - "--NotebookApp.token=''" + - "--NotebookApp.shutdown_no_activity_timeout=5400" + - "--NotebookApp.quit_button=False" + command: + - start-notebook.sh + path-rewrite: "/lw-workspace/proxy/" + use-tls: "false" + ready-probe: "/lw-workspace/proxy/" + lifecycle-post-start: + - "/bin/sh" + - "-c" + - export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; + ln -s /data /home/$IAM/pd/; true + user-uid: 1000 + fs-gid: 100 + user-volume-location: "/home/jovyan/pd" + gen3-volume-location: "/home/jovyan/.gen3" + indexd: # -- (bool) Whether to deploy the indexd subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the indexd service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: + # -- (string) the default prefix for indexd records + defaultPrefix: "PREFIX/" -# -- (map) Configurations for manifest service chart. manifestservice: # -- (bool) Whether to deploy the manifest service subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the manifest service service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for metadata chart. metadata: # -- (bool) Whether to deploy the metadata subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the metadata service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for peregrine chart. peregrine: # -- (bool) Whether to deploy the peregrine subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the peregrine service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for pidgin chart. pidgin: # -- (bool) Whether to deploy the pidgin subchart. - enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the pidgin service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: + enabled: false -# -- (map) Configurations for portal chart. portal: # -- (bool) Whether to deploy the portal subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the portal service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for requestor chart. requestor: # -- (bool) Whether to deploy the requestor subchart. enabled: false - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the requestor service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for revproxy chart. revproxy: # -- (bool) Whether to deploy the revproxy subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the revproxy service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for sheepdog chart. + ingress: + # -- (bool) Whether to create the custom revproxy ingress + enabled: false + # -- (map) Annotations to add to the ingress. + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # -- (list) Where to route the traffic. + hosts: + - host: chart-example.local + # -- (list) To secure an Ingress by specifying a secret that contains a TLS private key and certificate. + tls: [] + sheepdog: # -- (bool) Whether to deploy the sheepdog subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the sheepdog service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for ssjdispatcher chart. ssjdispatcher: # -- (bool) Whether to deploy the ssjdispatcher subchart. enabled: false - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the ssjdispatcher service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: -# -- (map) Configurations for wts chart. wts: # -- (bool) Whether to deploy the wts subchart. enabled: true - # -- (map) Docker image information. - image: - # -- (string) The Docker image repository for the wts service. - repository: - # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: - -# -- (map) AWS credentials to access the db restore job S3 bucket -secrets: - # -- (string) AWS access key. - awsAccessKeyId: test - # -- (string) AWS secret access key. - awsSecretAccessKey: test # -- (map) To configure postgresql subchart # Disable persistence by default so we can spin up and down ephemeral environments @@ -286,3 +285,46 @@ postgresql: persistence: # -- (bool) Option to persist the dbs data. enabled: false + +elasticsearch: + clusterName: gen3-elasticsearch + maxUnavailable: 0 + singleNode: true + replicas: 1 + clusterHealthCheckParams: "wait_for_status=yellow&timeout=1s" + esConfig: + elasticsearch.yml: | + # Here we can add elasticsearch config + +# (optional) NeuVector Kubernetes Security Policy templates to protect Gen3 +# NeuVector must be installed separately. +# Reference: https://open-docs.neuvector.com/basics/overview +# Reference: https://github.com/neuvector/neuvector-helm +# For more information, please use the Gen3 community Slack. +neuvector: + # install Neuvector + enabled: false + policies: + # deploy predefined Neuvector policies for Gen3 + include: false + # Discover, Monitor, or Protect + policyMode: Monitor + # Configure your ingress controller information for enabling ingress to containers + ingress: + # service name of your ingress controller + controller: nginx-ingress-controller + # installation namespace of your ingress controller + namespace: nginx + # classname of your ingress + class: nginx + # Required to allow egress to in-cluster database or external, managed database + DB_HOST: development-gen3-postgresql + # hostname/service name for our ElasitcSearch instance, used to allow egress from containers + ES_HOST: gen3-elasticsearch-master + +# -- (map) Secret information for External Secrets and DB Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: diff --git a/helm/guppy/Chart.yaml b/helm/guppy/Chart.yaml index 10954c1c..ff0c3447 100644 --- a/helm/guppy/Chart.yaml +++ b/helm/guppy/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.4 +version: 0.1.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -24,6 +24,6 @@ version: 0.1.4 appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/guppy/README.md b/helm/guppy/README.md index 80a58e00..2e94cdf8 100644 --- a/helm/guppy/README.md +++ b/helm/guppy/README.md @@ -1,6 +1,6 @@ # guppy -![Version: 0.1.4](https://img.shields.io/badge/Version-0.1.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.16](https://img.shields.io/badge/Version-0.1.16-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Guppy Service @@ -8,7 +8,7 @@ A Helm chart for gen3 Guppy Service | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | ## Values @@ -29,29 +29,31 @@ A Helm chart for gen3 Guppy Service | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | | configIndex | string | `"dev_case-array-config"` | The Elasticsearch configuration index | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | dataDog | bool | `{"enabled":false,"env":"dev"}` | Whether Datadog is enabled. | -| dbRestore | bool | `true` | Whether or not to restore elasticsearch indices from a snapshot in s3 | +| dbRestore | bool | `false` | Whether or not to restore elasticsearch indices from a snapshot in s3 | | enableEncryptWhitelist | bool | `true` | Whether or not to enable encryption for specified fields | | encryptWhitelist | string | `"test1"` | A comma-separated list of fields to encrypt | -| esEndpoint | string | `""` | Elasticsearch endpoint. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | +| esEndpoint | string | `"gen3-elasticsearch-master:9200"` | Elasticsearch endpoint. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -59,14 +61,16 @@ A Helm chart for gen3 Guppy Service | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | +| global.tierAccessLimit | int | `"1000"` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/guppy","tag":""}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/guppy"` | Docker repository. | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | | indices | list | `[{"index":"dev_case","type":"case"},{"index":"dev_file","type":"file"}]` | Elasticsearch index configurations | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| partOf | string | `"Explorer-Tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"2Gi"},"requests":{"cpu":0.1,"memory":"500Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"2Gi"}` | The maximum amount of resources that the container is allowed to use | @@ -76,19 +80,15 @@ A Helm chart for gen3 Guppy Service | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"500Mi"` | The amount of memory requested | | revisionHistoryLimit | int | `2` | Number of old revisions to retain | -| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | AWS credentials to access the db restore job S3 bucket | -| secrets.awsAccessKeyId | string | `nil` | AWS access key. | -| secrets.awsSecretAccessKey | string | `nil` | AWS secret access key. | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information to access the db restore job S3 bucket. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":[{"name":"http","port":80,"protocol":"TCP","targetPort":8000}],"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `[{"name":"http","port":80,"protocol":"TCP","targetPort":8000}]` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | | strategy | map | `{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"}` | Rolling update deployment strategy | | strategy.rollingUpdate.maxSurge | int | `1` | Number of additional replicas to add during rollout. | | strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | -| tierAccessLevel | string | `"regular"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| tierAccessLimit | int | `1000` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | | volumeMounts | list | `[{"mountPath":"/guppy/guppy_config.json","name":"guppy-config","readOnly":true,"subPath":"guppy_config.json"}]` | Volumes to mount to the container. | | volumes | list | `[{"configMap":{"items":[{"key":"guppy_config.json","path":"guppy_config.json"}],"name":"manifest-guppy"},"name":"guppy-config"}]` | Volumes to attach to the pod. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/guppy/templates/_helpers.tpl b/helm/guppy/templates/_helpers.tpl index 82776b04..751dc6a7 100644 --- a/helm/guppy/templates/_helpers.tpl +++ b/helm/guppy/templates/_helpers.tpl @@ -34,21 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "guppy.labels" -}} -helm.sh/chart: {{ include "guppy.chart" . }} -{{ include "guppy.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "guppy.selectorLabels" -}} -app.kubernetes.io/name: {{ include "guppy.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ include "guppy.name" . }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -61,25 +66,3 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} - -{{/* -Define tierAccessLevel -*/}} -{{- define "guppy.tierAccessLevel" -}} -{{- if .Values.global }} -{{- .Values.global.tierAccessLevel }} -{{- else}} -{{- .Values.tierAccessLevel }} -{{- end }} -{{- end }} - -{{/* -Define ddEnabled -*/}} -{{- define "guppy.ddEnabled" -}} -{{- if .Values.global }} -{{- .Values.global.ddEnabled }} -{{- else}} -{{- .Values.dataDog.enabled }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/guppy/templates/aws-config.yaml b/helm/guppy/templates/aws-config.yaml new file mode 100644 index 00000000..4723e6b3 --- /dev/null +++ b/helm/guppy/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end }} \ No newline at end of file diff --git a/helm/guppy/templates/aws-creds.yaml b/helm/guppy/templates/aws-creds.yaml deleted file mode 100644 index 77f70daf..00000000 --- a/helm/guppy/templates/aws-creds.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} -apiVersion: v1 -kind: Secret -metadata: - name: aws-config-guppy -type: Opaque -stringData: - credentials: | - [default] - aws_access_key_id={{ .Values.secrets.awsAccessKeyId | default .Values.global.aws.awsAccessKeyId }} - aws_secret_access_key={{ .Values.secrets.awsSecretAccessKey | default .Values.global.aws.awsSecretAccessKey }} -{{- end }} - diff --git a/helm/guppy/templates/deployment.yaml b/helm/guppy/templates/deployment.yaml index 32ee6abf..f23baf53 100644 --- a/helm/guppy/templates/deployment.yaml +++ b/helm/guppy/templates/deployment.yaml @@ -2,6 +2,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: guppy-deployment + labels: + {{- include "guppy.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} @@ -17,16 +19,16 @@ spec: template: metadata: labels: + {{- include "guppy.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} # gen3 networkpolicy labels netnolimit: 'yes' public: 'yes' - {{- if eq (include "guppy.ddEnabled" . ) "true" }} - tags.datadoghq.com/service: "guppy" - # TODO: move this to helpers so we can have this populated from a configmap - tags.datadoghq.com/env: {{ .Values.dataDog.env }} - tags.datadoghq.com/version: {{ .Values.image.tag | default .Chart.AppVersion }} - {{- end }} - {{- include "guppy.selectorLabels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/guppy_config.yaml") . | sha256sum }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: {{- with .Values.affinity }} affinity: @@ -60,19 +62,15 @@ spec: - name: GUPPY_CONFIG_FILEPATH value: /guppy/guppy_config.json - name: GEN3_ES_ENDPOINT - value: {{ default "elasticsearch:9200" .Values.esEndpoint }} + value: {{ default "gen3-elasticsearch-master:9200" .Values.esEndpoint }} {{- with .Values.arboristUrl }} - name: GEN3_ARBORIST_ENDPOINT value: {{ . }} {{- end }} - name: TIER_ACCESS_LEVEL - value: {{ include "guppy.tierAccessLevel" . }} - {{- with .Values.tierAccessLimit }} + value: {{ .Values.global.tierAccessLevel | quote }} - name: TIER_ACCESS_LIMIT - value: {{ . | quote }} - {{- end }} - - + value: {{ .Values.global.tierAccessLimit | quote }} {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 10 }} diff --git a/helm/guppy/templates/guppy_config.yaml b/helm/guppy/templates/guppy_config.yaml index ff7ab9be..ad8bcd40 100644 --- a/helm/guppy/templates/guppy_config.yaml +++ b/helm/guppy/templates/guppy_config.yaml @@ -6,9 +6,7 @@ data: guppy_config.json: | { "indices": {{ .Values.indices | toJson }}, - {{- with .Values.configIndex }} - "config_index": {{ . | quote }}, - {{- end }} + "config_index": {{ .Values.configIndex | toJson }}, "auth_filter_field": {{ .Values.authFilterField | quote }}, "enable_encrypt_whitelist": {{ .Values.enableEncryptWhitelist | quote }}, "encrypt_whitelist": {{ .Values.encryptWhitelist | quote }} diff --git a/helm/guppy/templates/pdb.yaml b/helm/guppy/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/guppy/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/guppy/values.yaml b/helm/guppy/values.yaml index 2d9cc8bf..55f3166c 100644 --- a/helm/guppy/values.yaml +++ b/helm/guppy/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -42,21 +44,24 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. + tierAccessLimit: "1000" + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false # -- (map) Configuration for autoscaling the number of replicas autoscaling: @@ -69,11 +74,11 @@ autoscaling: # -- (int) The target CPU utilization percentage for autoscaling targetCPUUtilizationPercentage: 80 -# -- (map) AWS credentials to access the db restore job S3 bucket +# -- (map) Secret information to access the db restore job S3 bucket. secrets: - # -- (string) AWS access key. + # -- (str) AWS access key ID. Overrides global key. awsAccessKeyId: - # -- (string) AWS secret access key. + # -- (str) AWS secret access key ID. Overrides global key. awsSecretAccessKey: # -- (int) Number of replicas for the deployment. @@ -101,20 +106,20 @@ affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - guppy - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - guppy + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (list) Volumes to attach to the pod. volumes: @@ -137,16 +142,11 @@ image: # -- (string) Overrides the image tag whose default is the chart appVersion. tag: "" - # Environment Variables # -- (string) Elasticsearch endpoint. -esEndpoint: "" +esEndpoint: "gen3-elasticsearch-master:9200" # -- (string) Arborist service URL. arboristUrl: http://arborist-service -# -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` -tierAccessLevel: regular -# -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. -tierAccessLimit: 1000 # -- (list) Volumes to mount to the container. volumeMounts: @@ -170,7 +170,6 @@ resources: # -- (string) The maximum amount of memory the container can use memory: 2Gi - # -- (map) Kubernetes service information. service: # -- (string) Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". @@ -185,10 +184,10 @@ service: # Configmap # -- (list) Elasticsearch index configurations indices: -- index: dev_case - type: case -- index: dev_file - type: file + - index: dev_case + type: case + - index: dev_file + type: file # -- (string) The Elasticsearch configuration index configIndex: dev_case-array-config # -- (string) The field used for access control and authorization filters @@ -199,4 +198,16 @@ enableEncryptWhitelist: true encryptWhitelist: test1 # -- (bool) Whether or not to restore elasticsearch indices from a snapshot in s3 -dbRestore: true +dbRestore: false + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Explorer-Tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/hatchery/Chart.yaml b/helm/hatchery/Chart.yaml index e8d1031a..f222c6b2 100644 --- a/helm/hatchery/Chart.yaml +++ b/helm/hatchery/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.3 +version: 0.1.12 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/hatchery/README.md b/helm/hatchery/README.md index fa3b7dff..1c429f03 100644 --- a/helm/hatchery/README.md +++ b/helm/hatchery/README.md @@ -1,9 +1,15 @@ # hatchery -![Version: 0.1.3](https://img.shields.io/badge/Version-0.1.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.12](https://img.shields.io/badge/Version-0.1.12-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Hatchery +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -14,25 +20,27 @@ A Helm chart for gen3 Hatchery | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | env | list | `[{"name":"HTTP_PORT","value":"8000"},{"name":"POD_NAMESPACE","valueFrom":{"fieldRef":{"fieldPath":"metadata.namespace"}}}]` | Environment variables to pass to the container | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -40,10 +48,9 @@ A Helm chart for gen3 Hatchery | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | -| hatchery | map | `{"containers":[{"args":["--NotebookApp.base_url=/lw-workspace/proxy/","--NotebookApp.default_url=/lab","--NotebookApp.password=''","--NotebookApp.token=''","--NotebookApp.shutdown_no_activity_timeout=5400","--NotebookApp.quit_button=False"],"command":["start-notebook.sh"],"cpu-limit":"1.0","env":{"FRAME_ANCESTORS":"https://{{ .Values.global.hostname }}"},"fs-gid":100,"gen3-volume-location":"/home/jovyan/.gen3","image":"quay.io/cdis/heal-notebooks:combined_tutorials__latest","lifecycle-post-start":["/bin/sh","-c","export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; ln -s /data /home/$IAM/pd/; true"],"memory-limit":"2Gi","name":"(Tutorials) Example Analysis Jupyter Lab Notebooks","path-rewrite":"/lw-workspace/proxy/","ready-probe":"/lw-workspace/proxy/","target-port":8888,"use-tls":"false","user-uid":1000,"user-volume-location":"/home/jovyan/pd"}],"sidecarContainer":{"args":[],"command":["/bin/bash","./sidecar.sh"],"cpu-limit":"0.1","env":{"HOSTNAME":"{{ .Values.global.hostname }}","NAMESPACE":"{{ .Release.Namespace }}"},"image":"quay.io/cdis/ecs-ws-sidecar:master","lifecycle-pre-stop":["su","-c","echo test","-s","/bin/sh","root"],"memory-limit":"256Mi"}}` | Hatchery sidcar container configuration. | +| hatchery | map | `{"containers":[{"args":["--NotebookApp.base_url=/lw-workspace/proxy/","--NotebookApp.default_url=/lab","--NotebookApp.password=''","--NotebookApp.token=''","--NotebookApp.shutdown_no_activity_timeout=5400","--NotebookApp.quit_button=False"],"command":["start-notebook.sh"],"cpu-limit":"1.0","env":{"FRAME_ANCESTORS":"https://{{ .Values.global.hostname }}"},"fs-gid":100,"gen3-volume-location":"/home/jovyan/.gen3","image":"quay.io/cdis/heal-notebooks:combined_tutorials__latest","lifecycle-post-start":["/bin/sh","-c","export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; ln -s /data /home/$IAM/pd/; true"],"memory-limit":"2Gi","name":"(Tutorials) Example Analysis Jupyter Lab Notebooks","path-rewrite":"/lw-workspace/proxy/","ready-probe":"/lw-workspace/proxy/","target-port":8888,"use-tls":"false","user-uid":1000,"user-volume-location":"/home/jovyan/pd"}],"sidecarContainer":{"args":[],"command":["/bin/bash","./sidecar.sh"],"cpu-limit":"0.1","env":{"HOSTNAME":"{{ .Values.global.hostname }}","NAMESPACE":"{{ .Release.Namespace }}"},"image":"quay.io/cdis/ecs-ws-sidecar:master","lifecycle-pre-stop":["su","-c","echo test","-s","/bin/sh","root"],"memory-limit":"256Mi"},"skipNodeSelector":false,"useInternalServicesUrl":false}` | Hatchery sidcar container configuration. | +| hatchery.containers | list | `[{"args":["--NotebookApp.base_url=/lw-workspace/proxy/","--NotebookApp.default_url=/lab","--NotebookApp.password=''","--NotebookApp.token=''","--NotebookApp.shutdown_no_activity_timeout=5400","--NotebookApp.quit_button=False"],"command":["start-notebook.sh"],"cpu-limit":"1.0","env":{"FRAME_ANCESTORS":"https://{{ .Values.global.hostname }}"},"fs-gid":100,"gen3-volume-location":"/home/jovyan/.gen3","image":"quay.io/cdis/heal-notebooks:combined_tutorials__latest","lifecycle-post-start":["/bin/sh","-c","export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; ln -s /data /home/$IAM/pd/; true"],"memory-limit":"2Gi","name":"(Tutorials) Example Analysis Jupyter Lab Notebooks","path-rewrite":"/lw-workspace/proxy/","ready-probe":"/lw-workspace/proxy/","target-port":8888,"use-tls":"false","user-uid":1000,"user-volume-location":"/home/jovyan/pd"}]` | Notebook configuration. | | hatchery.sidecarContainer.args | list | `[]` | Arguments to pass to the sidecare container. | | hatchery.sidecarContainer.command | list | `["/bin/bash","./sidecar.sh"]` | Commands to run for the sidecar container. | | hatchery.sidecarContainer.cpu-limit | string | `"0.1"` | The maximum amount of CPU the sidecar container can use | @@ -51,13 +58,18 @@ A Helm chart for gen3 Hatchery | hatchery.sidecarContainer.image | string | `"quay.io/cdis/ecs-ws-sidecar:master"` | The sidecar image. | | hatchery.sidecarContainer.lifecycle-pre-stop | list | `["su","-c","echo test","-s","/bin/sh","root"]` | Commands that are run before the container is stopped. | | hatchery.sidecarContainer.memory-limit | string | `"256Mi"` | The maximum amount of memory the sidecar container can use | +| hatchery.skipNodeSelector | bool | `false` | Whether to skip node selector for . Defaults to `global.dev`. | +| hatchery.useInternalServicesUrl | bool | `false` | Whether to use internal services url. Defaults to `global.dev`. | | image | map | `{"pullPolicy":"IfNotPresent","repository":"quay.io/cdis/hatchery","tag":""}` | Docker image information. | | image.pullPolicy | string | `"IfNotPresent"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/hatchery"` | Docker repository. | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | | nodeSelector | map | `{}` | Node selector labels. | +| partOf | string | `"Workspace-Tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -66,12 +78,10 @@ A Helm chart for gen3 Hatchery | resources.requests | map | `{"cpu":0.1,"memory":"12Mi"}` | The amount of resources that the container requests | | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | | tolerations | list | `[]` | Tolerations to use for the deployment. | | volumeMounts | list | `[{"mountPath":"/hatchery.json","name":"hatchery-config","readOnly":true,"subPath":"json"}]` | Volumes to mount to the container. | | volumes | list | `[{"configMap":{"name":"manifest-hatchery"},"name":"hatchery-config"}]` | Volumes to attach to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/hatchery/templates/_helpers.tpl b/helm/hatchery/templates/_helpers.tpl index 7ea986c8..03655ba2 100644 --- a/helm/hatchery/templates/_helpers.tpl +++ b/helm/hatchery/templates/_helpers.tpl @@ -34,20 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "hatchery.labels" -}} -helm.sh/chart: {{ include "hatchery.chart" . }} -{{ include "hatchery.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "hatchery.selectorLabels" -}} -app.kubernetes.io/name: {{ include "hatchery.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* diff --git a/helm/hatchery/templates/deployment.yaml b/helm/hatchery/templates/deployment.yaml index 40d02752..04c7ca21 100644 --- a/helm/hatchery/templates/deployment.yaml +++ b/helm/hatchery/templates/deployment.yaml @@ -13,12 +13,20 @@ spec: {{- include "hatchery.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print $.Template.BasePath "/hatchery-manifest.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: + netnolimit: "yes" + public: "yes" + userhelper: "yes" {{- include "hatchery.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: diff --git a/helm/hatchery/templates/hatchery-manifest.yaml b/helm/hatchery/templates/hatchery-manifest.yaml index 777e6922..3d643a84 100644 --- a/helm/hatchery/templates/hatchery-manifest.yaml +++ b/helm/hatchery/templates/hatchery-manifest.yaml @@ -6,7 +6,8 @@ data: json: | { "user-namespace": "jupyter-pods-{{ .Release.Name }}", - "localdev": {{ .Values.global.dev }}, + "skip-node-selector": {{ or .Values.global.dev .Values.hatchery.skipNodeSelector }}, + "use-internal-services-url": {{ or .Values.global.dev .Values.hatchery.useInternalServicesUrl }}, "sub-dir": "/lw-workspace", "user-volume-size": "10Gi", "sidecar": {{ tpl (.Values.hatchery.sidecarContainer | toJson) . }}, diff --git a/helm/hatchery/templates/pdb.yaml b/helm/hatchery/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/hatchery/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/hatchery/templates/tests/test-connection.yaml b/helm/hatchery/templates/tests/test-connection.yaml index 606b6519..141c6c25 100644 --- a/helm/hatchery/templates/tests/test-connection.yaml +++ b/helm/hatchery/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "hatchery.fullname" . }}-test-connection" + name: "hatchery-test-connection" labels: {{- include "hatchery.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "hatchery.fullname" . }}:{{ .Values.service.port }}'] + args: ['hatchery-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/hatchery/values.yaml b/helm/hatchery/values.yaml index b1712608..db9d8241 100644 --- a/helm/hatchery/values.yaml +++ b/helm/hatchery/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -42,20 +44,22 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false # -- (int) Number of replicas for the deployment. replicaCount: 1 @@ -123,31 +127,31 @@ affinity: {} # -- (list) Environment variables to pass to the container env: -- name: HTTP_PORT - value: "8000" -- name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace + - name: HTTP_PORT + value: "8000" + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace # -- (list) Volumes to attach to the container. volumes: -- name: hatchery-config - configMap: - name: manifest-hatchery + - name: hatchery-config + configMap: + name: manifest-hatchery # -- (list) Volumes to mount to the container. volumeMounts: -- name: hatchery-config - readOnly: true - mountPath: /hatchery.json - subPath: json + - name: hatchery-config + readOnly: true + mountPath: /hatchery.json + subPath: json # -- (map) Hatchery sidcar container configuration. hatchery: sidecarContainer: # -- (string) The maximum amount of CPU the sidecar container can use - cpu-limit: '0.1' + cpu-limit: "0.1" # -- (string) The maximum amount of memory the sidecar container can use memory-limit: 256Mi # -- (string) The sidecar image. @@ -160,44 +164,61 @@ hatchery: args: [] # -- (list) Commands to run for the sidecar container. command: - - "/bin/bash" - - "./sidecar.sh" + - "/bin/bash" + - "./sidecar.sh" # -- (list) Commands that are run before the container is stopped. lifecycle-pre-stop: - - su - - "-c" - - echo test - - "-s" - - "/bin/sh" - - root - -# -- (list) Notebook configuration. + - su + - "-c" + - echo test + - "-s" + - "/bin/sh" + - root + + # -- (list) Notebook configuration. containers: - target-port: 8888 - cpu-limit: '1.0' + cpu-limit: "1.0" memory-limit: 2Gi name: "(Tutorials) Example Analysis Jupyter Lab Notebooks" image: quay.io/cdis/heal-notebooks:combined_tutorials__latest env: FRAME_ANCESTORS: https://{{ .Values.global.hostname }} args: - - "--NotebookApp.base_url=/lw-workspace/proxy/" - - "--NotebookApp.default_url=/lab" - - "--NotebookApp.password=''" - - "--NotebookApp.token=''" - - "--NotebookApp.shutdown_no_activity_timeout=5400" - - "--NotebookApp.quit_button=False" + - "--NotebookApp.base_url=/lw-workspace/proxy/" + - "--NotebookApp.default_url=/lab" + - "--NotebookApp.password=''" + - "--NotebookApp.token=''" + - "--NotebookApp.shutdown_no_activity_timeout=5400" + - "--NotebookApp.quit_button=False" command: - - start-notebook.sh + - start-notebook.sh path-rewrite: "/lw-workspace/proxy/" - use-tls: 'false' + use-tls: "false" ready-probe: "/lw-workspace/proxy/" lifecycle-post-start: - - "/bin/sh" - - "-c" - - export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; - ln -s /data /home/$IAM/pd/; true + - "/bin/sh" + - "-c" + - export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; + ln -s /data /home/$IAM/pd/; true user-uid: 1000 fs-gid: 100 user-volume-location: "/home/jovyan/pd" gen3-volume-location: "/home/jovyan/.gen3" + + # -- (bool) Whether to skip node selector for . Defaults to `global.dev`. + skipNodeSelector: false + # -- (bool) Whether to use internal services url. Defaults to `global.dev`. + useInternalServicesUrl: false + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Workspace-Tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/indexd/Chart.yaml b/helm/indexd/Chart.yaml index 8f254b7c..99ac703b 100644 --- a/helm/indexd/Chart.yaml +++ b/helm/indexd/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 +version: 0.1.18 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,12 +23,11 @@ version: 0.1.5 # It is recommended to use it with quotes. appVersion: "master" - dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/indexd/README.md b/helm/indexd/README.md index 77e31e61..6bc891a9 100644 --- a/helm/indexd/README.md +++ b/helm/indexd/README.md @@ -1,6 +1,6 @@ # indexd -![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.18](https://img.shields.io/badge/Version-0.1.18-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 indexd @@ -8,7 +8,7 @@ A Helm chart for gen3 indexd | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values @@ -20,25 +20,34 @@ A Helm chart for gen3 indexd | autoscaling.maxReplicas | int | `100` | Maximum number of replicas | | autoscaling.minReplicas | int | `1` | Minimum number of replicas | | autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage | -| env | list | `[{"name":"GEN3_DEBUG","value":"False"}]` | Environment variables to pass to the container | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| defaultPrefix | string | `"PREFIX/"` | default prefix for indexd | +| env | list | `[{"name":"ARBORIST","value":"true"},{"name":"GEN3_DEBUG","value":"False"}]` | Environment variables to pass to the container | +| externalSecrets | map | `{"createK8sServiceCredsSecret":false,"dbcreds":null,"serviceCreds":"indexd-service-creds"}` | External Secrets settings. | +| externalSecrets.createK8sServiceCredsSecret | string | `false` | Will create the Helm "indexd-service-creds" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any indexd secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -46,16 +55,20 @@ A Helm chart for gen3 indexd | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | +| global.tierAccessLimit | int | `"1000"` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | | image | map | `{"pullPolicy":"IfNotPresent","repository":"quay.io/cdis/indexd","tag":""}` | Docker image information. | | image.pullPolicy | string | `"IfNotPresent"` | When to pull the image. | | image.repository | string | `"quay.io/cdis/indexd"` | The Docker image repository for the indexd service | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | +| netPolicy | map | `{"egressApps":["fence","presigned-url-fence","fenceshib","peregrine","sheepdog","ssjdispatcherjob","metadata","mariner","mariner-engine"],"ingressApps":["fence","presigned-url-fence","fenceshib","peregrine","sheepdog","ssjdispatcherjob","metadata","mariner","mariner-engine"]}` | Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true | +| netPolicy.egressApps | array | `["fence","presigned-url-fence","fenceshib","peregrine","sheepdog","ssjdispatcherjob","metadata","mariner","mariner-engine"]` | List of apps that this app requires egress to | +| netPolicy.ingressApps | array | `["fence","presigned-url-fence","fenceshib","peregrine","sheepdog","ssjdispatcherjob","metadata","mariner","mariner-engine"]` | List of app labels that require ingress to this service | | nodeSelector | map | `{}` | Node Selector for the pods | +| partOf | string | `"S3-GS"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod | | podSecurityContext | map | `{}` | Security context for the pod | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | @@ -68,6 +81,7 @@ A Helm chart for gen3 indexd | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of desired replicas | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -76,8 +90,11 @@ A Helm chart for gen3 indexd | resources.requests | map | `{"cpu":0.1,"memory":"12Mi"}` | The amount of resources that the container requests | | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | -| secrets | map | `{"userdb":{"fence":"test","gateway":null,"gdcapi":null}}` | Values for indexd secret. | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"userdb":{"fence":null,"sheepdog":null}}` | Values for indexd secret. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID to access the db restore job S3 bucket. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID to access the db restore job S3 bucket. Overrides global key. | | securityContext | map | `{}` | Security context for the containers in the pod | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -86,8 +103,6 @@ A Helm chart for gen3 indexd | serviceAccount.create | bool | `false` | Specifies whether a service account should be created. | | serviceAccount.name | string | `""` | The name of the service account | | tolerations | list | `[]` | Tolerations for the pods | +| uwsgi | map | `{"listen":1024}` | Values for overriding uwsgi settings | | volumeMounts | list | `[{"mountPath":"/var/www/indexd/local_settings.py","name":"config-volume","readOnly":true,"subPath":"local_settings.py"}]` | Volumes to mount to the container. | -| volumes | list | `[{"name":"config-volume","secret":{"secretName":"indexd-settings"}},{"name":"creds-volume","secret":{"secretName":"indexd-creds"}}]` | Volumes to attach to the pod | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) +| volumes | list | `[{"configMap":{"name":"indexd-uwsgi"},"name":"uwsgi-config"},{"name":"config-volume","secret":{"secretName":"indexd-settings"}}]` | Volumes to attach to the pod | diff --git a/helm/indexd/indexd-settings/local_settings.py b/helm/indexd/indexd-settings/local_settings.py index cc30e482..42d74247 100644 --- a/helm/indexd/indexd-settings/local_settings.py +++ b/helm/indexd/indexd-settings/local_settings.py @@ -17,7 +17,7 @@ # TODO: FIX THIS TO READ FROM ENV VARS index_config = { - "DEFAULT_PREFIX": environ.get("DEFAULT_PREFIX", "testprefix"), + "DEFAULT_PREFIX": environ.get("DEFAULT_PREFIX", "testprefix/"), "PREPEND_PREFIX": environ.get("PREPEND_PREFIX", True), } diff --git a/helm/indexd/templates/_helpers.tpl b/helm/indexd/templates/_helpers.tpl index dcb848be..5778d510 100644 --- a/helm/indexd/templates/_helpers.tpl +++ b/helm/indexd/templates/_helpers.tpl @@ -34,20 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "indexd.labels" -}} -helm.sh/chart: {{ include "indexd.chart" . }} -{{ include "indexd.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "indexd.selectorLabels" -}} -app.kubernetes.io/name: {{ include "indexd.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -75,9 +81,6 @@ Create the name of the service account to use {{- end }} -# fence: {{ default (randAlphaNum 32) .Values.secrets.userdb.fence | quote }}, -# gdcapi: {{ default (randAlphaNum 32) .Values.secrets.userdb.gdcapi | quote }}, -# gateway: {{ default (randAlphaNum 32) .Values.secrets.userdb.gateway | quote }} {{/* Indexd Fence Creds @@ -98,4 +101,4 @@ Create the name of the service account to use */}} {{- define "indexd-gateway-creds" -}} {{- default (randAlphaNum 32) .Values.secrets.userdb.gateway }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/indexd/templates/aws-config.yaml b/helm/indexd/templates/aws-config.yaml new file mode 100644 index 00000000..4723e6b3 --- /dev/null +++ b/helm/indexd/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end }} \ No newline at end of file diff --git a/helm/indexd/templates/deployment.yaml b/helm/indexd/templates/deployment.yaml index 72c89d3f..cbb05552 100644 --- a/helm/indexd/templates/deployment.yaml +++ b/helm/indexd/templates/deployment.yaml @@ -13,12 +13,17 @@ spec: {{- include "indexd.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: + public: "yes" {{- include "indexd.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: {{- with .Values.volumes }} volumes: @@ -68,8 +73,13 @@ spec: name: indexd-dbcreds key: dbcreated optional: false + - name: DEFAULT_PREFIX + value: {{ .Values.defaultPrefix }} {{- toYaml .Values.env | nindent 12 }} volumeMounts: + - name: "uwsgi-config" + mountPath: "/etc/uwsgi/uwsgi.ini" + subPath: uwsgi.ini - name: "config-volume" readOnly: true mountPath: "/var/www/indexd/local_settings.py" diff --git a/helm/indexd/templates/external-secrets.yaml b/helm/indexd/templates/external-secrets.yaml new file mode 100644 index 00000000..b8cb2a38 --- /dev/null +++ b/helm/indexd/templates/external-secrets.yaml @@ -0,0 +1,21 @@ +{{ include "common.externalSecret.db" . }} +--- +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: indexd-service-creds +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: indexd-service-creds + creationPolicy: Owner + dataFrom: + - extract: + key: {{ .Values.externalSecrets.serviceCreds }} + conversionStrategy: Default + decodingStrategy: None +{{- end }} \ No newline at end of file diff --git a/helm/indexd/templates/indexd-secret.yaml b/helm/indexd/templates/indexd-secret.yaml index 391e9383..5b296ccf 100644 --- a/helm/indexd/templates/indexd-secret.yaml +++ b/helm/indexd/templates/indexd-secret.yaml @@ -6,33 +6,13 @@ type: Opaque data: {{ (.Files.Glob "indexd-settings/*").AsSecrets | indent 2 }} --- -apiVersion: v1 -kind: Secret -metadata: - name: indexd-creds -type: Opaque -stringData: - creds.json: |- - { - "db_host": "{{ include "gen3.service-postgres" (dict "key" "host" "service" $.Chart.Name "context" $) }}", - "db_username": "{{include "gen3.service-postgres" (dict "key" "username" "service" $.Chart.Name "context" $) }}", - "db_password": "{{include "gen3.service-postgres" (dict "key" "password" "service" $.Chart.Name "context" $) }}", - "db_database": "{{ include "gen3.service-postgres" (dict "key" "database" "service" $.Chart.Name "context" $)}}", - "user_db": { - "fence": {{ include "indexd-fence-creds" . | quote }}, - "gdcapi": {{ include "indexd-sheepdog-creds" . | quote }}, - "gateway": {{ include "indexd-gateway-creds" . | quote }} - } - } ---- +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sServiceCredsSecret) }} apiVersion: v1 kind: Secret metadata: name: indexd-service-creds type: Opaque -stringData: +data: fence: {{ include "common.getOrGenSecret" (list .Values.secrets.userdb.fence "indexd-service-creds" "fence" 20 .Release.Namespace) }} - gdcapi: {{ include "common.getOrGenSecret" (list .Values.secrets.userdb.gdcapi "indexd-service-creds" "gdcapi" 20 .Release.Namespace) }} - gateway: {{ include "common.getOrGenSecret" (list .Values.secrets.userdb.gateway "indexd-service-creds" "gateway" 20 .Release.Namespace) }} - - + sheepdog: {{ include "common.getOrGenSecret" (list .Values.secrets.userdb.sheepdog "indexd-service-creds" "sheepdog" 20 .Release.Namespace) }} +{{- end }} \ No newline at end of file diff --git a/helm/indexd/templates/netpolicy.yaml b/helm/indexd/templates/netpolicy.yaml new file mode 100644 index 00000000..93949e3a --- /dev/null +++ b/helm/indexd/templates/netpolicy.yaml @@ -0,0 +1,9 @@ +{{ include "common.db_netpolicy" . }} + +--- + +{{ include "common.ingress_netpolicy" . }} + +--- + +{{ include "common.egress_netpolicy" . }} \ No newline at end of file diff --git a/helm/indexd/templates/pdb.yaml b/helm/indexd/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/indexd/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/indexd/templates/pre-install.yaml b/helm/indexd/templates/pre-install.yaml index bd3b7b1e..f6c2e358 100644 --- a/helm/indexd/templates/pre-install.yaml +++ b/helm/indexd/templates/pre-install.yaml @@ -19,6 +19,11 @@ spec: volumes: {{- toYaml . | nindent 8 }} {{- end }} + initContainers: + - name: wait-for-indexd + image: curlimages/curl:latest + command: ["/bin/sh","-c"] + args: ["while [ $(curl -sw '%{http_code}' http://indexd-service/index -o /dev/null) -ne 200 ]; do sleep 5; echo 'Waiting for indexd...'; done"] containers: - name: indexd image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" @@ -68,7 +73,7 @@ spec: valueFrom: secretKeyRef: name: indexd-service-creds - key: fence + key: sheepdog optional: false imagePullPolicy: Always command: ["/bin/bash" ] @@ -77,8 +82,15 @@ spec: # Script always succeeds if it runs (echo exits with 0) # indexd image does not include jq, so use python - | - echo 'python /indexd/bin/index_admin.py create --username "fence" --password "${FENCE_PASS}' - python /indexd/bin/index_admin.py create --username "fence" --password "${FENCE_PASS}" - echo 'python /indexd/bin/index_admin.py create --username "sheepdog" --password "${SHEEPDOG_PASS}' - python /indexd/bin/index_admin.py create --username "sheepdog" --password "${SHEEPDOG_PASS}" + if command -v python &> /dev/null; then + echo 'python /indexd/bin/index_admin.py create --username "fence" --password "${FENCE_PASS}' + python /indexd/bin/index_admin.py create --username "fence" --password "${FENCE_PASS}" + echo 'python /indexd/bin/index_admin.py create --username "sheepdog" --password "${SHEEPDOG_PASS}' + python /indexd/bin/index_admin.py create --username "sheepdog" --password "${SHEEPDOG_PASS}" + else + echo 'poetry run python /indexd/bin/index_admin.py create --username "fence" --password "${FENCE_PASS}' + poetry run python /indexd/bin/index_admin.py create --username "fence" --password "${FENCE_PASS}" + echo 'poetry run python /indexd/bin/index_admin.py create --username "sheepdog" --password "${SHEEPDOG_PASS}' + poetry run python /indexd/bin/index_admin.py create --username "sheepdog" --password "${SHEEPDOG_PASS}" + fi restartPolicy: Never \ No newline at end of file diff --git a/helm/indexd/templates/secret-store.yaml b/helm/indexd/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/indexd/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/indexd/templates/tests/test-connection.yaml b/helm/indexd/templates/tests/test-connection.yaml index a33cf51f..fc5d3935 100644 --- a/helm/indexd/templates/tests/test-connection.yaml +++ b/helm/indexd/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "indexd.fullname" . }}-test-connection" + name: "indexd-test-connection" labels: {{- include "indexd.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "indexd.fullname" . }}:{{ .Values.service.port }}'] + args: ['indexd-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/indexd/templates/uwsgi.yaml b/helm/indexd/templates/uwsgi.yaml new file mode 100644 index 00000000..a6eef58c --- /dev/null +++ b/helm/indexd/templates/uwsgi.yaml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: indexd-uwsgi +data: + uwsgi.ini: | + [uwsgi] + protocol = uwsgi + socket = /var/run/gen3/uwsgi.sock + buffer-size = 32768 + uid = nginx + gid = nginx + chown-socket = nginx:nginx + chmod-socket = 666 + master = true + harakiri-verbose = true + # No global HARAKIRI, using only user HARAKIRI, because export overwrites it + # Cannot overwrite global HARAKIRI with user's: https://git.io/fjYuD + # harakiri = 45 + ; If VIRTUAL_ENV is set then use its value to specify the virtualenv directory + if-env = VIRTUAL_ENV + virtualenv = %(_) + endif = + http-timeout = 45 + socket-timeout = 45 + worker-reload-mercy = 45 + reload-mercy = 45 + mule-reload-mercy = 45 + disable-logging = true + wsgi-file=/indexd/wsgi.py + plugins = python3 + vacuum = true + pythonpath = /indexd/ + stats = 127.0.0.1:9191 + stats-http = true + env = prometheus_multiproc_dir=/var/tmp/uwsgi_flask_metrics + exec-asap = /indexd/clear_prometheus_multiproc /var/tmp/uwsgi_flask_metrics + # Initialize application in worker processes, not master. This prevents the + # workers from all trying to open the same database connections at startup. + lazy = true + lazy-apps = true + listen = {{ .Values.uwsgi.listen }} diff --git a/helm/indexd/values.yaml b/helm/indexd/values.yaml index a9375380..935e9d86 100644 --- a/helm/indexd/values.yaml +++ b/helm/indexd/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -42,20 +44,75 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. + tierAccessLimit: "1000" + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any indexd secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true +netPolicy: + # -- (array) List of app labels that require ingress to this service + ingressApps: + - fence + - presigned-url-fence + - fenceshib + - peregrine + - sheepdog + - ssjdispatcherjob + - metadata + - mariner + - mariner-engine + + # -- (array) List of apps that this app requires egress to + egressApps: + - fence + - presigned-url-fence + - fenceshib + - peregrine + - sheepdog + - ssjdispatcherjob + - metadata + - mariner + - mariner-engine + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will create the Helm "indexd-service-creds" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sServiceCredsSecret: false + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: + serviceCreds: "indexd-service-creds" + +# -- (map) Values for indexd secret. +secrets: + userdb: + fence: + sheepdog: + # gateway: + # -- (str) AWS access key ID to access the db restore job S3 bucket. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID to access the db restore job S3 bucket. Overrides global key. + awsSecretAccessKey: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -119,11 +176,13 @@ serviceAccount: podAnnotations: {} # -- (map) Security context for the pod -podSecurityContext: {} +podSecurityContext: + {} # fsGroup: 2000 # -- (map) Security context for the containers in the pod -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -175,28 +234,42 @@ affinity: {} # -- (list) Volumes to attach to the pod volumes: -- name: config-volume - secret: - secretName: "indexd-settings" -- name: creds-volume - secret: - secretName: "indexd-creds" + - name: uwsgi-config + configMap: + name: indexd-uwsgi + - name: config-volume + secret: + secretName: "indexd-settings" # -- (list) Volumes to mount to the container. volumeMounts: -- name: "config-volume" - readOnly: true - mountPath: "/var/www/indexd/local_settings.py" - subPath: "local_settings.py" + - name: "config-volume" + readOnly: true + mountPath: "/var/www/indexd/local_settings.py" + subPath: "local_settings.py" # -- (list) Environment variables to pass to the container env: + - name: "ARBORIST" + value: "true" - name: "GEN3_DEBUG" value: "False" -# -- (map) Values for indexd secret. -secrets: - userdb: - fence: test - gdcapi: - gateway: +# -- (map) Values for overriding uwsgi settings +uwsgi: + listen: 1024 + +# -- (string) default prefix for indexd +defaultPrefix: "PREFIX/" + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "S3-GS" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/manifestservice/Chart.yaml b/helm/manifestservice/Chart.yaml index 00803ba1..cde53eee 100644 --- a/helm/manifestservice/Chart.yaml +++ b/helm/manifestservice/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.6 +version: 0.1.18 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/manifestservice/README.md b/helm/manifestservice/README.md index a3440139..5fef5aa6 100644 --- a/helm/manifestservice/README.md +++ b/helm/manifestservice/README.md @@ -1,9 +1,15 @@ # manifestservice -![Version: 0.1.6](https://img.shields.io/badge/Version-0.1.6-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.18](https://img.shields.io/badge/Version-0.1.18-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for Kubernetes +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -21,16 +27,35 @@ A Helm chart for Kubernetes | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | env | list | `[{"name":"REQUESTS_CA_BUNDLE","value":"/etc/ssl/certs/ca-certificates.crt"},{"name":"MANIFEST_SERVICE_CONFIG_PATH","value":"/var/gen3/config/config.json"},{"name":"GEN3_DEBUG","value":"False"}]` | Environment variables to pass to the container | -| labels | map | `{"public":"yes","s3":"yes","userhelper":"yes"}` | Labels to use for the deployment | -| labels.public | string | `"yes"` | Grants ingress from the revproxy service for pods labeled with public=yes | -| labels.s3 | string | `"yes"` | Grants egress to AWS S3 addresses for pods labeled with s3=yes - note that the networkpolicy-s3 grants permissions to a superset of ip addresses that includes S3 | -| labels.userhelper | string | `"yes"` | Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes | +| externalSecrets | map | `{"createK8sManifestServiceSecret":false,"manifestserviceG3auto":null}` | External Secrets settings. | +| externalSecrets.createK8sManifestServiceSecret | string | `false` | Will create the Helm "manifestservice-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.manifestserviceG3auto | string | `nil` | Will override the name of the aws secrets manager secret. Default is "manifestservice-g3auto" | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any manifestservice secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | +| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/manifestservice","tag":""}` | Docker image information. | +| image.pullPolicy | string | `"Always"` | Docker pull policy. | +| image.repository | string | `"quay.io/cdis/manifestservice"` | Docker repository. | +| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | | manifestserviceG3auto | map | `{"awsaccesskey":"","awssecretkey":"","bucketName":"testbucket","hostname":"testinstall","prefix":"test"}` | Values for manifestservice secret. | | manifestserviceG3auto.awsaccesskey | string | `""` | AWS access key. | | manifestserviceG3auto.awssecretkey | string | `""` | AWS secret access key. | | manifestserviceG3auto.bucketName | string | `"testbucket"` | Bucket for the manifestservice to read and write to. | | manifestserviceG3auto.prefix | string | `"test"` | Directory name to use within the s3 bucket. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| partOf | string | `"Workspace-tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | +| replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | | resources.limits.cpu | string | `1` | The maximum amount of CPU the container can use | @@ -39,8 +64,10 @@ A Helm chart for Kubernetes | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | | revisionHistoryLimit | int | `2` | Number of old revisions to retain | -| selectorLabels.app | string | `"manifestservice"` | | -| selectorLabels.release | string | `"production"` | | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information for External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -54,6 +81,3 @@ A Helm chart for Kubernetes | terminationGracePeriodSeconds | int | `50` | Grace period that applies to the total time it takes for both the PreStop hook to execute and for the Container to stop normally. | | volumeMounts | list | `[{"mountPath":"/var/gen3/config/","name":"config-volume","readOnly":true}]` | Volumes to mount to the container. | | volumes | list | `[{"name":"config-volume","secret":{"secretName":"manifestservice-g3auto"}}]` | Volumes to attach to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/manifestservice/templates/_helpers.tpl b/helm/manifestservice/templates/_helpers.tpl index c790a343..fb9b68c7 100644 --- a/helm/manifestservice/templates/_helpers.tpl +++ b/helm/manifestservice/templates/_helpers.tpl @@ -30,24 +30,29 @@ Create chart name and version as used by the chart label. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} -{{/* Common labels */}} {{- define "manifestservice.labels" -}} -helm.sh/chart: {{ include "manifestservice.chart" . }} -{{ include "manifestservice.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "manifestservice.selectorLabels" -}} -app.kubernetes.io/name: {{ include "manifestservice.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -60,3 +65,10 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* + Audit g3 Auto Secrets Manager Name +*/}} +{{- define "manifestservice-g3auto" -}} +{{- default "manifestservice-g3auto" .Values.externalSecrets.manifestserviceG3auto }} +{{- end }} diff --git a/helm/manifestservice/templates/aws-config.yaml b/helm/manifestservice/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/manifestservice/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/manifestservice/templates/deployment.yaml b/helm/manifestservice/templates/deployment.yaml index 37bfc450..c13f4f0a 100644 --- a/helm/manifestservice/templates/deployment.yaml +++ b/helm/manifestservice/templates/deployment.yaml @@ -2,20 +2,31 @@ apiVersion: apps/v1 kind: Deployment metadata: name: manifestservice-deployment + labels: + {{- include "manifestservice.labels" . | nindent 4 }} spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} selector: - matchLabels: - {{- include "manifestservice.selectorLabels" . | nindent 8 }} + matchLabels: + {{- include "manifestservice.selectorLabels" . | nindent 6 }} revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} strategy: {{- toYaml .Values.strategy | nindent 8 }} template: metadata: - {{- with .Values.labels }} labels: - {{- toYaml . | nindent 8 }} - {{- end }} + public: "yes" + s3: "yes" + userhelper: "yes" {{- include "manifestservice.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/manifestservice-creds.yaml") . | sha256sum }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: {{- with .Values.affinity }} affinity: @@ -27,8 +38,8 @@ spec: terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds}} containers: - name: manifestservice - image: "quay.io/cdis/manifestservice:2022.09" - imagePullPolicy: Always + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} env: {{- toYaml .Values.env | nindent 12 }} volumeMounts: diff --git a/helm/manifestservice/templates/external-secret.yaml b/helm/manifestservice/templates/external-secret.yaml new file mode 100644 index 00000000..7d94f5c9 --- /dev/null +++ b/helm/manifestservice/templates/external-secret.yaml @@ -0,0 +1,19 @@ +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: manifestservice-g3auto +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: manifestservice-g3auto + creationPolicy: Owner + data: + - secretKey: config.json + remoteRef: + #name of secret in secrets manager + key: {{include "manifestservice-g3auto" .}} +{{- end }} \ No newline at end of file diff --git a/helm/manifestservice/templates/metadataservice-creds.yaml b/helm/manifestservice/templates/manifestservice-creds.yaml similarity index 56% rename from helm/manifestservice/templates/metadataservice-creds.yaml rename to helm/manifestservice/templates/manifestservice-creds.yaml index 22dd070a..11fd9990 100644 --- a/helm/manifestservice/templates/metadataservice-creds.yaml +++ b/helm/manifestservice/templates/manifestservice-creds.yaml @@ -1,3 +1,4 @@ +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sManifestServiceSecret) }} apiVersion: v1 kind: Secret metadata: @@ -7,9 +8,11 @@ stringData: config.json: |- { "manifest_bucket_name": "{{ .Values.manifestserviceG3auto.bucketName }}", - "hostname": "{{ .Values.manifestserviceG3auto.hostname }}", + "hostname": "{{ .Values.global.hostname }}", + {{ if and .Values.manifestserviceG3auto.awsaccesskey .Values.manifestserviceG3auto.awssecretkey }} "aws_access_key_id": "{{ .Values.manifestserviceG3auto.awsaccesskey }}", "aws_secret_access_key": "{{ .Values.manifestserviceG3auto.awssecretkey }}", + {{ end }} "prefix": "{{ .Values.manifestserviceG3auto.prefix }}" } - +{{- end }} \ No newline at end of file diff --git a/helm/manifestservice/templates/pdb.yaml b/helm/manifestservice/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/manifestservice/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/manifestservice/templates/secret-store.yaml b/helm/manifestservice/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/manifestservice/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/manifestservice/templates/service.yaml b/helm/manifestservice/templates/service.yaml index df8ef44a..173ba48c 100644 --- a/helm/manifestservice/templates/service.yaml +++ b/helm/manifestservice/templates/service.yaml @@ -1,14 +1,14 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "manifestservice.fullname" . }} + name: manifestservice-service labels: {{- include "manifestservice.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} - targetPort: http + targetPort: 80 protocol: TCP name: http selector: diff --git a/helm/manifestservice/templates/tests/test-connection.yaml b/helm/manifestservice/templates/tests/test-connection.yaml index e54df7be..3d4b1d87 100644 --- a/helm/manifestservice/templates/tests/test-connection.yaml +++ b/helm/manifestservice/templates/tests/test-connection.yaml @@ -1,9 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "manifestservice.fullname" . }}-test-connection" - labels: - {{- include "manifestservice.labels" . | nindent 4 }} + name: "manifestservice-test-connection" annotations: "helm.sh/hook": test spec: @@ -11,5 +9,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "manifestservice.fullname" . }}:{{ .Values.service.port }}'] + args: ['manifestservice-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/manifestservice/values.yaml b/helm/manifestservice/values.yaml index 54b36ac2..87578372 100644 --- a/helm/manifestservice/values.yaml +++ b/helm/manifestservice/values.yaml @@ -2,13 +2,61 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -selectorLabels: - app: manifestservice - release: production + +# Global configuration +global: + # -- (map) AWS configuration + aws: + # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. + enabled: false + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: + # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + environment: default + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any manifestservice secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will create the Helm "manifestservice-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sManifestServiceSecret: false + # -- (string) Will override the name of the aws secrets manager secret. Default is "manifestservice-g3auto" + manifestserviceG3auto: +# -- (map) Secret information for External Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: # -- (int) Number of old revisions to retain revisionHistoryLimit: 2 +# -- (int) Number of replicas for the deployment. +replicaCount: 1 + +# -- (map) Docker image information. +image: + # -- (string) Docker repository. + repository: quay.io/cdis/manifestservice + # -- (string) Docker pull policy. + pullPolicy: Always + # -- (string) Overrides the image tag whose default is the chart appVersion. + tag: "" + # -- (map) Kubernetes service information. service: # -- (string) Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". @@ -46,15 +94,6 @@ strategy: # -- (int) Maximum amount of pods that can be unavailable during the update. maxUnavailable: 0 -# -- (map) Labels to use for the deployment -labels: - # -- (string) Grants egress to AWS S3 addresses for pods labeled with s3=yes - note that the networkpolicy-s3 grants permissions to a superset of ip addresses that includes S3 - s3: "yes" - # -- (string) Grants ingress from the revproxy service for pods labeled with public=yes - public: "yes" - # -- (string) Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes - userhelper: "yes" - # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: @@ -128,3 +167,15 @@ manifestserviceG3auto: awsaccesskey: "" # -- (string) AWS secret access key. awssecretkey: "" + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Workspace-tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/metadata/Chart.yaml b/helm/metadata/Chart.yaml index 8ab843a3..02368daa 100644 --- a/helm/metadata/Chart.yaml +++ b/helm/metadata/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 +version: 0.1.17 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -24,10 +24,14 @@ version: 0.1.5 appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate + - name: elasticsearch + version: "7.17.1" + repository: "https://helm.elastic.co" + condition: elasticsearch.separate diff --git a/helm/metadata/README.md b/helm/metadata/README.md index 51cb1f55..642e62b3 100644 --- a/helm/metadata/README.md +++ b/helm/metadata/README.md @@ -1,6 +1,6 @@ # metadata -![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.17](https://img.shields.io/badge/Version-0.1.17-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Metadata Service @@ -8,8 +8,9 @@ A Helm chart for gen3 Metadata Service | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | +| https://helm.elastic.co | elasticsearch | 7.17.1 | ## Values @@ -22,8 +23,9 @@ A Helm chart for gen3 Metadata Service | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].operator | string | `"In"` | Operation type for the match expression. | | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values | list | `["metadata"]` | Value for the match expression key. | | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey | string | `"kubernetes.io/hostname"` | Value for topology key label. | -| aggMdsNamespace | string | `nil` | Namespae to use if AggMds is enabled. | -| args | list | `["-c","/env/bin/alembic upgrade head\n"]` | Arguments to pass to the init container. | +| aggMdsConfig | string | `"{\n \"configuration\": {\n \"schema\": {\n \"_subjects_count\": {\n \"type\": \"integer\"\n },\n \"__manifest\": {\n \"description\": \"an array of filename (usually DRS ids and its size\",\n \"type\": \"array\",\n \"properties\": {\n \"file_name\": {\n \"type\": \"string\"\n },\n \"file_size\": {\n \"type\": \"integer\"\n }\n }\n },\n \"tags\": {\n \"type\": \"array\"\n },\n \"_unique_id\": {},\n \"study_description\": {},\n \"study_id\": {},\n \"study_url\": {},\n \"project_id\": {},\n \"short_name\": {\n \"default\": \"not_set\"\n },\n \"year\": {\n \"default\": \"not_set\"\n },\n \"full_name\": {},\n \"commons_url\": {},\n \"commons\": {}\n },\n \"settings\": {\n \"cache_drs\": true\n }\n },\n \"adapter_commons\": {\n \"Gen3\": {\n \"mds_url\": \"https://gen3.datacommons.io/\",\n \"commons_url\": \"gen3.datacommons.io/\",\n \"adapter\": \"gen3\",\n \"config\": {\n \"guid_type\": \"discovery_metadata\",\n \"study_field\": \"gen3_discovery\"\n },\n \"keep_original_fields\": false,\n \"field_mappings\": {\n \"tags\": \"path:tags\",\n \"_unique_id\": \"path:_unique_id\",\n \"study_description\": \"path:summary\",\n \"full_name\": \"path:study_title\",\n \"short_name\": \"path:short_name\",\n \"year\": \"path:year\",\n \"accession_number\": \"path:accession_number\",\n \"commons\": \"Gen3 Data Commons\",\n \"study_url\": {\n \"path\": \"link\",\n \"default\": \"unknown\"\n }\n }\n }\n }\n}\n"` | | +| aggMdsNamespace | string | `"default"` | Namespae to use if AggMds is enabled. | +| args | list | `["-c","# Managing virtual environments via poetry instead of python since the AL base image update, but retaining backwards compatibility\npoetry run alembic upgrade head || /env/bin/alembic upgrade head\n"]` | Arguments to pass to the init container. | | automountServiceAccountToken | bool | `false` | Automount the default service account token | | autoscaling | map | `{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Configuration for autoscaling the number of replicas | | autoscaling.enabled | bool | `false` | Whether autoscaling is enabled | @@ -31,26 +33,40 @@ A Helm chart for gen3 Metadata Service | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | | command | list | `["/bin/sh"]` | Command to run for the init container. | -| dataDog | bool | `{"enabled":false,"env":"dev"}` | Whether Datadog is enabled. | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | debug | bool | `false` | | -| esEndpoint | string | `"elasticsearch:9200"` | Elasticsearch endpoint. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | +| elasticsearch.clusterName | string | `"gen3-elasticsearch"` | | +| elasticsearch.esConfig."elasticsearch.yml" | string | `"# Here we can add elasticsearch config\n"` | | +| elasticsearch.maxUnavailable | int | `0` | | +| elasticsearch.replicas | int | `1` | | +| elasticsearch.separate | bool | `false` | | +| elasticsearch.singleNode | bool | `true` | | +| esEndpoint | string | `"http://gen3-elasticsearch-master:9200"` | Elasticsearch endpoint. | +| externalSecrets | map | `{"createK8sMetadataSecret":false,"dbcreds":null,"metadataG3auto":null}` | External Secrets settings. | +| externalSecrets.createK8sMetadataSecret | string | `false` | Will create the Helm "metadata-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | +| externalSecrets.metadataG3auto | string | `nil` | Will override the name of the aws secrets manager secret. Default is "metadata-g3auto" | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any metadata secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -58,19 +74,19 @@ A Helm chart for gen3 Metadata Service | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | -| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/metadata-service","tag":"master"}` | Docker image information. | +| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/metadata-service","tag":"feat_es-7"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/metadata-service"` | Docker repository. | -| image.tag | string | `"master"` | Overrides the image tag whose default is the chart appVersion. | +| image.tag | string | `"feat_es-7"` | Overrides the image tag whose default is the chart appVersion. | | initContainerName | string | `"metadata-db-migrate"` | Name of the init container. | | initResources | map | `{"limits":{"cpu":0.8,"memory":"512Mi"}}` | Resource limits for the init container. | | initResources.limits | map | `{"cpu":0.8,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | | initResources.limits.cpu | string | `0.8` | The maximum amount of CPU the container can use | | initResources.limits.memory | string | `"512Mi"` | The maximum amount of memory the container can use | -| initVolumeMounts | list | `[{"mountPath":"/src/.env","name":"config-volume-g3auto","readOnly":true,"subPath":"metadata.env"}]` | Volumes to mount to the init container. | +| initVolumeMounts | list | `[{"mountPath":"/src/.env","name":"config-volume-g3auto","readOnly":true,"subPath":"metadata.env"},{"mountPath":"/mds/.env","name":"config-volume-g3auto","readOnly":true,"subPath":"metadata.env"}]` | Volumes to mount to the init container. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| partOf | string | `"Discovery-Tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | | postgres.database | string | `nil` | Database name for postgres. This is a service override, defaults to - | | postgres.dbCreate | bool | `nil` | Whether the database should be created. Default to global.postgres.dbCreate | @@ -81,7 +97,7 @@ A Helm chart for gen3 Metadata Service | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | -| releaseLabel | string | `"production"` | | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -91,6 +107,10 @@ A Helm chart for gen3 Metadata Service | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | | revisionHistoryLimit | int | `2` | Number of old revisions to retain | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information to access the db restore job S3 bucket. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":[{"name":"http","port":80,"protocol":"TCP","targetPort":80}],"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `[{"name":"http","port":80,"protocol":"TCP","targetPort":80}]` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -98,8 +118,5 @@ A Helm chart for gen3 Metadata Service | strategy | map | `{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"}` | Rolling update deployment strategy | | strategy.rollingUpdate.maxSurge | int | `1` | Number of additional replicas to add during rollout. | | strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | -| useAggMds | bool | `nil` | Set to true to aggregate metadata from multiple other Metadata Service instances. | -| volumeMounts | list | `[{"mountPath":"/src/.env","name":"config-volume-g3auto","readOnly":true,"subPath":"metadata.env"},{"mountPath":"/aggregate_config.json","name":"config-volume","readOnly":true,"subPath":"aggregate_config.json"},{"mountPath":"/metadata.json","name":"config-manifest","readOnly":true,"subPath":"json"}]` | Volumes to mount to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) +| useAggMds | bool | `"False"` | Set to true to aggregate metadata from multiple other Metadata Service instances. | +| volumeMounts | list | `[{"mountPath":"/src/.env","name":"config-volume-g3auto","readOnly":true,"subPath":"metadata.env"},{"mountPath":"/mds/.env","name":"config-volume-g3auto","readOnly":true,"subPath":"metadata.env"},{"mountPath":"/aggregate_config.json","name":"config-volume","readOnly":true,"subPath":"aggregate_config.json"},{"mountPath":"/metadata.json","name":"config-manifest","readOnly":true,"subPath":"json"}]` | Volumes to mount to the container. | diff --git a/helm/metadata/templates/_helpers.tpl b/helm/metadata/templates/_helpers.tpl index 8e99ad6d..91c790fd 100644 --- a/helm/metadata/templates/_helpers.tpl +++ b/helm/metadata/templates/_helpers.tpl @@ -30,26 +30,29 @@ Create chart name and version as used by the chart label. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} -{{/* Common labels */}} {{- define "metadata.labels" -}} -helm.sh/chart: {{ include "metadata.chart" . }} -{{ include "metadata.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "metadata.selectorLabels" -}} -app.kubernetes.io/name: {{ include "metadata.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ include "metadata.name" . }} -release: {{ .Values.releaseLabel }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -76,12 +79,8 @@ Create the name of the service account to use {{- end }} {{/* -Define ddEnabled + Metadata g3 Auto Secrets Manager Name */}} -{{- define "metadata.ddEnabled" -}} -{{- if .Values.global }} -{{- .Values.global.ddEnabled }} -{{- else}} -{{- .Values.dataDog.enabled }} -{{- end }} +{{- define "metadata-g3auto" -}} +{{- default "metadata-g3auto" .Values.externalSecrets.metadataG3auto }} {{- end }} \ No newline at end of file diff --git a/helm/metadata/templates/agg-mds-sync.yaml b/helm/metadata/templates/agg-mds-sync.yaml new file mode 100644 index 00000000..08754b40 --- /dev/null +++ b/helm/metadata/templates/agg-mds-sync.yaml @@ -0,0 +1,160 @@ +{{- if .Values.useAggMds }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: agg-mds-config +data: + aggregate_config.json: | + {{ .Values.aggMdsConfig | default "{}" | nindent 4 }} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: metadata-aggregate-sync +spec: + schedule: "0 0 1 1 */5" + jobTemplate: + spec: + template: + metadata: + labels: + app: gen3job + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: karpenter.sh/capacity-type + operator: In + values: + - on-demand + - weight: 99 + preference: + matchExpressions: + - key: eks.amazonaws.com/capacityType + operator: In + values: + - ONDEMAND + volumes: + - name: config-volume + configMap: + name: agg-mds-config + - name: shared-data + emptyDir: {} + initContainers: + - name: wait-for-es + image: alpine/curl + env: + - name: GEN3_ES_ENDPOINT + value: {{ .Values.esEndpoint | default "http://gen3-elasticsearch-master:9200" }} + imagePullPolicy: IfNotPresent + command: ["/bin/sh"] + args: + - "-c" + - | + echo "Waiting for Elasticsearch to be ready..." + until curl -s -XGET $GEN3_ES_ENDPOINT; do + echo "Elasticsearch is not ready yet..." + sleep 5 + done + echo "Elasticsearch is ready!" + - name: wait-for-metadata + image: alpine/curl + env: + - name: GEN3_ES_ENDPOINT + value: {{ .Values.esEndpoint | default "http://gen3-elasticsearch-master:9200" }} + imagePullPolicy: IfNotPresent + command: ["/bin/sh"] + args: + - "-c" + - | + echo "Waiting for metadata service to be ready" + until curl -s -XGET http://metadata-service; do + echo "Metadata service is not ready yet..." + sleep 5 + done + containers: + - name: metadata-sync + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + volumeMounts: + # - name: config-volume-g3auto + # readOnly: true + # mountPath: /src/.env + # subPath: metadata.env + - name: config-volume + readOnly: true + mountPath: /aggregate_config.json + subPath: aggregate_config.json + # - name: config-manifest + # readOnly: true + # mountPath: /metadata.json + # subPath: json + - name: shared-data + mountPath: /mnt/shared + env: + - name: GEN3_DEBUG + value: "False" + - name: GEN3_ES_ENDPOINT + value: {{ .Values.esEndpoint | default "http://gen3-elasticsearch-master:9200" }} + - name: USE_AGG_MDS + value: {{ (.Values.useAggMds | quote | default "True") }} + - name: AGG_MDS_NAMESPACE + value: {{ .Values.aggMdsNamespace | default .Release.Name }} + imagePullPolicy: Always + command: ["/bin/sh"] + args: + - "-c" + - | + cat /aggregate_config.json + /env/bin/python /src/src/mds/populate.py --config /aggregate_config.json + if [ $? -ne 0 ]; then + echo "WARNING: non zero exit code: $?" + echo "WARNING: non zero exit code: $?" > /mnt/shared/status + else + echo "Success" > /mnt/shared/status + fi + - name: slack-alert + env: + - name: slackWebHook + valueFrom: + configMapKeyRef: + name: global + key: slack_webhook + optional: true + - name: gen3Env + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname + optional: true + image: quay.io/cdis/awshelper:master + volumeMounts: + - name: shared-data + mountPath: /mnt/shared + command: ["/bin/bash"] + args: + - "-c" + - | + if [[ ! "$slackWebHook" =~ ^http ]]; then + echo "Slack webhook not set" + exit 0 + fi + while [ ! -f /mnt/shared/status ]; do + echo "Waiting for status file..." + sleep 5 + done + if ! [[ $(cat /mnt/shared/status) =~ "Success" ]]; then + success="SUCCESS" + color="2EB67D" + else + success="FAILED" + color="FF0000" + fi + echo "Sending ${success} message to slack..." + payload="{\"attachments\": [{\"fallback\": \"JOB ${success}: metadata-aggregate-sync cronjob on ${gen3Env}\",\"color\": \"#${color}\",\"title\": \"JOB ${success}: metadata-aggregate-sync cronjob on ${gen3Env}\",\"text\": \"Pod name: ${HOSTNAME}\",\"ts\": \"$(date +%s)\"}]}" + echo "Payload=${payload}" + curl -X POST --data-urlencode "payload=${payload}" "${slackWebHook}" + restartPolicy: Never +{{- end}} \ No newline at end of file diff --git a/helm/metadata/templates/aws-config.yaml b/helm/metadata/templates/aws-config.yaml new file mode 100644 index 00000000..4723e6b3 --- /dev/null +++ b/helm/metadata/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end }} \ No newline at end of file diff --git a/helm/metadata/templates/deployment.yaml b/helm/metadata/templates/deployment.yaml index 18dbef8b..58e06603 100644 --- a/helm/metadata/templates/deployment.yaml +++ b/helm/metadata/templates/deployment.yaml @@ -2,6 +2,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: metadata-deployment + labels: + {{- include "metadata.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} @@ -17,17 +19,15 @@ spec: template: metadata: labels: - # gen3 networkpolicy labels - netnolimit: 'yes' - public: 'yes' - userhelper: 'yes' - {{- if eq (include "metadata.ddEnabled" . ) "true" }} - tags.datadoghq.com/service: "guppy" - # TODO: move this to helpers so we can have this populated from a configmap - tags.datadoghq.com/env: {{ .Values.dataDog.env }} - tags.datadoghq.com/version: {{ .Values.image.tag | default .Chart.AppVersion }} - {{- end }} + netnolimit: "yes" + public: "yes" + userhelper: "yes" {{- include "metadata.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: {{- with .Values.affinity }} affinity: @@ -86,7 +86,7 @@ spec: optional: false {{- with .Values.useAggMds }} - name: USE_AGG_MDS - value: {{ . }} + value: {{ . | quote }} {{- end }} {{- with .Values.aggMdsNamespace}} - name: AGG_MDS_NAMESPACE diff --git a/helm/metadata/templates/external-secret.yaml b/helm/metadata/templates/external-secret.yaml new file mode 100644 index 00000000..c3bb3465 --- /dev/null +++ b/helm/metadata/templates/external-secret.yaml @@ -0,0 +1,32 @@ +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: metadata-g3auto +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: metadata-g3auto + creationPolicy: Owner + data: + - secretKey: base64Authz.txt + remoteRef: + #name of secret in secrets manager + key: {{include "metadata-g3auto" .}} + property: base64Authz.txt + - secretKey: dbcreds.json + remoteRef: + #name of secret in secrets manager + key: {{include "metadata-g3auto" .}} + property: dbcreds.json + - secretKey: metadata.env + remoteRef: + #name of secret in secrets manager + key: {{include "metadata-g3auto" .}} + property: metadata.env +{{- end }} +--- +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/metadata/templates/metadata.yaml b/helm/metadata/templates/metadata.yaml deleted file mode 100644 index 58700a6e..00000000 --- a/helm/metadata/templates/metadata.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: manifest-metadata -data: - metadata.json: | - { - "USE_AGG_MDS": "{{ .Values.USE_AGG_MDS }}", - "AGG_MDS_NAMESPACE": "{{ .Values.AGG_MDS_NAMESPACE }}" - } \ No newline at end of file diff --git a/helm/metadata/templates/netpolicy.yaml b/helm/metadata/templates/netpolicy.yaml new file mode 100644 index 00000000..70a5c3b5 --- /dev/null +++ b/helm/metadata/templates/netpolicy.yaml @@ -0,0 +1 @@ +{{ include "common.db_netpolicy" . }} \ No newline at end of file diff --git a/helm/metadata/templates/pdb.yaml b/helm/metadata/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/metadata/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/metadata/templates/secret-store.yaml b/helm/metadata/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/metadata/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/metadata/templates/secrets.yaml b/helm/metadata/templates/secrets.yaml index fcde0e48..0bd639d7 100644 --- a/helm/metadata/templates/secrets.yaml +++ b/helm/metadata/templates/secrets.yaml @@ -1,21 +1,16 @@ +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sMetadataSecret) }} apiVersion: v1 kind: Secret metadata: name: metadata-g3auto stringData: {{- $randomPass := printf "%s%s" "gateway:" (randAlphaNum 32) }} - base64Authz.txt: {{ $randomPass | b64enc | quote }} - dbcreds.json: | - { - "db_host": {{ .Values.postgres.host | quote }}, - "db_username": {{ .Values.postgres.user | quote}}, - "db_password": {{ include "metadata.postgres.password" . | quote }}, - "db_database": {{ .Values.postgres.dbname | quote }} - } + base64Authz.txt: {{ $randomPass | quote | b64enc }} metadata.env: | DEBUG={{ .Values.debug}} DB_HOST={{ .Values.postgres.host }} DB_USER={{ .Values.postgres.user }} DB_PASSWORD={{ include "metadata.postgres.password" . }} DB_DATABASE={{ .Values.postgres.dbname }} - ADMIN_LOGINS={{ $randomPass }} \ No newline at end of file + ADMIN_LOGINS={{ $randomPass }} +{{- end }} \ No newline at end of file diff --git a/helm/metadata/templates/tests/test-connection.yaml b/helm/metadata/templates/tests/test-connection.yaml index 007dd312..4bafd3c8 100644 --- a/helm/metadata/templates/tests/test-connection.yaml +++ b/helm/metadata/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "metadata.fullname" . }}:{{ .Values.service.port }}'] + args: ['metadata-service:80/_status'] restartPolicy: Never diff --git a/helm/metadata/values.yaml b/helm/metadata/values.yaml index adfecfc0..fd63e46d 100644 --- a/helm/metadata/values.yaml +++ b/helm/metadata/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -42,20 +44,43 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any metadata secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will create the Helm "metadata-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sMetadataSecret: false + # -- (string) Will override the name of the aws secrets manager secret. Default is "metadata-g3auto" + metadataG3auto: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: +# -- (map) Secret information to access the db restore job S3 bucket. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -84,9 +109,6 @@ postgresql: # -- (bool) Option to persist the dbs data. enabled: false -# Deployment -releaseLabel: "production" - # -- (map) Configuration for autoscaling the number of replicas autoscaling: # -- (bool) Whether autoscaling is enabled @@ -113,30 +135,25 @@ strategy: # -- (int) Maximum amount of pods that can be unavailable during the update. maxUnavailable: 0 -# -- (bool) Whether Datadog is enabled. -dataDog: - enabled: false - env: dev - # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - metadata - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - metadata + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (bool) Automount the default service account token automountServiceAccountToken: false @@ -148,17 +165,86 @@ image: # -- (string) Docker pull policy. pullPolicy: Always # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: "master" + tag: "feat_es-7" debug: false # Environment Variables # -- (string) Elasticsearch endpoint. -esEndpoint: elasticsearch:9200 +esEndpoint: http://gen3-elasticsearch-master:9200 # -- (bool) Set to true to aggregate metadata from multiple other Metadata Service instances. -useAggMds: +useAggMds: "False" # -- (string) Namespae to use if AggMds is enabled. -aggMdsNamespace: +aggMdsNamespace: default + +aggMdsConfig: | + { + "configuration": { + "schema": { + "_subjects_count": { + "type": "integer" + }, + "__manifest": { + "description": "an array of filename (usually DRS ids and its size", + "type": "array", + "properties": { + "file_name": { + "type": "string" + }, + "file_size": { + "type": "integer" + } + } + }, + "tags": { + "type": "array" + }, + "_unique_id": {}, + "study_description": {}, + "study_id": {}, + "study_url": {}, + "project_id": {}, + "short_name": { + "default": "not_set" + }, + "year": { + "default": "not_set" + }, + "full_name": {}, + "commons_url": {}, + "commons": {} + }, + "settings": { + "cache_drs": true + } + }, + "adapter_commons": { + "Gen3": { + "mds_url": "https://gen3.datacommons.io/", + "commons_url": "gen3.datacommons.io/", + "adapter": "gen3", + "config": { + "guid_type": "discovery_metadata", + "study_field": "gen3_discovery" + }, + "keep_original_fields": false, + "field_mappings": { + "tags": "path:tags", + "_unique_id": "path:_unique_id", + "study_description": "path:summary", + "full_name": "path:study_title", + "short_name": "path:short_name", + "year": "path:year", + "accession_number": "path:accession_number", + "commons": "Gen3 Data Commons", + "study_url": { + "path": "link", + "default": "unknown" + } + } + } + } + } # -- (list) Volumes to mount to the container. volumeMounts: @@ -166,6 +252,11 @@ volumeMounts: readOnly: true mountPath: /src/.env subPath: metadata.env + # Added an additional volume mount for new images using the / directory, while retaining the 'src' mount for backward compatibility. + - name: config-volume-g3auto + readOnly: true + mountPath: /mds/.env + subPath: metadata.env - name: config-volume readOnly: true mountPath: /aggregate_config.json @@ -199,6 +290,11 @@ initVolumeMounts: readOnly: true mountPath: /src/.env subPath: metadata.env + # Added an additional volume mount for new images using the / directory, while retaining the 'src' mount for backward compatibility. + - name: config-volume-g3auto + readOnly: true + mountPath: /mds/.env + subPath: metadata.env # -- (map) Resource limits for the init container. initResources: # -- (map) The maximum amount of resources that the container is allowed to use @@ -213,7 +309,8 @@ command: ["/bin/sh"] args: - "-c" - | - /env/bin/alembic upgrade head + # Managing virtual environments via poetry instead of python since the AL base image update, but retaining backwards compatibility + poetry run alembic upgrade head || /env/bin/alembic upgrade head # Service and Pod serviceAnnotations: @@ -236,3 +333,25 @@ service: port: 80 targetPort: 80 name: http + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Discovery-Tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: + +elasticsearch: + separate: false + clusterName: gen3-elasticsearch + maxUnavailable: 0 + singleNode: true + replicas: 1 + esConfig: + elasticsearch.yml: | + # Here we can add elasticsearch config diff --git a/helm/pidgin/.helmignore b/helm/neuvector/.helmignore similarity index 100% rename from helm/pidgin/.helmignore rename to helm/neuvector/.helmignore diff --git a/helm/neuvector/Chart.yaml b/helm/neuvector/Chart.yaml new file mode 100644 index 00000000..845c1797 --- /dev/null +++ b/helm/neuvector/Chart.yaml @@ -0,0 +1,34 @@ +apiVersion: v2 +name: neuvector +description: NeuVector Kubernetes Security Policy templates to protect Gen3 + +# NeuVector must be installed separately. +# Reference: https://open-docs.neuvector.com/basics/overview +# Reference: https://github.com/neuvector/neuvector-helm +# For more information, please use the Gen3 community Slack. +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.2 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" + +# Todo: Evaluate inclusion of NeuVector installation +# dependencies: +# - name: neuvector +# version: "5.2.2-s1" +# repository: "https://neuvector.github.io/neuvector-helm/core" diff --git a/helm/neuvector/README.md b/helm/neuvector/README.md new file mode 100644 index 00000000..928cd747 --- /dev/null +++ b/helm/neuvector/README.md @@ -0,0 +1,20 @@ +# neuvector + +![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.16.0](https://img.shields.io/badge/AppVersion-1.16.0-informational?style=flat-square) + +NeuVector Kubernetes Security Policy templates to protect Gen3 + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| ARGOCD_PREFIX | string | `"development-gen3"` | | +| DB_HOST | string | `"development-gen3-postgresql"` | | +| ES_HOST | string | `"gen3-elasticsearch-master"` | | +| fullnameOverride | string | `""` | | +| ingress.class | string | `"nginx"` | | +| ingress.controller | string | `"nginx-ingress-controller"` | | +| ingress.namespace | string | `"nginx"` | | +| nameOverride | string | `""` | | +| policies.include | bool | `true` | | +| policies.policyMode | string | `"Monitor"` | | diff --git a/helm/elasticsearch/templates/_helpers.tpl b/helm/neuvector/templates/_helpers.tpl similarity index 73% rename from helm/elasticsearch/templates/_helpers.tpl rename to helm/neuvector/templates/_helpers.tpl index 4e828574..cc8472e7 100644 --- a/helm/elasticsearch/templates/_helpers.tpl +++ b/helm/neuvector/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "elasticsearch.name" -}} +{{- define "neuvector.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} @@ -10,7 +10,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "elasticsearch.fullname" -}} +{{- define "neuvector.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "elasticsearch.chart" -}} +{{- define "neuvector.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "elasticsearch.labels" -}} -helm.sh/chart: {{ include "elasticsearch.chart" . }} -{{ include "elasticsearch.selectorLabels" . }} +{{- define "neuvector.labels" -}} +helm.sh/chart: {{ include "neuvector.chart" . }} +{{ include "neuvector.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -45,17 +45,17 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "elasticsearch.selectorLabels" -}} -app.kubernetes.io/name: {{ include "elasticsearch.name" . }} +{{- define "neuvector.selectorLabels" -}} +app.kubernetes.io/name: {{ include "neuvector.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "elasticsearch.serviceAccountName" -}} +{{- define "neuvector.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "elasticsearch.fullname" .) .Values.serviceAccount.name }} +{{- default (include "neuvector.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} diff --git a/helm/neuvector/templates/ambassador-nvsecurityrule.yaml b/helm/neuvector/templates/ambassador-nvsecurityrule.yaml new file mode 100644 index 00000000..01166345 --- /dev/null +++ b/helm/neuvector/templates/ambassador-nvsecurityrule.yaml @@ -0,0 +1,212 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.ambassador-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-0 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - SSL + name: external-egress-1 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.ambassador-deployment.{{ .Release.Namespace }}-ingress-6 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - SSL + name: nv.ambassador-deployment.{{ .Release.Namespace }}-ingress-7 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: hatchery-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.hatchery-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.ambassador-deployment.{{ .Release.Namespace }}-ingress-8 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.ambassador-deployment.{{ .Release.Namespace }}-ingress-9 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - any + name: nv.ambassador-deployment.{{ .Release.Namespace }}-ingress-10 + ports: tcp/8080 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: ambex + path: /opt/ambassador/bin/ambassador + - action: allow + allow_update: false + name: bash + path: /bin/bash + - action: allow + allow_update: false + name: busybox + path: /bin/busybox + - action: allow + allow_update: false + name: curl + path: /usr/bin/curl + - action: allow + allow_update: false + name: date + path: /bin/busybox + - action: allow + allow_update: false + name: diagd + path: /usr/bin/python3.7 + - action: allow + allow_update: false + name: env + path: /bin/busybox + - action: allow + allow_update: false + name: envoy + path: /usr/local/bin/envoy + - action: allow + allow_update: false + name: find + path: /bin/busybox + - action: allow + allow_update: false + name: grep + path: /bin/busybox + - action: allow + allow_update: false + name: mkdir + path: /bin/busybox + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: python + path: /usr/bin/python3.7 + - action: allow + allow_update: false + name: python3 + path: /usr/bin/python3.7 + - action: allow + allow_update: false + name: sh + path: /bin/busybox + - action: allow + allow_update: false + name: sleep + path: /bin/busybox + - action: allow + allow_update: false + name: sort + path: /bin/busybox + - action: allow + allow_update: false + name: uname + path: /bin/busybox + - action: allow + allow_update: false + name: watt + path: /opt/ambassador/bin/ambassador + - action: allow + allow_update: false + name: wc + path: /bin/busybox + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: ambassador-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.ambassador-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/arborist-nvsecurityrule.yaml b/helm/neuvector/templates/arborist-nvsecurityrule.yaml new file mode 100644 index 00000000..5ee722d7 --- /dev/null +++ b/helm/neuvector/templates/arborist-nvsecurityrule.yaml @@ -0,0 +1,218 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.arborist-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-7 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - HTTP + name: nv.fence-deployment.{{ .Release.Namespace }}-egress-8 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: fence-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.fence-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-egress-9 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.DB_HOST }}.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-17 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - HTTP + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-18 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-19 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-20 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: fence-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.fence-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - HTTP + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-21 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: peregrine-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.peregrine-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - HTTP + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-22 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: guppy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.guppy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - HTTP + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-23 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.arborist-deployment.{{ .Release.Namespace }}-ingress-24 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: hatchery-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.hatchery-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: arborist + path: /go/src/github.com/uc-cdis/arborist/bin/arborist + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: sh + path: /bin/dash + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: arborist-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.arborist-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/audit-nvsecurityrule.yaml b/helm/neuvector/templates/audit-nvsecurityrule.yaml new file mode 100644 index 00000000..5320e394 --- /dev/null +++ b/helm/neuvector/templates/audit-nvsecurityrule.yaml @@ -0,0 +1,98 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.audit-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-5 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-egress-6 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.DB_HOST }}.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.audit-deployment.{{ .Release.Namespace }}-ingress-15 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - any + name: nv.audit-deployment.{{ .Release.Namespace }}-ingress-16 + ports: tcp/80 + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + process: + - action: allow + allow_update: false + name: alembic + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: gunicorn + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: pause + path: /pause + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: audit-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.audit-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/elasticsearch-nvsecurityrule.yaml b/helm/neuvector/templates/elasticsearch-nvsecurityrule.yaml new file mode 100644 index 00000000..4089bf63 --- /dev/null +++ b/helm/neuvector/templates/elasticsearch-nvsecurityrule.yaml @@ -0,0 +1,87 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.gen3-elasticsearch-master.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: [] + file: [] + ingress: + - action: allow + applications: + - any + name: nv.gen3-elasticsearch-master.{{ .Release.Namespace }}-ingress-39 + ports: tcp/9200 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - any + name: nv.gen3-elasticsearch-master.{{ .Release.Namespace }}-ingress-40 + ports: tcp/9200 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: guppy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.guppy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - any + name: nv.gen3-elasticsearch-master.{{ .Release.Namespace }}-ingress-41 + ports: tcp/9200 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: guppy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.guppy-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: sh + path: '*' + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: gen3-elasticsearch-master.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.gen3-elasticsearch-master.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/etl-cronjob-nvsecurityrule.yaml b/helm/neuvector/templates/etl-cronjob-nvsecurityrule.yaml new file mode 100644 index 00000000..8a75b418 --- /dev/null +++ b/helm/neuvector/templates/etl-cronjob-nvsecurityrule.yaml @@ -0,0 +1,346 @@ +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.etl-cronjob-rule.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-0 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - HTTP + name: nv.{{ .Values.ES_HOST }}.{{ .Release.Namespace }}-egress-1 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ES_HOST }}.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.{{ .Values.ES_HOST }}.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-egress-2 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.DB_HOST }}.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: external-egress-3 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.revproxy-deployment.{{ .Release.Namespace }}-egress-4 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - any + name: nv.etl-cronjob.{{ .Release.Namespace }}-ingress-0 + ports: tcp/9000 + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + process: + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: bash + path: /bin/bash + - action: allow + allow_update: false + name: bash + path: /usr/bin/env + - action: allow + allow_update: false + name: bash + path: /usr/bin/setsid + - action: allow + allow_update: false + name: cat + path: /bin/cat + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: cp + path: /bin/cp + - action: allow + allow_update: false + name: df + path: /bin/df + - action: allow + allow_update: false + name: dirname + path: /usr/bin/dirname + - action: allow + allow_update: false + name: dpkg + path: /usr/bin/dpkg + - action: allow + allow_update: false + name: dpkg-query + path: /usr/bin/dpkg-query + - action: allow + allow_update: false + name: du + path: /usr/bin/du + - action: allow + allow_update: false + name: env + path: /usr/bin/env + - action: allow + allow_update: false + name: getconf + path: /usr/bin/getconf + - action: allow + allow_update: false + name: gzip + path: /bin/gzip + - action: allow + allow_update: false + name: hadoop + path: /usr/bin/env + - action: allow + allow_update: false + name: hdfs + path: /usr/bin/env + - action: allow + allow_update: false + name: head + path: /usr/bin/head + - action: allow + allow_update: false + name: id + path: /usr/bin/id + - action: allow + allow_update: false + name: java + path: /usr/lib/jvm/java-11-openjdk-amd64/bin/java + - action: allow + allow_update: false + name: jks-keystore + path: /bin/dash + - action: allow + allow_update: false + name: ld-2.28.so + path: /lib/x86_64-linux-gnu/ld-2.28.so + - action: allow + allow_update: false + name: ld-linux-x86-64 + path: /lib/x86_64-linux-gnu/ld-2.28.so + - action: allow + allow_update: false + name: ld-linux-x86-64.so.2 + path: /lib/x86_64-linux-gnu/ld-2.28.so + - action: allow + allow_update: false + name: ldd + path: /bin/bash + - action: allow + allow_update: false + name: ls + path: /bin/ls + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mountpoint + path: /bin/mountpoint + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nice + path: /usr/bin/nice + - action: allow + allow_update: false + name: nohup + path: /usr/bin/nohup + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: ps + path: /bin/ps + - action: allow + allow_update: false + name: psql + path: /usr/bin/perl + - action: allow + allow_update: false + name: psql + path: /usr/lib/postgresql/11/bin/psql + - action: allow + allow_update: false + name: python + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: python3 + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: renice + path: /usr/bin/renice + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: setsid + path: /usr/bin/setsid + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: slaves.sh + path: /usr/bin/env + - action: allow + allow_update: false + name: sleep + path: /bin/sleep + - action: allow + allow_update: false + name: spark-class + path: /usr/bin/env + - action: allow + allow_update: false + name: spark-submit + path: /usr/bin/env + - action: allow + allow_update: false + name: sqoop + path: /bin/bash + - action: allow + allow_update: false + name: ssh-keygen + path: /usr/bin/ssh-keygen + - action: allow + allow_update: false + name: sysctl + path: /sbin/sysctl + - action: allow + allow_update: false + name: tail + path: /usr/bin/tail + - action: allow + allow_update: false + name: tar + path: /bin/tar + - action: allow + allow_update: false + name: touch + path: /bin/touch + - action: allow + allow_update: false + name: tr + path: /usr/bin/tr + - action: allow + allow_update: false + name: uname + path: /bin/uname + - action: allow + allow_update: false + name: wget + path: /usr/bin/wget + - action: allow + allow_update: false + name: yarn + path: /usr/bin/env + process_profile: + baseline: zero-drift + target: + selector: + comment: "" + criteria: + - key: service + op: regex + value: etl-cronjob-.*\.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: etl-cronjob.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true diff --git a/helm/neuvector/templates/fence-nvsecurityrule.yaml b/helm/neuvector/templates/fence-nvsecurityrule.yaml new file mode 100644 index 00000000..8dc08b8f --- /dev/null +++ b/helm/neuvector/templates/fence-nvsecurityrule.yaml @@ -0,0 +1,294 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.fence-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - any + name: nodes-egress-12 + ports: tcp/8126 + priority: 0 + selector: + comment: "" + criteria: [] + name: nodes + original_name: "" + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-13 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - SSL + name: external-egress-14 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-31 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - SSL + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-32 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-33 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-34 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: peregrine-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.peregrine-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-35 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: hatchery-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.hatchery-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-36 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-37 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: cert-manager.cert-manager + - key: domain + op: = + value: cert-manager + name: nv.cert-manager.cert-manager + original_name: "" + - action: allow + applications: + - HTTP + name: nv.fence-deployment.{{ .Release.Namespace }}-ingress-38 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: manifestservice-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.manifestservice-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: bash + path: /bin/bash + - action: allow + allow_update: false + name: cat + path: /bin/cat + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: chown + path: /bin/chown + - action: allow + allow_update: false + name: ddtrace-run + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: fence-create + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: find + path: /usr/bin/find + - action: allow + allow_update: false + name: ldconfig + path: /sbin/ldconfig + - action: allow + allow_update: false + name: ls + path: /bin/ls + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: pip + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: python + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: sort + path: /usr/bin/sort + - action: allow + allow_update: false + name: uname + path: /bin/uname + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + - action: allow + allow_update: false + name: wc + path: /usr/bin/wc + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: fence-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.fence-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/guppy-nvsecurityrule.yaml b/helm/neuvector/templates/guppy-nvsecurityrule.yaml new file mode 100644 index 00000000..6d05c7fd --- /dev/null +++ b/helm/neuvector/templates/guppy-nvsecurityrule.yaml @@ -0,0 +1,129 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.guppy-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-15 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.guppy-deployment.{{ .Release.Namespace }}-ingress-42 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - any + name: nv.guppy-deployment.{{ .Release.Namespace }}-ingress-43 + ports: tcp/8000 + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - SSL + name: nv.guppy-deployment.{{ .Release.Namespace }}-ingress-44 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.guppy-deployment.{{ .Release.Namespace }}-ingress-45 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: bash + path: /usr/bin/bash + - action: allow + allow_update: false + name: cat + path: /usr/bin/cat + - action: allow + allow_update: false + name: dash + path: /usr/bin/dash + - action: allow + allow_update: false + name: ls + path: /usr/bin/ls + - action: allow + allow_update: false + name: node + path: /usr/bin/node + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: runc + path: /usr/bin/runc + - action: allow + allow_update: false + name: sh + path: /usr/bin/dash + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: guppy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.guppy-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/hatchery-nvsecurityrule.yaml b/helm/neuvector/templates/hatchery-nvsecurityrule.yaml new file mode 100644 index 00000000..cf46a487 --- /dev/null +++ b/helm/neuvector/templates/hatchery-nvsecurityrule.yaml @@ -0,0 +1,140 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.hatchery-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-16 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - SSL + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }}-egress-17 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: external-egress-18 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - SSL + name: nv.revproxy-deployment.{{ .Release.Namespace }}-egress-19 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.hatchery-deployment.{{ .Release.Namespace }}-ingress-46 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - SSL + name: nv.hatchery-deployment.{{ .Release.Namespace }}-ingress-47 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.hatchery-deployment.{{ .Release.Namespace }}-ingress-48 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: hatchery + path: /hatchery + - action: allow + allow_update: false + name: pause + path: /pause + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: hatchery-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.hatchery-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/indexd-nvsecurityrule.yaml b/helm/neuvector/templates/indexd-nvsecurityrule.yaml new file mode 100644 index 00000000..db1e1000 --- /dev/null +++ b/helm/neuvector/templates/indexd-nvsecurityrule.yaml @@ -0,0 +1,152 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.indexd-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-20 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.indexd-deployment.{{ .Release.Namespace }}-ingress-49 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - SSL + name: nv.indexd-deployment.{{ .Release.Namespace }}-ingress-50 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.indexd-deployment.{{ .Release.Namespace }}-ingress-51 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - HTTP + name: nv.indexd-deployment.{{ .Release.Namespace }}-ingress-52 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: bash + path: /bin/bash + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: chown + path: /bin/chown + - action: allow + allow_update: false + name: dockerrun.sh + path: /bin/dash + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: indexd-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.indexd-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/ingress-nvclustersecurityrule.yaml b/helm/neuvector/templates/ingress-nvclustersecurityrule.yaml new file mode 100644 index 00000000..febb5ef0 --- /dev/null +++ b/helm/neuvector/templates/ingress-nvclustersecurityrule.yaml @@ -0,0 +1,130 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvClusterSecurityRule +metadata: + name: {{ .Release.Namespace }} + namespace: "" +spec: + dlp: + settings: [] + status: true + egress: [] + file: [] + ingress: + - action: allow + applications: + - any + name: {{ .Release.Namespace }}-ingress-0 + ports: tcp/8089 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: allow + applications: + - any + name: {{ .Release.Namespace }}-ingress-1 + ports: tcp/8089 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: allow + applications: + - any + name: {{ .Release.Namespace }}-ingress-2 + ports: tcp/8089 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: allow + applications: + - any + name: {{ .Release.Namespace }}-ingress-3 + ports: tcp/8089 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: allow + applications: + - any + name: {{ .Release.Namespace }}-ingress-4 + ports: tcp/8089 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: deny + applications: + - any + name: {{ .Release.Namespace }}-ingress-5 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + process: [] + target: + policymode: N/A + selector: + comment: gen3 development group + criteria: + - key: namespace + op: = + value: {{ .Release.Namespace }} + name: {{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/manifestservice-nvsecurityrule.yaml b/helm/neuvector/templates/manifestservice-nvsecurityrule.yaml new file mode 100644 index 00000000..1efd07f5 --- /dev/null +++ b/helm/neuvector/templates/manifestservice-nvsecurityrule.yaml @@ -0,0 +1,166 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.manifestservice-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-21 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.manifestservice-deployment.{{ .Release.Namespace }}-ingress-53 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - any + name: nv.manifestservice-deployment.{{ .Release.Namespace }}-ingress-54 + ports: tcp/80 + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - HTTP + name: nv.manifestservice-deployment.{{ .Release.Namespace }}-ingress-55 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: cat + path: /bin/cat + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: dockerrun.sh + path: /bin/dash + - action: allow + allow_update: false + name: find + path: /usr/bin/find + - action: allow + allow_update: false + name: grep + path: /bin/grep + - action: allow + allow_update: false + name: ldconfig + path: /sbin/ldconfig + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mktemp + path: /bin/mktemp + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: sort + path: /usr/bin/sort + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + - action: allow + allow_update: false + name: wc + path: /usr/bin/wc + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: manifestservice-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.manifestservice-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/metadata-nvsecurityrule.yaml b/helm/neuvector/templates/metadata-nvsecurityrule.yaml new file mode 100644 index 00000000..3d85201c --- /dev/null +++ b/helm/neuvector/templates/metadata-nvsecurityrule.yaml @@ -0,0 +1,87 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.metadata-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-22 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.metadata-deployment.{{ .Release.Namespace }}-ingress-56 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + process: + - action: allow + allow_update: false + name: alembic + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: grep + path: /bin/grep + - action: allow + allow_update: false + name: gunicorn + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: runc + path: /usr/bin/runc + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: metadata-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.metadata-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/peregrine-nvsecurityrule.yaml b/helm/neuvector/templates/peregrine-nvsecurityrule.yaml new file mode 100644 index 00000000..f9df4477 --- /dev/null +++ b/helm/neuvector/templates/peregrine-nvsecurityrule.yaml @@ -0,0 +1,258 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.peregrine-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-23 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - SSL + name: nv.revproxy-deployment.{{ .Release.Namespace }}-egress-24 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - HTTP + - SSL + name: external-egress-25 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + - SSL + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }}-egress-26 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.peregrine-deployment.{{ .Release.Namespace }}-ingress-57 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - HTTP + name: nv.peregrine-deployment.{{ .Release.Namespace }}-ingress-58 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: pidgin-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.pidgin-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.peregrine-deployment.{{ .Release.Namespace }}-ingress-59 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.peregrine-deployment.{{ .Release.Namespace }}-ingress-60 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - any + name: nv.peregrine-deployment.{{ .Release.Namespace }}-ingress-61 + ports: tcp/80 + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - HTTP + name: nv.peregrine-deployment.{{ .Release.Namespace }}-ingress-62 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: portal-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.portal-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: cat + path: /bin/cat + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: dash + path: /bin/dash + - action: allow + allow_update: false + name: dockerrun.sh + path: /bin/dash + - action: allow + allow_update: false + name: find + path: /usr/bin/find + - action: allow + allow_update: false + name: grep + path: /bin/grep + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mktemp + path: /bin/mktemp + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: sort + path: /usr/bin/sort + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + - action: allow + allow_update: false + name: wc + path: /usr/bin/wc + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: peregrine-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.peregrine-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/pidgin-nvsecurityrule.yaml b/helm/neuvector/templates/pidgin-nvsecurityrule.yaml new file mode 100644 index 00000000..8f68b21a --- /dev/null +++ b/helm/neuvector/templates/pidgin-nvsecurityrule.yaml @@ -0,0 +1,135 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.pidgin-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-27 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.pidgin-deployment.{{ .Release.Namespace }}-ingress-63 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + process: + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: dash + path: /bin/dash + - action: allow + allow_update: false + name: dockerrun.sh + path: /bin/dash + - action: allow + allow_update: false + name: find + path: /usr/bin/find + - action: allow + allow_update: false + name: grep + path: /bin/grep + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mktemp + path: /bin/mktemp + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: sort + path: /usr/bin/sort + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + - action: allow + allow_update: false + name: wc + path: /usr/bin/wc + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: pidgin-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.pidgin-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/portal-nvsecurityrule.yaml b/helm/neuvector/templates/portal-nvsecurityrule.yaml new file mode 100644 index 00000000..0bbb87f3 --- /dev/null +++ b/helm/neuvector/templates/portal-nvsecurityrule.yaml @@ -0,0 +1,182 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.portal-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - any + name: nv.revproxy-deployment.{{ .Release.Namespace }}-egress-28 + ports: tcp/80 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - any + name: nv.revproxy-deployment.{{ .Release.Namespace }}-egress-29 + ports: tcp/80 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - any + name: nv.revproxy-deployment.{{ .Release.Namespace }}-egress-30 + ports: tcp/80 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - HTTP + name: nv.sheepdog-deployment.{{ .Release.Namespace }}-egress-31 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.portal-deployment.{{ .Release.Namespace }}-ingress-64 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - HTTP + name: nv.portal-deployment.{{ .Release.Namespace }}-ingress-65 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.portal-deployment.{{ .Release.Namespace }}-ingress-66 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + process: + - action: allow + allow_update: false + name: bash + path: /usr/bin/bash + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: node + path: /usr/bin/node + - action: allow + allow_update: false + name: npm + path: /usr/bin/env + - action: allow + allow_update: false + name: npm + path: /usr/bin/node + - action: allow + allow_update: false + name: npx + path: /usr/bin/env + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: relay-compiler + path: /usr/bin/env + - action: allow + allow_update: false + name: sh + path: /usr/bin/dash + - action: allow + allow_update: false + name: webpack + path: /usr/bin/env + - action: allow + allow_update: false + name: webpack + path: /usr/bin/node + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: portal-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.portal-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/postgresql-nvsecurityrule.yaml b/helm/neuvector/templates/postgresql-nvsecurityrule.yaml new file mode 100644 index 00000000..d1d1f076 --- /dev/null +++ b/helm/neuvector/templates/postgresql-nvsecurityrule.yaml @@ -0,0 +1,138 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: [] + file: [] + ingress: + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-ingress-25 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: peregrine-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.peregrine-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-ingress-26 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-ingress-27 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: indexd-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.indexd-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-ingress-28 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: metadata-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.metadata-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-ingress-29 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: presigned-url-fence-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.presigned-url-fence-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-ingress-30 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: fence-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.fence-deployment.{{ .Release.Namespace }} + original_name: "" + process: + - action: allow + allow_update: false + name: sh + path: '*' + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.DB_HOST }}.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/presigned-url-fence-nvsecurityrule.yaml b/helm/neuvector/templates/presigned-url-fence-nvsecurityrule.yaml new file mode 100644 index 00000000..5677d408 --- /dev/null +++ b/helm/neuvector/templates/presigned-url-fence-nvsecurityrule.yaml @@ -0,0 +1,173 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.presigned-url-fence-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - any + name: nodes-egress-32 + ports: tcp/8126 + priority: 0 + selector: + comment: "" + criteria: [] + name: nodes + original_name: "" + - action: allow + applications: + - SSL + name: external-egress-33 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-34 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.presigned-url-fence-deployment.{{ .Release.Namespace }}-ingress-67 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + process: + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: bash + path: /bin/bash + - action: allow + allow_update: false + name: cat + path: /bin/cat + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: chown + path: /bin/chown + - action: allow + allow_update: false + name: ddtrace-run + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: find + path: /usr/bin/find + - action: allow + allow_update: false + name: ldconfig + path: /sbin/ldconfig + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mktemp + path: /bin/mktemp + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: pip + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: sort + path: /usr/bin/sort + - action: allow + allow_update: false + name: uname + path: /bin/uname + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + - action: allow + allow_update: false + name: wc + path: /usr/bin/wc + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: presigned-url-fence-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.presigned-url-fence-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/revproxy-nvsecurityrule.yaml b/helm/neuvector/templates/revproxy-nvsecurityrule.yaml new file mode 100644 index 00000000..3661369b --- /dev/null +++ b/helm/neuvector/templates/revproxy-nvsecurityrule.yaml @@ -0,0 +1,175 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.revproxy-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-35 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - HTTP + name: nv.sheepdog-deployment.{{ .Release.Namespace }}-egress-36 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - any + name: nv.revproxy-deployment.{{ .Release.Namespace }}-ingress-68 + ports: tcp/80,tcp/443 + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: allow + applications: + - HTTP + name: nv.revproxy-deployment.{{ .Release.Namespace }}-ingress-69 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - HTTP + name: nv.revproxy-deployment.{{ .Release.Namespace }}-ingress-70 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.revproxy-deployment.{{ .Release.Namespace }}-ingress-71 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - SSL + name: nv.revproxy-deployment.{{ .Release.Namespace }}-ingress-72 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - SSL + name: nv.revproxy-deployment.{{ .Release.Namespace }}-ingress-73 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: cert-manager.cert-manager + - key: domain + op: = + value: cert-manager + name: nv.cert-manager.cert-manager + original_name: "" + process: + - action: allow + allow_update: false + name: bash + path: /bin/bash + - action: allow + allow_update: false + name: curl + path: /usr/bin/curl + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: sh + path: /bin/dash + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/sheepdog-nvsecurityrule.yaml b/helm/neuvector/templates/sheepdog-nvsecurityrule.yaml new file mode 100644 index 00000000..d587e45a --- /dev/null +++ b/helm/neuvector/templates/sheepdog-nvsecurityrule.yaml @@ -0,0 +1,184 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.sheepdog-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-37 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - HTTP + - SSL + name: external-egress-38 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + - SSL + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }}-egress-39 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + - key: domain + op: = + value: {{ .Values.ingress.class }} + name: nv.{{ .Values.ingress.controller }}.{{ .Values.ingress.namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - HTTP + name: nv.sheepdog-deployment.{{ .Release.Namespace }}-ingress-74 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - SSL + name: nv.sheepdog-deployment.{{ .Release.Namespace }}-ingress-75 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + process: + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: bash + path: /bin/bash + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: dockerrun.sh + path: /bin/dash + - action: allow + allow_update: false + name: find + path: /usr/bin/find + - action: allow + allow_update: false + name: ldconfig + path: /sbin/ldconfig + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: python + path: /usr/local/bin/python3.6 + - action: allow + allow_update: false + name: python3 + path: /usr/local/bin/python3.6 + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: sort + path: /usr/bin/sort + - action: allow + allow_update: false + name: uname + path: /bin/uname + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + - action: allow + allow_update: false + name: wc + path: /usr/bin/wc + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: sheepdog-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sheepdog-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/sower-nvsecurityrule.yaml b/helm/neuvector/templates/sower-nvsecurityrule.yaml new file mode 100644 index 00000000..fac23984 --- /dev/null +++ b/helm/neuvector/templates/sower-nvsecurityrule.yaml @@ -0,0 +1,32 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.sower.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: [] + file: [] + ingress: [] + process: [] + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: sower.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.sower.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/templates/wts-nvsecurityrule.yaml b/helm/neuvector/templates/wts-nvsecurityrule.yaml new file mode 100644 index 00000000..de07659a --- /dev/null +++ b/helm/neuvector/templates/wts-nvsecurityrule.yaml @@ -0,0 +1,199 @@ +{{- if .Values.policies.include }} +apiVersion: neuvector.com/v1 +kind: NvSecurityRule +metadata: + name: nv.wts-deployment.{{ .Release.Namespace }} +spec: + dlp: + settings: [] + status: true + egress: + - action: allow + applications: + - DNS + name: nv.kube-dns.kube-system-egress-2 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: kube-dns.kube-system + - key: domain + op: = + value: kube-system + name: nv.kube-dns.kube-system + original_name: "" + - action: allow + applications: + - HTTP + name: nv.fence-deployment.{{ .Release.Namespace }}-egress-3 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: fence-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.fence-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - PostgreSQL + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }}-egress-4 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: {{ .Values.DB_HOST }}.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.{{ .Values.DB_HOST }}.{{ .Release.Namespace }} + original_name: "" + file: [] + ingress: + - action: allow + applications: + - any + name: nv.wts-deployment.{{ .Release.Namespace }}-ingress-11 + ports: tcp/80 + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + - action: allow + applications: + - SSL + name: nv.wts-deployment.{{ .Release.Namespace }}-ingress-12 + ports: any + priority: 0 + selector: + comment: "" + criteria: [] + name: external + original_name: "" + - action: allow + applications: + - HTTP + name: nv.wts-deployment.{{ .Release.Namespace }}-ingress-13 + ports: any + priority: 0 + selector: + comment: "" + criteria: + - key: service + op: = + value: revproxy-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.revproxy-deployment.{{ .Release.Namespace }} + original_name: "" + - action: allow + applications: + - HTTP + name: nv.wts-deployment.{{ .Release.Namespace }}-ingress-14 + ports: any + priority: 0 + selector: + comment: "" + name: Workload:ingress + original_name: "" + process: + - action: allow + allow_update: false + name: alembic + path: /usr/local/bin/python3.9 + - action: allow + allow_update: false + name: basename + path: /usr/bin/basename + - action: allow + allow_update: false + name: chmod + path: /bin/chmod + - action: allow + allow_update: false + name: dockerrun.sh + path: /bin/dash + - action: allow + allow_update: false + name: find + path: /usr/bin/find + - action: allow + allow_update: false + name: mkdir + path: /bin/mkdir + - action: allow + allow_update: false + name: mv + path: /bin/mv + - action: allow + allow_update: false + name: nginx + path: /usr/sbin/nginx + - action: allow + allow_update: false + name: pause + path: /pause + - action: allow + allow_update: false + name: readlink + path: /bin/readlink + - action: allow + allow_update: false + name: rm + path: /bin/rm + - action: allow + allow_update: false + name: run-parts + path: /bin/run-parts + - action: allow + allow_update: false + name: sed + path: /bin/sed + - action: allow + allow_update: false + name: sh + path: /bin/dash + - action: allow + allow_update: false + name: sort + path: /usr/bin/sort + - action: allow + allow_update: false + name: uwsgi + path: /usr/local/bin/uwsgi + - action: allow + allow_update: false + name: wc + path: /usr/bin/wc + process_profile: + baseline: zero-drift + target: + policymode: {{ .Values.policies.policyMode }} + selector: + comment: "" + criteria: + - key: service + op: = + value: wts-deployment.{{ .Release.Namespace }} + - key: domain + op: = + value: {{ .Release.Namespace }} + name: nv.wts-deployment.{{ .Release.Namespace }} + original_name: "" + waf: + settings: [] + status: true +{{- end }} \ No newline at end of file diff --git a/helm/neuvector/values.yaml b/helm/neuvector/values.yaml new file mode 100644 index 00000000..410689cd --- /dev/null +++ b/helm/neuvector/values.yaml @@ -0,0 +1,32 @@ +# Default values for neuvector. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +nameOverride: "" +fullnameOverride: "" + +policies: + # deploy predefined Neuvector policies for Gen3 + include: true + # Neuvector policy enforcement mode + # Discover, Monitor, or Protect + # dev: Monitor + # prod: Protect + policyMode: Monitor + +# hostname/service name for our DB +DB_HOST: development-gen3-postgresql +# hostname/service name for our ElasitcSearch instance +ES_HOST: gen3-elasticsearch-master + +# Prefix for relevant services deployed through Argo +ARGOCD_PREFIX: development-gen3 + +# Configure your ingress controller information for enabling ingress to containers +ingress: + # service name of your ingress controller + controller: nginx-ingress-controller + # installation namespace of your ingress controller + namespace: nginx + # classname of your ingress + class: nginx diff --git a/helm/observability/Chart.yaml b/helm/observability/Chart.yaml new file mode 100644 index 00000000..0d0317ef --- /dev/null +++ b/helm/observability/Chart.yaml @@ -0,0 +1,31 @@ +apiVersion: v2 +name: lgtma-chart +description: A Helm chart for deploying the LGTM stack with additional resources + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.3 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.0.0" + +# Dependencies +dependencies: + - name: lgtm-distributed + version: "2.1.0" + alias: lgtm + repository: "https://grafana.github.io/helm-charts" diff --git a/helm/observability/Grafana.png b/helm/observability/Grafana.png new file mode 100644 index 00000000..7ed5d6ac Binary files /dev/null and b/helm/observability/Grafana.png differ diff --git a/helm/observability/README.md b/helm/observability/README.md new file mode 100644 index 00000000..72b9951d --- /dev/null +++ b/helm/observability/README.md @@ -0,0 +1,308 @@ +# lgtma-chart + +![Version: 0.1.3](https://img.shields.io/badge/Version-0.1.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0](https://img.shields.io/badge/AppVersion-1.0.0-informational?style=flat-square) + +A Helm chart for deploying the LGTM stack with additional resources + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://grafana.github.io/helm-charts | lgtm(lgtm-distributed) | 2.1.0 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| lgtm.grafana | map | `{"affinity":{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}},"alerting":{"contactpoints.yaml":{"secret":{"apiVersion":1,"contactPoints":[{"name":"slack","orgId":1,"receivers":[{"settings":{"group":"slack","summary":"{{ `{{ include \"default.message\" . }}` }}\n","url":"https://hooks.slack.com/services/XXXXXXXXXX"},"type":"Slack","uid":"first_uid"}]}]}},"rules.yaml":{"apiVersion":1,"groups":[{"folder":"Alerts","interval":"5m","name":"Alerts","orgId":1,"rules":[{"annotations":{"summary":"Alert: HTTP 500 errors detected in the environment: {{`{{ $labels.clusters }}`}}"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\"} | json | http_status_code=\"500\" [1h])) > 0","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"HTTP 500 errors detected","uid":"edwb8zgcvq96oc"},{"annotations":{"description":"Error in usersync job detected in cluster {{`{{ $labels.clusters }}`}}, namespace {{`{{ $labels.namespace }}`}}.","summary":"Error Logs Detected in Usersync Job"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster, namespace) (count_over_time({ app=\"gen3job\", job_name=~\"usersync-.*\"} |= \"ERROR - could not revoke policies from user `N/A`\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Error Logs Detected in Usersync Job","uid":"adwb9vhb7irr4b"},{"annotations":{"description":"Panic detected in app {{`{{ $labels.app }}`}} within cluster {{`{{ $labels.clusters }}`}}.","summary":"Hatchery panic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({app=\"hatchery\"} |= \"panic\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Hatchery panic in {{`{{ env.name }}`}}","uid":"ddwbc12l6wc8wf"},{"annotations":{"description":"Detected 431 HTTP status codes in the logs within the last 5 minutes.","summary":"Http status code 431"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum(count_over_time({cluster=~\".+\"} | json | http_status_code=\"431\" [5m])) >= 2","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Http status code 431","uid":"cdwbcbphz1zb4a"},{"annotations":{"description":"High number of info status logs detected in the indexd service in cluster {{`{{ $labels.clusters }}`}}.","summary":"Indexd is getting an excessive amount of traffic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\", app=\"indexd\", status=\"info\"} [5m])) > 50000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Indexd is getting an excessive amount of traffic","uid":"bdwbck1lgwdfka"},{"annotations":{"description":"More than 10 errors detected in the karpenter namespace in cluster {{`{{ $labels.clusters }}`}} related to providerRef not found.","summary":"Karpenter Resource Mismatch"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({namespace=\"karpenter\", cluster=~\".+\"} |= \"ERROR\" |= \"not found\" |= \"getting providerRef\" [5m])) > 10\n","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Karpenter Resource Mismatch","uid":"fdwbe5t439zpcd"},{"annotations":{"description":"More than 1000 \"limiting requests, excess\" errors detected in service {{`{{ $labels.app }}`}} (cluster: {{`{{ $labels.clusters }}`}}) within the last 5 minutes.","summary":"Nginx is logging excessive \" limiting requests, excess:\""},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (app, cluster) (count_over_time({app=~\".+\", cluster=~\".+\"} |= \"status:error\" |= \"limiting requests, excess:\" [5m])) > 1000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Nginx is logging excessive \" limiting requests, excess:\"","uid":"fdwbeuftc7400c"}]}]}},"dashboardProviders":{"dashboardproviders.yaml":{"apiVersion":1,"providers":[{"disableDeletion":true,"editable":true,"folder":"Kubernetes","name":"grafana-dashboards-kubernetes","options":{"path":"/var/lib/grafana/dashboards/grafana-dashboards-kubernetes"},"orgId":1,"type":"file"}]}},"dashboards":{"grafana-dashboards-kubernetes":{"k8s-system-api-server":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-api-server.json"},"k8s-system-coredns":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-coredns.json"},"k8s-views-global":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-global.json"},"k8s-views-namespaces":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-namespaces.json"},"k8s-views-nodes":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-nodes.json"},"k8s-views-pods":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-pods.json"}}},"downloadDashboardsImage":{"registry":"quay.io/curl","repository":"curl","tag":"8.8.0"},"enabled":true,"env":{"GF_SERVER_ROOT_URL":"https://grafana.example.com"},"envFromSecret":null,"grafana.ini":{"auth.okta":{"allow_sign_up":true,"auto_login":true,"enabled":true,"icon":"okta"},"feature_toggles":{"enable":"ssoSettingsAPI transformationsVariableSupport","ssoSettingsApi":true,"transformationsVariableSupport":true},"log":{"level":"debug"},"server":{"domain":"grafana.example.com","root_url":"https://%(domain)s/"},"users":{"auto_assign_org_role":"Editor"}},"image":{"pullPolicy":"Always","registry":"quay.io/cdis","repository":"grafana","tag":"master"},"ingress":{"annotations":{},"enabled":true,"hosts":["grafana.example.com"],"ingressClassName":"alb","tls":[{"secretName":null}]},"initChownData":{"image":{"registry":"quay.io/cdis","repository":"busybox","tag":"1.32.0"}},"persistence":{"enabled":true}}` | Grafana configuration. | +| lgtm.grafana."grafana.ini"."auth.okta" | map | `{"allow_sign_up":true,"auto_login":true,"enabled":true,"icon":"okta"}` | Okta authentication settings in Grafana. | +| lgtm.grafana."grafana.ini"."auth.okta".allow_sign_up | bool | `true` | Allow users to sign up automatically using Okta. | +| lgtm.grafana."grafana.ini"."auth.okta".auto_login | bool | `true` | Automatically log in users using Okta when visiting Grafana. | +| lgtm.grafana."grafana.ini"."auth.okta".enabled | bool | `true` | Enable or disable Okta authentication. | +| lgtm.grafana."grafana.ini"."auth.okta".icon | string | `"okta"` | Icon used for Okta in the Grafana UI. | +| lgtm.grafana."grafana.ini".feature_toggles | map | `{"enable":"ssoSettingsAPI transformationsVariableSupport","ssoSettingsApi":true,"transformationsVariableSupport":true}` | Feature toggles in Grafana. | +| lgtm.grafana."grafana.ini".feature_toggles.enable | list | `"ssoSettingsAPI transformationsVariableSupport"` | Features to be enabled in Grafana. | +| lgtm.grafana."grafana.ini".feature_toggles.ssoSettingsApi | bool | `true` | Enable Single Sign-On (SSO) settings API. | +| lgtm.grafana."grafana.ini".feature_toggles.transformationsVariableSupport | bool | `true` | Enable support for transformations using variables in Grafana. | +| lgtm.grafana."grafana.ini".log | map | `{"level":"debug"}` | Logging configuration in Grafana. | +| lgtm.grafana."grafana.ini".log.level | string | `"debug"` | Logging level for Grafana. Options: debug, info, warn, error. | +| lgtm.grafana."grafana.ini".server | map | `{"domain":"grafana.example.com","root_url":"https://%(domain)s/"}` | Server configuration in Grafana. | +| lgtm.grafana."grafana.ini".server.domain | string | `"grafana.example.com"` | Domain name for the Grafana server. | +| lgtm.grafana."grafana.ini".server.root_url | string | `"https://%(domain)s/"` | Root URL for Grafana, using the domain name. | +| lgtm.grafana."grafana.ini".users | map | `{"auto_assign_org_role":"Editor"}` | User configuration settings in Grafana. | +| lgtm.grafana."grafana.ini".users.auto_assign_org_role | string | `"Editor"` | Auto-assign the specified role to new users upon login. Options: Viewer, Editor, Admin. | +| lgtm.grafana.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling Grafana pods. | +| lgtm.grafana.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.grafana.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.grafana.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.grafana.alerting | map | `{"contactpoints.yaml":{"secret":{"apiVersion":1,"contactPoints":[{"name":"slack","orgId":1,"receivers":[{"settings":{"group":"slack","summary":"{{ `{{ include \"default.message\" . }}` }}\n","url":"https://hooks.slack.com/services/XXXXXXXXXX"},"type":"Slack","uid":"first_uid"}]}]}},"rules.yaml":{"apiVersion":1,"groups":[{"folder":"Alerts","interval":"5m","name":"Alerts","orgId":1,"rules":[{"annotations":{"summary":"Alert: HTTP 500 errors detected in the environment: {{`{{ $labels.clusters }}`}}"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\"} | json | http_status_code=\"500\" [1h])) > 0","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"HTTP 500 errors detected","uid":"edwb8zgcvq96oc"},{"annotations":{"description":"Error in usersync job detected in cluster {{`{{ $labels.clusters }}`}}, namespace {{`{{ $labels.namespace }}`}}.","summary":"Error Logs Detected in Usersync Job"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster, namespace) (count_over_time({ app=\"gen3job\", job_name=~\"usersync-.*\"} |= \"ERROR - could not revoke policies from user `N/A`\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Error Logs Detected in Usersync Job","uid":"adwb9vhb7irr4b"},{"annotations":{"description":"Panic detected in app {{`{{ $labels.app }}`}} within cluster {{`{{ $labels.clusters }}`}}.","summary":"Hatchery panic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({app=\"hatchery\"} |= \"panic\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Hatchery panic in {{`{{ env.name }}`}}","uid":"ddwbc12l6wc8wf"},{"annotations":{"description":"Detected 431 HTTP status codes in the logs within the last 5 minutes.","summary":"Http status code 431"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum(count_over_time({cluster=~\".+\"} | json | http_status_code=\"431\" [5m])) >= 2","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Http status code 431","uid":"cdwbcbphz1zb4a"},{"annotations":{"description":"High number of info status logs detected in the indexd service in cluster {{`{{ $labels.clusters }}`}}.","summary":"Indexd is getting an excessive amount of traffic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\", app=\"indexd\", status=\"info\"} [5m])) > 50000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Indexd is getting an excessive amount of traffic","uid":"bdwbck1lgwdfka"},{"annotations":{"description":"More than 10 errors detected in the karpenter namespace in cluster {{`{{ $labels.clusters }}`}} related to providerRef not found.","summary":"Karpenter Resource Mismatch"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({namespace=\"karpenter\", cluster=~\".+\"} |= \"ERROR\" |= \"not found\" |= \"getting providerRef\" [5m])) > 10\n","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Karpenter Resource Mismatch","uid":"fdwbe5t439zpcd"},{"annotations":{"description":"More than 1000 \"limiting requests, excess\" errors detected in service {{`{{ $labels.app }}`}} (cluster: {{`{{ $labels.clusters }}`}}) within the last 5 minutes.","summary":"Nginx is logging excessive \" limiting requests, excess:\""},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (app, cluster) (count_over_time({app=~\".+\", cluster=~\".+\"} |= \"status:error\" |= \"limiting requests, excess:\" [5m])) > 1000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Nginx is logging excessive \" limiting requests, excess:\"","uid":"fdwbeuftc7400c"}]}]}}` | Gen3 built-in alerting configuration in Grafana. | +| lgtm.grafana.alerting."rules.yaml" | string | `{"apiVersion":1,"groups":[{"folder":"Alerts","interval":"5m","name":"Alerts","orgId":1,"rules":[{"annotations":{"summary":"Alert: HTTP 500 errors detected in the environment: {{`{{ $labels.clusters }}`}}"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\"} | json | http_status_code=\"500\" [1h])) > 0","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"HTTP 500 errors detected","uid":"edwb8zgcvq96oc"},{"annotations":{"description":"Error in usersync job detected in cluster {{`{{ $labels.clusters }}`}}, namespace {{`{{ $labels.namespace }}`}}.","summary":"Error Logs Detected in Usersync Job"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster, namespace) (count_over_time({ app=\"gen3job\", job_name=~\"usersync-.*\"} |= \"ERROR - could not revoke policies from user `N/A`\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Error Logs Detected in Usersync Job","uid":"adwb9vhb7irr4b"},{"annotations":{"description":"Panic detected in app {{`{{ $labels.app }}`}} within cluster {{`{{ $labels.clusters }}`}}.","summary":"Hatchery panic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({app=\"hatchery\"} |= \"panic\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Hatchery panic in {{`{{ env.name }}`}}","uid":"ddwbc12l6wc8wf"},{"annotations":{"description":"Detected 431 HTTP status codes in the logs within the last 5 minutes.","summary":"Http status code 431"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum(count_over_time({cluster=~\".+\"} | json | http_status_code=\"431\" [5m])) >= 2","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Http status code 431","uid":"cdwbcbphz1zb4a"},{"annotations":{"description":"High number of info status logs detected in the indexd service in cluster {{`{{ $labels.clusters }}`}}.","summary":"Indexd is getting an excessive amount of traffic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\", app=\"indexd\", status=\"info\"} [5m])) > 50000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Indexd is getting an excessive amount of traffic","uid":"bdwbck1lgwdfka"},{"annotations":{"description":"More than 10 errors detected in the karpenter namespace in cluster {{`{{ $labels.clusters }}`}} related to providerRef not found.","summary":"Karpenter Resource Mismatch"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({namespace=\"karpenter\", cluster=~\".+\"} |= \"ERROR\" |= \"not found\" |= \"getting providerRef\" [5m])) > 10\n","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Karpenter Resource Mismatch","uid":"fdwbe5t439zpcd"},{"annotations":{"description":"More than 1000 \"limiting requests, excess\" errors detected in service {{`{{ $labels.app }}`}} (cluster: {{`{{ $labels.clusters }}`}}) within the last 5 minutes.","summary":"Nginx is logging excessive \" limiting requests, excess:\""},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (app, cluster) (count_over_time({app=~\".+\", cluster=~\".+\"} |= \"status:error\" |= \"limiting requests, excess:\" [5m])) > 1000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Nginx is logging excessive \" limiting requests, excess:\"","uid":"fdwbeuftc7400c"}]}]}` | Alerting rules configuration file. | +| lgtm.grafana.alerting."rules.yaml".apiVersion | int | `1` | API version for the alerting rules configuration. | +| lgtm.grafana.alerting."rules.yaml".groups | list | `[{"folder":"Alerts","interval":"5m","name":"Alerts","orgId":1,"rules":[{"annotations":{"summary":"Alert: HTTP 500 errors detected in the environment: {{`{{ $labels.clusters }}`}}"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\"} | json | http_status_code=\"500\" [1h])) > 0","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"HTTP 500 errors detected","uid":"edwb8zgcvq96oc"},{"annotations":{"description":"Error in usersync job detected in cluster {{`{{ $labels.clusters }}`}}, namespace {{`{{ $labels.namespace }}`}}.","summary":"Error Logs Detected in Usersync Job"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster, namespace) (count_over_time({ app=\"gen3job\", job_name=~\"usersync-.*\"} |= \"ERROR - could not revoke policies from user `N/A`\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Error Logs Detected in Usersync Job","uid":"adwb9vhb7irr4b"},{"annotations":{"description":"Panic detected in app {{`{{ $labels.app }}`}} within cluster {{`{{ $labels.clusters }}`}}.","summary":"Hatchery panic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({app=\"hatchery\"} |= \"panic\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Hatchery panic in {{`{{ env.name }}`}}","uid":"ddwbc12l6wc8wf"},{"annotations":{"description":"Detected 431 HTTP status codes in the logs within the last 5 minutes.","summary":"Http status code 431"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum(count_over_time({cluster=~\".+\"} | json | http_status_code=\"431\" [5m])) >= 2","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Http status code 431","uid":"cdwbcbphz1zb4a"},{"annotations":{"description":"High number of info status logs detected in the indexd service in cluster {{`{{ $labels.clusters }}`}}.","summary":"Indexd is getting an excessive amount of traffic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\", app=\"indexd\", status=\"info\"} [5m])) > 50000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Indexd is getting an excessive amount of traffic","uid":"bdwbck1lgwdfka"},{"annotations":{"description":"More than 10 errors detected in the karpenter namespace in cluster {{`{{ $labels.clusters }}`}} related to providerRef not found.","summary":"Karpenter Resource Mismatch"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({namespace=\"karpenter\", cluster=~\".+\"} |= \"ERROR\" |= \"not found\" |= \"getting providerRef\" [5m])) > 10\n","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Karpenter Resource Mismatch","uid":"fdwbe5t439zpcd"},{"annotations":{"description":"More than 1000 \"limiting requests, excess\" errors detected in service {{`{{ $labels.app }}`}} (cluster: {{`{{ $labels.clusters }}`}}) within the last 5 minutes.","summary":"Nginx is logging excessive \" limiting requests, excess:\""},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (app, cluster) (count_over_time({app=~\".+\", cluster=~\".+\"} |= \"status:error\" |= \"limiting requests, excess:\" [5m])) > 1000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Nginx is logging excessive \" limiting requests, excess:\"","uid":"fdwbeuftc7400c"}]}]` | Groups of alerting rules. | +| lgtm.grafana.alerting."rules.yaml".groups[0].folder | string | `"Alerts"` | Folder where the alerts will be placed in Grafana. | +| lgtm.grafana.alerting."rules.yaml".groups[0].interval | string | `"5m"` | Interval at which the alert rules are evaluated. | +| lgtm.grafana.alerting."rules.yaml".groups[0].name | string | `"Alerts"` | Name of the alert group. | +| lgtm.grafana.alerting."rules.yaml".groups[0].rules | list | `[{"annotations":{"summary":"Alert: HTTP 500 errors detected in the environment: {{`{{ $labels.clusters }}`}}"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\"} | json | http_status_code=\"500\" [1h])) > 0","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"HTTP 500 errors detected","uid":"edwb8zgcvq96oc"},{"annotations":{"description":"Error in usersync job detected in cluster {{`{{ $labels.clusters }}`}}, namespace {{`{{ $labels.namespace }}`}}.","summary":"Error Logs Detected in Usersync Job"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster, namespace) (count_over_time({ app=\"gen3job\", job_name=~\"usersync-.*\"} |= \"ERROR - could not revoke policies from user `N/A`\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Error Logs Detected in Usersync Job","uid":"adwb9vhb7irr4b"},{"annotations":{"description":"Panic detected in app {{`{{ $labels.app }}`}} within cluster {{`{{ $labels.clusters }}`}}.","summary":"Hatchery panic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({app=\"hatchery\"} |= \"panic\" [5m])) > 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Hatchery panic in {{`{{ env.name }}`}}","uid":"ddwbc12l6wc8wf"},{"annotations":{"description":"Detected 431 HTTP status codes in the logs within the last 5 minutes.","summary":"Http status code 431"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum(count_over_time({cluster=~\".+\"} | json | http_status_code=\"431\" [5m])) >= 2","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Http status code 431","uid":"cdwbcbphz1zb4a"},{"annotations":{"description":"High number of info status logs detected in the indexd service in cluster {{`{{ $labels.clusters }}`}}.","summary":"Indexd is getting an excessive amount of traffic"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({cluster=~\".+\", app=\"indexd\", status=\"info\"} [5m])) > 50000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Indexd is getting an excessive amount of traffic","uid":"bdwbck1lgwdfka"},{"annotations":{"description":"More than 10 errors detected in the karpenter namespace in cluster {{`{{ $labels.clusters }}`}} related to providerRef not found.","summary":"Karpenter Resource Mismatch"},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (cluster) (count_over_time({namespace=\"karpenter\", cluster=~\".+\"} |= \"ERROR\" |= \"not found\" |= \"getting providerRef\" [5m])) > 10\n","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Karpenter Resource Mismatch","uid":"fdwbe5t439zpcd"},{"annotations":{"description":"More than 1000 \"limiting requests, excess\" errors detected in service {{`{{ $labels.app }}`}} (cluster: {{`{{ $labels.clusters }}`}}) within the last 5 minutes.","summary":"Nginx is logging excessive \" limiting requests, excess:\""},"condition":"A","data":[{"datasourceUid":"loki","model":{"datasource":{"type":"loki","uid":"loki"},"editorMode":"code","expr":"sum by (app, cluster) (count_over_time({app=~\".+\", cluster=~\".+\"} |= \"status:error\" |= \"limiting requests, excess:\" [5m])) > 1000","hide":false,"intervalMs":1000,"maxDataPoints":43200,"queryType":"instant","refId":"A"},"queryType":"instant","refId":"A","relativeTimeRange":{"from":600,"to":0}}],"execErrState":"KeepLast","for":"5m","isPaused":false,"labels":{},"noDataState":"OK","notification_settings":{"receiver":"Slack"},"title":"Nginx is logging excessive \" limiting requests, excess:\"","uid":"fdwbeuftc7400c"}]` | List of alerting rules to be defined (add specific rules here). | +| lgtm.grafana.dashboardProviders | map | `{"dashboardproviders.yaml":{"apiVersion":1,"providers":[{"disableDeletion":true,"editable":true,"folder":"Kubernetes","name":"grafana-dashboards-kubernetes","options":{"path":"/var/lib/grafana/dashboards/grafana-dashboards-kubernetes"},"orgId":1,"type":"file"}]}}` | Configuration for dashboard providers in Grafana. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".apiVersion | int | `1` | API version for dashboard provider configuration. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers | list | `[{"disableDeletion":true,"editable":true,"folder":"Kubernetes","name":"grafana-dashboards-kubernetes","options":{"path":"/var/lib/grafana/dashboards/grafana-dashboards-kubernetes"},"orgId":1,"type":"file"}]` | List of dashboard providers. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers[0].disableDeletion | bool | `true` | Prevent deletion of the provided dashboards. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers[0].editable | bool | `true` | Allow editing of the dashboards. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers[0].folder | string | `"Kubernetes"` | Folder where the dashboards will be placed in Grafana. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers[0].options | map | `{"path":"/var/lib/grafana/dashboards/grafana-dashboards-kubernetes"}` | Options for the dashboard provider. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers[0].options.path | string | `"/var/lib/grafana/dashboards/grafana-dashboards-kubernetes"` | Path to the dashboard files. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers[0].orgId | int | `1` | Organization ID in Grafana. | +| lgtm.grafana.dashboardProviders."dashboardproviders.yaml".providers[0].type | string | `"file"` | Type of dashboard provider, usually 'file'. | +| lgtm.grafana.dashboards | map | `{"grafana-dashboards-kubernetes":{"k8s-system-api-server":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-api-server.json"},"k8s-system-coredns":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-coredns.json"},"k8s-views-global":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-global.json"},"k8s-views-namespaces":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-namespaces.json"},"k8s-views-nodes":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-nodes.json"},"k8s-views-pods":{"token":"","url":"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-pods.json"}}}` | Dashboards configuration. URLs to fetch specific Kubernetes-related Grafana dashboards. Gen3 specific dashboards can be found here. https://github.com/uc-cdis/grafana-dashboards | +| lgtm.grafana.dashboards.grafana-dashboards-kubernetes.k8s-system-api-server.token | string | `""` | Authentication token for accessing the dashboard URL (optional). | +| lgtm.grafana.dashboards.grafana-dashboards-kubernetes.k8s-system-api-server.url | string | `"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-api-server.json"` | URL to the dashboard JSON file for the Kubernetes API server. | +| lgtm.grafana.dashboards.grafana-dashboards-kubernetes.k8s-system-coredns.url | string | `"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-coredns.json"` | URL to the dashboard JSON file for CoreDNS in Kubernetes. | +| lgtm.grafana.dashboards.grafana-dashboards-kubernetes.k8s-views-global.url | string | `"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-global.json"` | URL to the dashboard JSON file for global views in Kubernetes. | +| lgtm.grafana.dashboards.grafana-dashboards-kubernetes.k8s-views-namespaces.url | string | `"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-namespaces.json"` | URL to the dashboard JSON file for Kubernetes namespace views. | +| lgtm.grafana.dashboards.grafana-dashboards-kubernetes.k8s-views-nodes.url | string | `"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-nodes.json"` | URL to the dashboard JSON file for Kubernetes node views. | +| lgtm.grafana.dashboards.grafana-dashboards-kubernetes.k8s-views-pods.url | string | `"https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-pods.json"` | URL to the dashboard JSON file for Kubernetes pod views. | +| lgtm.grafana.downloadDashboardsImage | map | `{"registry":"quay.io/curl","repository":"curl","tag":"8.8.0"}` | Image used to download Grafana dashboards. | +| lgtm.grafana.downloadDashboardsImage.registry | string | `"quay.io/curl"` | Container image registry for the dashboard download image. | +| lgtm.grafana.downloadDashboardsImage.repository | string | `"curl"` | Repository for the curl image. | +| lgtm.grafana.downloadDashboardsImage.tag | string | `"8.8.0"` | Tag for the curl image version. | +| lgtm.grafana.enabled | bool | `true` | Deploy Grafana if enabled. See [upstream readme](https://github.com/grafana/helm-charts/tree/main/charts/grafana#configuration) for full values reference. | +| lgtm.grafana.env | map | `{"GF_SERVER_ROOT_URL":"https://grafana.example.com"}` | Environment variables for Grafana. | +| lgtm.grafana.env.GF_SERVER_ROOT_URL | string | `"https://grafana.example.com"` | Root URL configuration for the Grafana server. | +| lgtm.grafana.envFromSecret | string | `nil` | Reference a secret for environment variables. | +| lgtm.grafana.image | map | `{"pullPolicy":"Always","registry":"quay.io/cdis","repository":"grafana","tag":"master"}` | Image configuration for Grafana. | +| lgtm.grafana.image.pullPolicy | string | `"Always"` | Pull policy for the Grafana image (e.g., 'Always'). | +| lgtm.grafana.image.registry | string | `"quay.io/cdis"` | Container image registry for Grafana. | +| lgtm.grafana.image.repository | string | `"grafana"` | Repository for the Grafana image. | +| lgtm.grafana.image.tag | string | `"master"` | Tag for the Grafana image version. | +| lgtm.grafana.ingress.annotations | map | `{}` | Annotations for Grafana ingress. | +| lgtm.grafana.ingress.enabled | bool | `true` | Enable or disable ingress for Grafana. | +| lgtm.grafana.ingress.hosts | list | `["grafana.example.com"]` | Hostname(s) for Grafana ingress. | +| lgtm.grafana.ingress.ingressClassName | string | `"alb"` | Ingress class name to be used (e.g., 'alb' for AWS Application Load Balancer). | +| lgtm.grafana.ingress.tls[0] | list | `{"secretName":null}` | TLS configuration for the ingress. Reference to a secret that contains the TLS certificate. | +| lgtm.grafana.initChownData | map | `{"image":{"registry":"quay.io/cdis","repository":"busybox","tag":"1.32.0"}}` | Init container to chown data directories for Grafana. | +| lgtm.grafana.initChownData.image.registry | string | `"quay.io/cdis"` | Container image registry for the init container. | +| lgtm.grafana.initChownData.image.repository | string | `"busybox"` | Repository for the busybox image. | +| lgtm.grafana.initChownData.image.tag | string | `"1.32.0"` | Tag for the busybox image version. | +| lgtm.grafana.persistence | map | `{"enabled":true}` | Persistence configuration for Grafana. | +| lgtm.grafana.persistence.enabled | bool | `true` | Enable or disable persistence for Grafana data. | +| lgtm.loki.distributor | map | `{"affinity":"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n","maxUnavailable":2,"replicas":3,"resources":{"limits":{"memory":"6Gi"},"requests":{"cpu":2,"memory":"4Gi"}}}` | Scaling and configuring loki distributor. | +| lgtm.loki.distributor.affinity | map | `"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n"` | Affinity rules for scheduling distributor pods. Passed in as a multiline string. | +| lgtm.loki.distributor.maxUnavailable | int | `2` | Maximum number of unavailable replicas allowed during an update. | +| lgtm.loki.distributor.replicas | int | `3` | Number of replicas for the distributor component. Determines how many instances to run. | +| lgtm.loki.distributor.resources.limits | map | `{"memory":"6Gi"}` | Resource limits for the distributor component. | +| lgtm.loki.distributor.resources.limits.memory | string | `"6Gi"` | Memory limit for the distributor pods. | +| lgtm.loki.distributor.resources.requests | map | `{"cpu":2,"memory":"4Gi"}` | Resource requests for the distributor component. | +| lgtm.loki.distributor.resources.requests.cpu | string | `2` | CPU request for the distributor pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.loki.distributor.resources.requests.memory | string | `"4Gi"` | Memory request for the distributor pods. Determines how much memory is guaranteed for the pod. | +| lgtm.loki.gateway.affinity | string | `"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n - key: topology.kubernetes.io/zone\n operator: In\n values:\n - us-east-1a\n"` | Affinity rules for scheduling gateway pods. Passed in as a multiline string. | +| lgtm.loki.gateway.ingress.annotations | object | `{}` | | +| lgtm.loki.gateway.ingress.enabled | bool | `true` | Enable or disable loki ingress. | +| lgtm.loki.gateway.ingress.hosts | list | `[{"host":"loki.example.com","paths":[{"path":"/","pathType":"Prefix"}]}]` | Hosts for loki ingress. | +| lgtm.loki.gateway.ingress.hosts[0] | string | `{"host":"loki.example.com","paths":[{"path":"/","pathType":"Prefix"}]}` | Hostname for loki ingress. | +| lgtm.loki.gateway.ingress.ingressClassName | string | `"alb"` | Class name for ingress. | +| lgtm.loki.ingester | map | `{"affinity":"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n","maxUnavailable":2,"persistentVolume":{"size":"50Gi"},"replicas":3,"resources":{"limits":{"memory":"12Gi"},"requests":{"cpu":3.5,"memory":"8Gi"}}}` | Scaling and configuring loki ingester. Passed in as a multiline string. | +| lgtm.loki.ingester.affinity | map | `"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n"` | Affinity rules for scheduling ingester pods. | +| lgtm.loki.ingester.maxUnavailable | int | `2` | Maximum number of unavailable replicas allowed during an update. | +| lgtm.loki.ingester.persistentVolume | map | `{"size":"50Gi"}` | Persistent volume configuration for the ingester component. | +| lgtm.loki.ingester.persistentVolume.size | string | `"50Gi"` | Size of the persistent volume to be used by the ingester. | +| lgtm.loki.ingester.replicas | int | `3` | Number of replicas for the ingester component. Determines how many instances to run. | +| lgtm.loki.ingester.resources.limits | map | `{"memory":"12Gi"}` | Resource limits for the ingester component. | +| lgtm.loki.ingester.resources.limits.memory | string | `"12Gi"` | Memory limit for the ingester pods. | +| lgtm.loki.ingester.resources.requests | map | `{"cpu":3.5,"memory":"8Gi"}` | Resource requests for the ingester component. | +| lgtm.loki.ingester.resources.requests.cpu | string | `3.5` | CPU request for the ingester pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.loki.ingester.resources.requests.memory | string | `"8Gi"` | Memory request for the ingester pods. Determines how much memory is guaranteed for the pod. | +| lgtm.loki.loki | map | `{"image":{"registry":"quay.io/cdis","repository":"loki","tag":"master"},"schemaConfig":{"configs":[{"from":"2024-04-01","index":{"period":"24h","prefix":"loki_index_"},"object_store":"s3","schema":"v13","store":"tsdb"}]},"structuredConfig":{"common":{"path_prefix":"/var/loki","storage":{"filesystem":null,"s3":{"region":"us-east-1"}}},"limits_config":{"max_entries_limit_per_query":100000000,"max_query_series":30000,"max_streams_per_user":100000},"server":{"log_level":"debug"}}}` | Loki configuration. | +| lgtm.loki.loki.image | map | `{"registry":"quay.io/cdis","repository":"loki","tag":"master"}` | Loki image details. | +| lgtm.loki.loki.image.registry | string | `"quay.io/cdis"` | Container image registry for Loki. | +| lgtm.loki.loki.image.repository | string | `"loki"` | Repository for the Loki image. | +| lgtm.loki.loki.image.tag | string | `"master"` | Tag for the Loki image version. | +| lgtm.loki.loki.schemaConfig | map | `{"configs":[{"from":"2024-04-01","index":{"period":"24h","prefix":"loki_index_"},"object_store":"s3","schema":"v13","store":"tsdb"}]}` | Schema configuration for Loki. | +| lgtm.loki.loki.schemaConfig.configs[0].index | map | `{"period":"24h","prefix":"loki_index_"}` | Index configuration for Loki. | +| lgtm.loki.loki.schemaConfig.configs[0].index.period | string | `"24h"` | Index rotation period for Loki, in hours. | +| lgtm.loki.loki.schemaConfig.configs[0].index.prefix | string | `"loki_index_"` | Prefix for the Loki index. | +| lgtm.loki.loki.schemaConfig.configs[0].object_store | string | `"s3"` | Object store for Loki data (e.g., S3). | +| lgtm.loki.loki.schemaConfig.configs[0].schema | string | `"v13"` | Schema version for Loki. | +| lgtm.loki.loki.schemaConfig.configs[0].store | string | `"tsdb"` | Storage engine used by Loki. | +| lgtm.loki.loki.structuredConfig | map | `{"common":{"path_prefix":"/var/loki","storage":{"filesystem":null,"s3":{"region":"us-east-1"}}},"limits_config":{"max_entries_limit_per_query":100000000,"max_query_series":30000,"max_streams_per_user":100000},"server":{"log_level":"debug"}}` | Structured configuration settings for Loki. | +| lgtm.loki.loki.structuredConfig.common.path_prefix | string | `"/var/loki"` | Path prefix where Loki stores data. | +| lgtm.loki.loki.structuredConfig.common.storage.filesystem | null | `nil` | Filesystem storage is disabled. | +| lgtm.loki.loki.structuredConfig.common.storage.s3.region | string | `"us-east-1"` | AWS region for S3 storage. | +| lgtm.loki.loki.structuredConfig.limits_config.max_entries_limit_per_query | int | `100000000` | Maximum number of log entries per query. | +| lgtm.loki.loki.structuredConfig.limits_config.max_query_series | int | `30000` | Maximum number of series that can be queried at once. | +| lgtm.loki.loki.structuredConfig.limits_config.max_streams_per_user | int | `100000` | Maximum number of streams a single user can have. | +| lgtm.loki.loki.structuredConfig.server.log_level | string | `"debug"` | Log level for Loki server. Options include 'info', 'debug', etc. | +| lgtm.loki.persistence | map | `{"enabled":true}` | Persistence settings for loki. | +| lgtm.loki.persistence.enabled | bool | `true` | Enable or disable persistence. | +| lgtm.loki.querier | map | `{"affinity":"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n","resources":{"limits":{"memory":"6Gi"},"requests":{"cpu":2,"memory":"4Gi"}}}` | Scaling and configuring loki querier. | +| lgtm.loki.querier.affinity | string | `"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n"` | Affinity rules for scheduling querier pods. Passed in as a multiline string. | +| lgtm.loki.querier.resources | map | `{"limits":{"memory":"6Gi"},"requests":{"cpu":2,"memory":"4Gi"}}` | Resource requests and limits for querier. | +| lgtm.loki.querier.resources.limits | map | `{"memory":"6Gi"}` | Resource limits for the querier component. | +| lgtm.loki.querier.resources.limits.memory | string | `"6Gi"` | Memory limit for the querier pods. | +| lgtm.loki.querier.resources.requests | map | `{"cpu":2,"memory":"4Gi"}` | Resource requests for the querier component. | +| lgtm.loki.querier.resources.requests.cpu | string | `2` | CPU request for the querier pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.loki.querier.resources.requests.memory | string | `"4Gi"` | Memory request for the querier pods. Determines how much memory is guaranteed for the pod. | +| lgtm.loki.queryFrontend | map | `{"affinity":"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n","resources":{"limits":{"memory":"6Gi"},"requests":{"cpu":2,"memory":"4Gi"}}}` | Scaling and configuring loki queryFrontend. | +| lgtm.loki.queryFrontend.affinity | map | `"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n"` | Affinity rules for scheduling queryFrontend pods. Passed in as a multiline string. | +| lgtm.loki.queryFrontend.resources | map | `{"limits":{"memory":"6Gi"},"requests":{"cpu":2,"memory":"4Gi"}}` | Resource requests and limits for queryFrontend. | +| lgtm.loki.queryFrontend.resources.limits | map | `{"memory":"6Gi"}` | Resource limits for the queryFrontend component. | +| lgtm.loki.queryFrontend.resources.limits.memory | string | `"6Gi"` | Memory limit for the queryFrontend pods. | +| lgtm.loki.queryFrontend.resources.requests | map | `{"cpu":2,"memory":"4Gi"}` | Resource requests for the queryFrontend component. | +| lgtm.loki.queryFrontend.resources.requests.cpu | string | `2` | CPU request for the queryFrontend pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.loki.queryFrontend.resources.requests.memory | string | `"4Gi"` | Memory request for the queryFrontend pods. Determines how much memory is guaranteed for the pod. | +| lgtm.loki.serviceAccount | string | `{"name":"observability"}` | Service account configuration for loki. | +| lgtm.loki.serviceAccount.name | string | `"observability"` | Service account to use (will be created by default via this helm chart). | +| lgtm.mimir.alertmanager.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling Alertmanager pods. | +| lgtm.mimir.alertmanager.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.alertmanager.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.alertmanager.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.alertmanager.persistentVolume | map | `{"enabled":true}` | Configuration for persistent volume in Alertmanager. | +| lgtm.mimir.alertmanager.persistentVolume.enabled | bool | `true` | Enable or disable the persistent volume for Alertmanager. Set to 'true' to enable, 'false' to disable. | +| lgtm.mimir.alertmanager.replicas | int | `3` | Number of replicas for Alertmanager. Determines how many instances of Alertmanager to run. | +| lgtm.mimir.alertmanager.resources.limits | map | `{"memory":"2Gi"}` | Resource limits for Alertmanager pods. | +| lgtm.mimir.alertmanager.resources.limits.memory | string | `"2Gi"` | Memory limit for Alertmanager pods. | +| lgtm.mimir.alertmanager.resources.requests | map | `{"cpu":1,"memory":"1Gi"}` | Resource requests for Alertmanager pods. | +| lgtm.mimir.alertmanager.resources.requests.cpu | string | `1` | CPU request for Alertmanager pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.alertmanager.resources.requests.memory | string | `"1Gi"` | Memory request for Alertmanager pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.alertmanager.statefulSet | map | `{"enabled":true}` | Configuration for deploying Alertmanager as a StatefulSet. | +| lgtm.mimir.alertmanager.statefulSet.enabled | bool | `true` | Enable or disable the StatefulSet deployment for Alertmanager. Set to 'true' to enable, 'false' to disable. | +| lgtm.mimir.compactor.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling compactor pods. | +| lgtm.mimir.compactor.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.compactor.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.compactor.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.compactor.persistentVolume | map | `{"size":"50Gi"}` | Persistent volume configuration for the compactor component. | +| lgtm.mimir.compactor.persistentVolume.size | string | `"50Gi"` | Size of the persistent volume to be used by the compactor. | +| lgtm.mimir.compactor.resources.limits | map | `{"memory":"3Gi"}` | Resource limits for the compactor component. | +| lgtm.mimir.compactor.resources.limits.memory | string | `"3Gi"` | Memory limit for the compactor pods. | +| lgtm.mimir.compactor.resources.requests | map | `{"cpu":1,"memory":"2Gi"}` | Resource requests for the compactor component. | +| lgtm.mimir.compactor.resources.requests.cpu | string | `1` | CPU request for the compactor pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.compactor.resources.requests.memory | string | `"2Gi"` | Memory request for the compactor pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.distributor.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling distributor pods. | +| lgtm.mimir.distributor.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.distributor.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.distributor.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.distributor.replicas | int | `3` | Number of replicas for the distributor component. Determines how many instances to run. | +| lgtm.mimir.distributor.resources.limits | map | `{"memory":"12Gi"}` | Resource limits for the distributor component. | +| lgtm.mimir.distributor.resources.limits.memory | string | `"12Gi"` | Memory limit for the distributor pods. | +| lgtm.mimir.distributor.resources.requests | map | `{"cpu":2,"memory":"8Gi"}` | Resource requests for the distributor component. | +| lgtm.mimir.distributor.resources.requests.cpu | string | `2` | CPU request for the distributor pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.distributor.resources.requests.memory | string | `"8Gi"` | Memory request for the distributor pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.gateway.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling gateway pods. | +| lgtm.mimir.gateway.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.gateway.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.gateway.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.gateway.replicas | int | `3` | Number of replicas for the gateway component. Determines how many instances to run. | +| lgtm.mimir.gateway.resources.limits | map | `{"memory":"731Mi"}` | Resource limits for the gateway component. | +| lgtm.mimir.gateway.resources.limits.memory | string | `"731Mi"` | Memory limit for the gateway pods. | +| lgtm.mimir.gateway.resources.requests | map | `{"cpu":1,"memory":"512Mi"}` | Resource requests for the gateway component. | +| lgtm.mimir.gateway.resources.requests.cpu | string | `1` | CPU request for the gateway pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.gateway.resources.requests.memory | string | `"512Mi"` | Memory request for the gateway pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.image | map | `{"repository":"quay.io/cdis/mimir","tag":"master"}` | Docker image information. | +| lgtm.mimir.image.repository | string | `"quay.io/cdis/mimir"` | The Docker image repository for mimir. | +| lgtm.mimir.ingester.affinity.nodeAffinity | map | `{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}` | Affinity rules for scheduling ingester pods. | +| lgtm.mimir.ingester.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.ingester.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.ingester.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.ingester.persistentVolume | map | `{"size":"50Gi"}` | Persistent volume configuration for the ingester component. | +| lgtm.mimir.ingester.persistentVolume.size | string | `"50Gi"` | Size of the persistent volume to be used by the ingester. | +| lgtm.mimir.ingester.replicas | int | `5` | Number of replicas for the ingester component. Determines how many instances to run. | +| lgtm.mimir.ingester.resources.limits | map | `{"memory":"12Gi"}` | Resource limits for the ingester component. | +| lgtm.mimir.ingester.resources.limits.memory | string | `"12Gi"` | Memory limit for the ingester pods. | +| lgtm.mimir.ingester.resources.requests | map | `{"cpu":3.5,"memory":"8Gi"}` | Resource requests for the ingester component. | +| lgtm.mimir.ingester.resources.requests.cpu | string | `3.5` | CPU request for the ingester pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.ingester.resources.requests.memory | string | `"8Gi"` | Memory request for the ingester pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.ingester.topologySpreadConstraints | map | `{}` | Topology spread constraints for the ingester component. Empty by default. | +| lgtm.mimir.ingester.zoneAwareReplication | map | `{"topologyKey":"kubernetes.io/hostname"}` | Zone-aware replication settings. Helps distribute data across zones. | +| lgtm.mimir.ingester.zoneAwareReplication.topologyKey | string | `"kubernetes.io/hostname"` | Topology key used for zone-aware replication. | +| lgtm.mimir.ingress.annotations | object | `{}` | | +| lgtm.mimir.ingress.enabled | bool | `true` | Enable or disable mirmir ingress. | +| lgtm.mimir.ingress.hosts | list | `["mimir.example.com"]` | hostname for mimir ingress. | +| lgtm.mimir.ingress.ingressClassName | string | `"alb"` | Class name for ingress. | +| lgtm.mimir.ingress.paths | map | `{"query-frontend":[{"path":"/prometheus/api/v1/query"}]}` | Additional paths to add to the ingress. | +| lgtm.mimir.ingress.paths.query-frontend | list | `[{"path":"/prometheus/api/v1/query"}]` | Additional paths to add to the query frontend. | +| lgtm.mimir.mimir.structuredConfig | map | `{"alertmanager_storage":{"storage_prefix":"alertmanager"},"blocks_storage":{"storage_prefix":"blocks"},"common":{"storage":{"backend":"s3","s3":{"endpoint":"s3.us-east-1.amazonaws.com","region":"us-east-1"}}},"limits":{"ingestion_rate":10000000,"max_global_series_per_user":0},"query_scheduler":{"service_discovery_mode":"dns"},"ruler_storage":{"storage_prefix":"ruler"}}` | Structured configuration settings for mimir. | +| lgtm.mimir.mimir.structuredConfig.alertmanager_storage.storage_prefix | string | `"alertmanager"` | Prefix used for storing Alertmanager data. | +| lgtm.mimir.mimir.structuredConfig.blocks_storage.storage_prefix | string | `"blocks"` | Prefix used for storing blocks data. | +| lgtm.mimir.mimir.structuredConfig.common.storage.backend | string | `"s3"` | Backend storage configuration. For example, s3 for AWS S3 storage. | +| lgtm.mimir.mimir.structuredConfig.common.storage.s3.endpoint | string | `"s3.us-east-1.amazonaws.com"` | The S3 endpoint to use for storage. Ensure this matches your region. | +| lgtm.mimir.mimir.structuredConfig.common.storage.s3.region | string | `"us-east-1"` | AWS region where your S3 bucket is located. | +| lgtm.mimir.mimir.structuredConfig.limits.ingestion_rate | int | `10000000` | The rate limit for ingestion, measured in samples per second. | +| lgtm.mimir.mimir.structuredConfig.limits.max_global_series_per_user | int | `0` | Maximum number of global series allowed per user. Set to '0' for unlimited. | +| lgtm.mimir.mimir.structuredConfig.query_scheduler.service_discovery_mode | string | `"dns"` | Mode for service discovery in the query scheduler. Set to 'dns' for DNS-based service discovery. | +| lgtm.mimir.mimir.structuredConfig.ruler_storage.storage_prefix | string | `"ruler"` | Prefix used for storing ruler data. | +| lgtm.mimir.minio | map | `{"enabled":false}` | minio configuration. | +| lgtm.mimir.minio.enabled | bool | `false` | Enable or disable minio. | +| lgtm.mimir.nginx.affinity | string | `"nodeAffinity:\n requiredDuringSchedulingIgnoredDuringExecution:\n nodeSelectorTerms:\n - matchExpressions:\n # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone.\n - key: topology.kubernetes.io/zone\n # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values.\n operator: In\n # -- (list) List of values for the node selector, representing allowed zones.\n values:\n - us-east-1a\n"` | Affinity rules for scheduling nginx pods. Passed in as a multiline string. | +| lgtm.mimir.nginx.image.registry | string | `"quay.io/nginx"` | Container image registry for nginx. | +| lgtm.mimir.nginx.image.repository | string | `"nginx-unprivileged"` | Repository for nginx unprivileged image. | +| lgtm.mimir.nginx.replicas | int | `3` | Number of replicas for the nginx component. Determines how many instances to run. | +| lgtm.mimir.nginx.resources.limits | map | `{"memory":"731Mi"}` | Resource limits for the nginx component. | +| lgtm.mimir.nginx.resources.limits.memory | string | `"731Mi"` | Memory limit for the nginx pods. | +| lgtm.mimir.nginx.resources.requests | map | `{"cpu":1,"memory":"512Mi"}` | Resource requests for the nginx component. | +| lgtm.mimir.nginx.resources.requests.cpu | string | `1` | CPU request for the nginx pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.nginx.resources.requests.memory | string | `"512Mi"` | Memory request for the nginx pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.overrides_exporter.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling overrides_exporter pods. | +| lgtm.mimir.overrides_exporter.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.overrides_exporter.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.overrides_exporter.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.overrides_exporter.replicas | int | `1` | Number of replicas for the overrides_exporter component. Determines how many instances to run. | +| lgtm.mimir.overrides_exporter.resources.limits | map | `{"memory":"128Mi"}` | Resource limits for the overrides_exporter component. | +| lgtm.mimir.overrides_exporter.resources.limits.memory | string | `"128Mi"` | Memory limit for the overrides_exporter pods. | +| lgtm.mimir.overrides_exporter.resources.requests | map | `{"cpu":"100m","memory":"128Mi"}` | Resource requests for the overrides_exporter component. | +| lgtm.mimir.overrides_exporter.resources.requests.cpu | string | `"100m"` | CPU request for the overrides_exporter pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.overrides_exporter.resources.requests.memory | string | `"128Mi"` | Memory request for the overrides_exporter pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.querier.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling querier pods. | +| lgtm.mimir.querier.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.querier.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.querier.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.querier.replicas | int | `3` | Number of replicas for the querier component. Determines how many instances to run. | +| lgtm.mimir.querier.resources.limits | map | `{"memory":"8Gi"}` | Resource limits for the querier component. | +| lgtm.mimir.querier.resources.limits.memory | string | `"8Gi"` | Memory limit for the querier pods. | +| lgtm.mimir.querier.resources.requests | map | `{"cpu":2,"memory":"6Gi"}` | Resource requests for the querier component. | +| lgtm.mimir.querier.resources.requests.cpu | string | `2` | CPU request for the querier pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.querier.resources.requests.memory | string | `"6Gi"` | Memory request for the querier pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.query_frontend.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling query_frontend pods. | +| lgtm.mimir.query_frontend.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.query_frontend.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.query_frontend.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.query_frontend.replicas | int | `2` | Number of replicas for the query_frontend component. Determines how many instances to run. | +| lgtm.mimir.query_frontend.resources.limits | map | `{"memory":"3Gi"}` | Resource limits for the query_frontend component. | +| lgtm.mimir.query_frontend.resources.limits.memory | string | `"3Gi"` | Memory limit for the query_frontend pods. | +| lgtm.mimir.query_frontend.resources.requests | map | `{"cpu":2,"memory":"2Gi"}` | Resource requests for the query_frontend component. | +| lgtm.mimir.query_frontend.resources.requests.cpu | string | `2` | CPU request for the query_frontend pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.query_frontend.resources.requests.memory | string | `"2Gi"` | Memory request for the query_frontend pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.query_scheduler.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling query_scheduler pods. | +| lgtm.mimir.query_scheduler.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.query_scheduler.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.query_scheduler.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.rollout_operator | map | `{"image":{"repository":"quay.io/cdis/rollout-operator","tag":"master"}}` | Rollout Operator configuration. | +| lgtm.mimir.rollout_operator.image | map | `{"repository":"quay.io/cdis/rollout-operator","tag":"master"}` | Docker image information. | +| lgtm.mimir.rollout_operator.image.repository | string | `"quay.io/cdis/rollout-operator"` | The Docker image repository for the rollout-operator. | +| lgtm.mimir.ruler.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling ruler pods. | +| lgtm.mimir.ruler.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.ruler.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.ruler.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.ruler.replicas | int | `2` | Number of replicas for the ruler component. Determines how many instances to run. | +| lgtm.mimir.ruler.resources.limits | map | `{"memory":"5Gi"}` | Resource limits for the ruler component. | +| lgtm.mimir.ruler.resources.limits.memory | string | `"5Gi"` | Memory limit for the ruler pods. | +| lgtm.mimir.ruler.resources.requests | map | `{"cpu":1,"memory":"4Gi"}` | Resource requests for the ruler component. | +| lgtm.mimir.ruler.resources.requests.cpu | string | `1` | CPU request for the ruler pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.ruler.resources.requests.memory | string | `"4Gi"` | Memory request for the ruler pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.serviceAccount.create | bool | `false` | Whether to create a service account or not. In case 'create' is false, do set 'name' to an existing service account name. The "observability" SA will be created by default via Helm. | +| lgtm.mimir.serviceAccount.name | string | `"observability"` | Override for the generated service account name. | +| lgtm.mimir.store_gateway.affinity | map | `{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}]}]}}}` | Affinity rules for scheduling store_gateway pods. | +| lgtm.mimir.store_gateway.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0] | string | `{"key":"topology.kubernetes.io/zone","operator":"In","values":["us-east-1a"]}` | Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. | +| lgtm.mimir.store_gateway.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].operator | string | `"In"` | Operator to apply to the node selector. 'In' means the node must match one of the values. | +| lgtm.mimir.store_gateway.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values | list | `["us-east-1a"]` | List of values for the node selector, representing allowed zones. | +| lgtm.mimir.store_gateway.persistentVolume | map | `{"size":"50Gi"}` | Persistent volume configuration for the store_gateway component. | +| lgtm.mimir.store_gateway.persistentVolume.size | string | `"50Gi"` | Size of the persistent volume to be used by the store_gateway. | +| lgtm.mimir.store_gateway.replicas | int | `2` | Number of replicas for the store_gateway component. Determines how many instances to run. | +| lgtm.mimir.store_gateway.resources.limits | map | `{"memory":"8Gi"}` | Resource limits for the store_gateway component. | +| lgtm.mimir.store_gateway.resources.limits.memory | string | `"8Gi"` | Memory limit for the store_gateway pods. | +| lgtm.mimir.store_gateway.resources.requests | map | `{"cpu":1,"memory":"6Gi"}` | Resource requests for the store_gateway component. | +| lgtm.mimir.store_gateway.resources.requests.cpu | string | `1` | CPU request for the store_gateway pods. Determines how much CPU is guaranteed for the pod. | +| lgtm.mimir.store_gateway.resources.requests.memory | string | `"6Gi"` | Memory request for the store_gateway pods. Determines how much memory is guaranteed for the pod. | +| lgtm.mimir.store_gateway.topologySpreadConstraints | map | `{}` | Topology spread constraints for the store_gateway component. Empty by default. | +| lgtm.mimir.store_gateway.zoneAwareReplication | map | `{"topologyKey":"kubernetes.io/hostname"}` | Zone-aware replication settings. Helps distribute data across zones. | +| lgtm.mimir.store_gateway.zoneAwareReplication.topologyKey | string | `"kubernetes.io/hostname"` | Topology key used for zone-aware replication. | +| lgtm.role.arn | string | `nil` | The arn of the aws role to associate with the service account that will be used for Loki and Mimir. Documentation on IRSA setup https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html | +| lgtm.tempo.enabled | bool | `false` | Enable or disable tempo. | diff --git a/helm/observability/SETUP.md b/helm/observability/SETUP.md new file mode 100644 index 00000000..d9c84977 --- /dev/null +++ b/helm/observability/SETUP.md @@ -0,0 +1,309 @@ +# Observability Helm Chart + +## Overview + +The Observability Helm chart provides an all-in-one solution for deploying Mimir, Loki, and Grafana to your Kubernetes cluster, enabling a complete observability stack for metrics, logs, and visualization. + +### Grafana: +A leading open-source platform for data visualization and monitoring. Grafana allows you to create rich, interactive dashboards from a variety of data sources, making it easy to analyze metrics and logs from your systems. + +### Mimir: +Grafana Mimir is a highly scalable time-series database optimized for storing and querying metrics. It enables powerful alerting and querying for real-time monitoring of your infrastructure and applications. + +### Loki: +Grafana Loki is a log aggregation system designed to efficiently collect, store, and query logs from your applications. It works seamlessly with Grafana, providing an integrated way to visualize logs alongside metrics. + +By deploying this Helm chart, you'll set up these three components together, allowing you to monitor your systems and applications comprehensively with metrics from Mimir, logs from Loki, and dashboards and alerts in Grafana. + +### Alloy: +Grafana Alloy is a powerful observability tool that collects and ships logs and metrics from your services to Grafana Loki and Mimir for storage and analysis. + +***Note: Grafana is deployed in a separate Helm Chart. You will need to follow the instructions outlined in [Alloy Chart](../alloy/SETUP.md) after completing the following guide. + +### Faro Collector (Alloy): +Alloy Faro Collector is a specialized configuration of Alloy that enables it to gather Real User Monitoring (RUM) data from Portal through Grafana Faro. In this role, Alloy acts as an ingestion point for RUM data. + +***Note: The Faro Collector is deployed in a separate Helm Chart. You will need to follow the instructions outlined in [faro.md](../faro-collector/SETUP.md) after completing the following guide. + +## General Architecture + +In this setup, Loki and Mimir are configured with internal ingress resources, enabling Alloy to send metrics and logs securely via VPC peering connections. Both Loki and Mimir write the ingested data to Amazon S3 for scalable and durable storage. This data can be queried and visualized through Grafana, which is hosted behind an internet-facing ingress. Access to Grafana can be restricted using CIDR ranges defined through the ALB ingress annotation: alb.ingress.kubernetes.io/inbound-cidrs: "cidrs". Additionally, the chart supports SAML authentication for Grafana, configured through the grafana.ini field, ensuring secure user access. + +![Grafana Architecture](Grafana.png) + +### Fips compliant images + +Gen3 provides FIPS-compliant images, which are set as the default in the values file for Grafana, Mimir, and Loki. These images are self-hosted and maintained by the Gen3 Platform Team, ensuring secure and compliant operations. The Platform Team is responsible for managing image upgrades, and service versions will be updated as deemed necessary by the team. + +### Helm Chart Links +The links below will take you to the Grafana LGTM chart, as well as the Grafana, Loki, and Mimir charts, providing a comprehensive list of configurable options to help you further customize your setup. +#### Link to lgtm Helm chart +- [LGTM Helm Chart](https://github.com/grafana/helm-charts/tree/main/charts/lgtm-distributed) +#### Full Configuration Options for all Components +- [Grafana](https://github.com/grafana/helm-charts/blob/main/charts/grafana/values.yaml) +- [Loki](https://github.com/grafana/helm-charts/blob/main/charts/loki-distributed/values.yaml) +- [Mimir](https://github.com/grafana/mimir/blob/main/operations/helm/charts/mimir-distributed/values.yaml) + +### Affinity Rules + +The affinity rule in the values.yaml file controls pod scheduling to specific nodes or zones. By default, pods are restricted to nodes in us-east-1a using a node label (topology.kubernetes.io/zone). + +Customize these rules to align with your cluster’s zones or labels to ensure pods can schedule properly. Mismatched configurations can lead to scheduling failures. + +```yaml + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a +``` + +### IRSA Role Setup + +This Helm chart automatically creates a service account named "observability" for use with Loki and Mimir. To ensure proper access to the storage buckets holding Loki and Mimir data, you’ll need to associate an AWS IAM Role with this service account. Configure the role with the necessary permissions to access the relevant S3 buckets, and then provide the role’s ARN in the appropriate section of your values.yaml file. + +```yaml +lgtm: + # -- (map) Configuration for IRSA role to use with service accounts. + role: + # -- (string) The arn of the aws role to associate with the service account that will be used for Loki and Mimir. + # Documentation on IRSA setup https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html + arn: +``` + +## Configuring Grafana + +When configuring the Grafana, you will need to update the hosts section of the values.yaml file to match the hostname you plan to use. For example, replace "grafana.example.com" with your desired hostname. + +### Ingress + +Grafana will require an internet-facing ingress in order to access the visualizations, alerts, etc. It is highly recommended that you uncomment and adjust the annotations provided for AWS ALB (Application Load Balancer) to fit your environment (if deploying via AWS). These annotations will help ensure proper configuration of the load balancer, SSL certificates, and other key settings. For instance, make sure to replace the placeholder values such as "cert arn", "ssl policy", and "environment name" with your specific details. Access to Grafana can be restricted using CIDR ranges defined through the ALB ingress annotation: alb.ingress.kubernetes.io/inbound-cidrs: "cidrs". + +```yaml +grafana: + ingress: + # -- (bool) Enable or disable ingress for Grafana. + enabled: true + # -- (map) Annotations for Grafana ingress. + annotations: + annotations: {} + ## Recommended annotations for AWS ALB (Application Load Balancer). + # alb.ingress.kubernetes.io/ssl-redirect: '443' + # alb.ingress.kubernetes.io/certificate-arn: + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + # alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + # alb.ingress.kubernetes.io/scheme: internet-facing + # alb.ingress.kubernetes.io/ssl-policy: + # alb.ingress.kubernetes.io/tags: Environment= + # alb.ingress.kubernetes.io/target-type: 'ip' + # alb.ingress.kubernetes.io/inbound-cidrs: + # -- (list) Hostname(s) for Grafana ingress. + hosts: + - grafana.example.com + # -- (string) Ingress class name to be used (e.g., 'alb' for AWS Application Load Balancer). + ingressClassName: "alb" +``` + +### Built-in Gen3 Alerts + +This Helm chart comes equipped with built-in Gen3 alerts, defined in the 'alerting' section of the values.yaml. These alerts enable you to immediately leverage your logs and metrics as soon as Grafana is up and running. + +### Built-in Gen3 Dashboards + +You can utilize Gen3-specific visualizations by visiting our [grafana-dashboards repo](https://github.com/uc-cdis/grafana-dashboards). + +## Configuring Mimir + +When configuring the Mimir, you will need to update the hosts section of the values.yaml file to match the hostname you plan to use. For example, replace "mimir.example.com" with your desired hostname. + +### Ingress + +Mimir will require an internal ingress in order to access the visualizations, alerts, etc. It is highly recommended that you uncomment and adjust the annotations provided for AWS ALB (Application Load Balancer) to fit your environment (if deploying via AWS). These annotations will help ensure proper configuration of the load balancer, SSL certificates, and other key settings. For instance, make sure to replace the placeholder values such as "cert arn", "ssl policy", and "environment name" with your specific details. + +```yaml +mimir: + ingress: + # -- (map) Annotations to add to mimir ingress. + annotations: {} + ## Recommended annotations for AWS ALB (Application Load Balancer). + # alb.ingress.kubernetes.io/certificate-arn: + # alb.ingress.kubernetes.io/ssl-redirect: '443' + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + # alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + # alb.ingress.kubernetes.io/scheme: internal + # alb.ingress.kubernetes.io/ssl-policy: + # alb.ingress.kubernetes.io/tags: Environment= + # alb.ingress.kubernetes.io/target-type: ip + # -- (bool) Enable or disable mirmir ingress. + enabled: true + # -- (string) Class name for ingress. + ingressClassName: "alb" + # -- (map) Additional paths to add to the ingress. + paths: + # -- (list) Additional paths to add to the query frontend. + query-frontend: + - path: /prometheus/api/v1/query + # -- (list) hostname for mimir ingress. + hosts: + - mimir.example.com +``` + +### Storage Configuration + +The structuredConfig section in Mimir’s configuration defines how backend storage is set up to persist metrics and time-series data. This configuration ensures that data is safely stored and retrievable over time, even if Mimir instances restart or scale. + +If you are utilizing Amazon S3 for storage, make sure to uncomment "bucket_name" and input a value. + +```yaml +mimir: + # -- (map) Structured configuration settings for mimir. + structuredConfig: + common: + storage: + # -- (string) Backend storage configuration. For example, s3 for AWS S3 storage. + backend: s3 + s3: + # -- (string) The S3 endpoint to use for storage. Ensure this matches your region. + endpoint: s3.us-east-1.amazonaws.com + # -- (string) AWS region where your S3 bucket is located. + region: us-east-1 + # # -- (string) Name of the S3 bucket used for storage. + # bucket_name: +``` + +### Mimir Components +Mimir is a high-performance time-series database, typically used for storing and querying metrics. +1. **Alertmanager** + - **Pods**: `grafana-mimir-alertmanager-*` + - **Purpose**: Manages alert notifications and routing. + - **Function**: Sends alerts to different channels like email, Slack, etc., based on defined rules. + +2. **Compactor** + - **Pods**: `grafana-mimir-compactor-*` + - **Purpose**: Compacts time-series data to optimize storage. + - **Function**: Periodically reduces the size of stored metrics by merging smaller chunks. + +3. **Distributor** + - **Pods**: `grafana-mimir-distributor-*` + - **Purpose**: Accepts incoming metric data and distributes it to ingesters. + - **Function**: Acts as a load balancer for metric ingestion. + +4. **Ingester** + - **Pods**: `grafana-mimir-ingester-*` + - **Purpose**: Temporarily holds and processes incoming metric data. + - **Function**: Ingesters store time-series data in memory before flushing to long-term storage. + +5. **Querier** + - **Pods**: `grafana-mimir-querier-*` + - **Purpose**: Handles metric queries. + - **Function**: Retrieves time-series data from ingesters and long-term storage for queries. + +6. **Query Frontend** + - **Pods**: `grafana-mimir-query-frontend-*` + - **Purpose**: Coordinates and optimizes query execution. + - **Function**: Distributes query workloads to ensure performance and efficiency. + +7. **Query Scheduler** + - **Pods**: `grafana-mimir-query-scheduler-*` + - **Purpose**: Schedules query jobs across queriers. + - **Function**: Ensures balanced query processing across components. + +8. **Ruler** + - **Pods**: `grafana-mimir-ruler-*` + - **Purpose**: Evaluates recording and alerting rules. + - **Function**: Generates time-series data or alerts based on predefined rules. + +9. **Store Gateway** + - **Pods**: `grafana-mimir-store-gateway-*` + - **Purpose**: Provides access to long-term storage. + - **Function**: Optimizes retrieval of historical data from object stores. + +## Configuring Loki + + +When configuring the Loki, you will need to update the hosts section of the values.yaml file to match the hostname you plan to use. For example, replace "loki.example.com" with your desired hostname. + +### Ingress + +Loki will require an internal ingress in order to access the visualizations, alerts, etc. It is highly recommended that you uncomment and adjust the annotations provided for AWS ALB (Application Load Balancer) to fit your environment (if deploying via AWS). These annotations will help ensure proper configuration of the load balancer, SSL certificates, and other key settings. For instance, make sure to replace the placeholder values such as "cert arn", "ssl policy", and "environment name" with your specific details. + +```yaml +loki: + ingress: + # -- (map) Annotations to add to loki ingress. + annotations: {} + ## Recommended annotations for AWS ALB (Application Load Balancer). + # alb.ingress.kubernetes.io/certificate-arn: + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + # alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + # alb.ingress.kubernetes.io/scheme: internal + # alb.ingress.kubernetes.io/ssl-policy: + # alb.ingress.kubernetes.io/ssl-redirect: '443' + # alb.ingress.kubernetes.io/tags: Environment= + # alb.ingress.kubernetes.io/target-type: ip + # -- (bool) Enable or disable loki ingress. + enabled: true + # -- (string) Class name for ingress. + ingressClassName: "alb" + # -- (list) Hosts for loki ingress. + hosts: + # -- (string) Hostname for loki ingress. + - host: loki.example.com +``` + +### Storage Configuration + +The structuredConfig section in Loki’s configuration defines how backend storage is set up to persist log data. This configuration ensures that logs are safely stored and retrievable over time, even if Loki instances restart or scale. + +If you are utilizing Amazon S3 for storage, make sure to uncomment "bucketnames" and input a value. + +```yaml +loki: + # -- (map) Structured configuration settings for Loki. + structuredConfig: + common: + # -- (string) Path prefix where Loki stores data. + path_prefix: /var/loki + storage: + # -- (null) Filesystem storage is disabled. + filesystem: null + s3: + # -- (string) AWS region for S3 storage. + region: us-east-1 + # # -- (string) S3 bucket names for Loki storage. + # bucketnames: +``` + +### Loki Components +Loki is used for log aggregation, querying, and management. Each Loki component has a specialized role in the log pipeline. +1. **Distributor** + - **Pods**: `grafana-loki-distributor-*` + - **Purpose**: Accepts log entries and forwards them to ingesters. + - **Function**: It load-balances logs from sources and ensures efficient distribution to ingesters. + +2. **Gateway** + - **Pods**: `grafana-loki-gateway-*` + - **Purpose**: Acts as an API gateway or entry point for requests. + - **Function**: Can be used for proxying queries to the appropriate backend components. + +3. **Ingester** + - **Pods**: `grafana-loki-ingester-*` + - **Purpose**: Receives and stores log entries in chunks. + - **Function**: Ingesters temporarily hold logs in memory and periodically flush them to storage (like S3 or other object stores). + +4. **Querier** + - **Pods**: `grafana-loki-querier-*` + - **Purpose**: Handles log queries from users. + - **Function**: Retrieves logs from ingesters and long-term storage for querying purposes. + +5. **Query Frontend** + - **Pods**: `grafana-loki-query-frontend-*` + - **Purpose**: Distributes and coordinates queries. + - **Function**: Splits large queries into smaller ones for faster execution by the queriers. diff --git a/helm/observability/templates/observability-sa.yaml b/helm/observability/templates/observability-sa.yaml new file mode 100644 index 00000000..14c97409 --- /dev/null +++ b/helm/observability/templates/observability-sa.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + annotations: + eks.amazonaws.com/role-arn: {{ .Values.lgtm.role.arn | quote }} + name: observability \ No newline at end of file diff --git a/helm/observability/values.yaml b/helm/observability/values.yaml new file mode 100644 index 00000000..a40f8eea --- /dev/null +++ b/helm/observability/values.yaml @@ -0,0 +1,1108 @@ +--- +lgtm: + # -- (map) Configuration for IRSA role to use with service accounts. + role: + # -- (string) The arn of the aws role to associate with the service account that will be used for Loki and Mimir. + # Documentation on IRSA setup https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html + arn: + + # -- (map) Tempo configuration (currently disabled). + tempo: + # -- (bool) Enable or disable tempo. + enabled: false + + # -- (map) Mimir configuration. + mimir: + # -- (map) Docker image information. + image: + # -- (string) The Docker image repository for mimir. + repository: quay.io/cdis/mimir + # -- (string) The Docker image tag for the mimir. + tag: master + # -- (map) Mimir ingress configuration. + ingress: + # -- (map) Annotations to add to mimir ingress. + annotations: {} + ## Recommended annotations for AWS ALB (Application Load Balancer). + # alb.ingress.kubernetes.io/certificate-arn: + # alb.ingress.kubernetes.io/ssl-redirect: '443' + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + # alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + # alb.ingress.kubernetes.io/scheme: internal + # alb.ingress.kubernetes.io/ssl-policy: + # alb.ingress.kubernetes.io/tags: Environment= + # alb.ingress.kubernetes.io/target-type: ip + # -- (bool) Enable or disable mirmir ingress. + enabled: true + # -- (string) Class name for ingress. + ingressClassName: "alb" + # -- (map) Additional paths to add to the ingress. + paths: + # -- (list) Additional paths to add to the query frontend. + query-frontend: + - path: /prometheus/api/v1/query + # -- (list) hostname for mimir ingress. + hosts: + - mimir.example.com + + # -- (map) minio configuration. + minio: + # -- (bool) Enable or disable minio. + enabled: false + + # -- (map) Rollout Operator configuration. + rollout_operator: + # -- (map) Docker image information. + image: + # -- (string) The Docker image repository for the rollout-operator. + repository: quay.io/cdis/rollout-operator + # -- (string) The Docker image tag for the rollout-operator. + tag: master + serviceAccount: + # -- (bool) Whether to create a service account or not. In case 'create' is false, do set 'name' to an existing service account name. The "observability" SA will be created by default via Helm. + create: false + # -- (string) Override for the generated service account name. + name: observability + + mimir: + # -- (map) Structured configuration settings for mimir. + structuredConfig: + limits: + # -- (int) Maximum number of global series allowed per user. Set to '0' for unlimited. + max_global_series_per_user: 0 + # -- (int) The rate limit for ingestion, measured in samples per second. + ingestion_rate: 10000000 + common: + storage: + # -- (string) Backend storage configuration. For example, s3 for AWS S3 storage. + backend: s3 + s3: + # -- (string) The S3 endpoint to use for storage. Ensure this matches your region. + endpoint: s3.us-east-1.amazonaws.com + # -- (string) AWS region where your S3 bucket is located. + region: us-east-1 + # # -- (string) Name of the S3 bucket used for storage. + # bucket_name: + blocks_storage: + # -- (string) Prefix used for storing blocks data. + storage_prefix: blocks + alertmanager_storage: + # -- (string) Prefix used for storing Alertmanager data. + storage_prefix: alertmanager + ruler_storage: + # -- (string) Prefix used for storing ruler data. + storage_prefix: ruler + query_scheduler: + # -- (string) Mode for service discovery in the query scheduler. Set to 'dns' for DNS-based service discovery. + service_discovery_mode: "dns" + + alertmanager: + # -- (map) Configuration for persistent volume in Alertmanager. + persistentVolume: + # -- (bool) Enable or disable the persistent volume for Alertmanager. Set to 'true' to enable, 'false' to disable. + enabled: true + # -- (int) Number of replicas for Alertmanager. Determines how many instances of Alertmanager to run. + replicas: 3 + # -- (map) Affinity rules for scheduling Alertmanager pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + resources: + # -- (map) Resource limits for Alertmanager pods. + limits: + # -- (string) Memory limit for Alertmanager pods. + memory: 2Gi + # -- (map) Resource requests for Alertmanager pods. + requests: + # -- (string) CPU request for Alertmanager pods. Determines how much CPU is guaranteed for the pod. + cpu: 1 + # -- (string) Memory request for Alertmanager pods. Determines how much memory is guaranteed for the pod. + memory: 1Gi + # -- (map) Configuration for deploying Alertmanager as a StatefulSet. + statefulSet: + # -- (bool) Enable or disable the StatefulSet deployment for Alertmanager. Set to 'true' to enable, 'false' to disable. + enabled: true + + compactor: + # -- (map) Affinity rules for scheduling compactor pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (map) Persistent volume configuration for the compactor component. + persistentVolume: + # -- (string) Size of the persistent volume to be used by the compactor. + size: 50Gi + resources: + # -- (map) Resource limits for the compactor component. + limits: + # -- (string) Memory limit for the compactor pods. + memory: 3Gi + # -- (map) Resource requests for the compactor component. + requests: + # -- (string) CPU request for the compactor pods. Determines how much CPU is guaranteed for the pod. + cpu: 1 + # -- (string) Memory request for the compactor pods. Determines how much memory is guaranteed for the pod. + memory: 2Gi + + distributor: + # -- (map) Affinity rules for scheduling distributor pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (int) Number of replicas for the distributor component. Determines how many instances to run. + replicas: 3 + resources: + # -- (map) Resource limits for the distributor component. + limits: + # -- (string) Memory limit for the distributor pods. + memory: 12Gi + # -- (map) Resource requests for the distributor component. + requests: + # -- (string) CPU request for the distributor pods. Determines how much CPU is guaranteed for the pod. + cpu: 2 + # -- (string) Memory request for the distributor pods. Determines how much memory is guaranteed for the pod. + memory: 8Gi + + ingester: + # -- (map) Persistent volume configuration for the ingester component. + persistentVolume: + # -- (string) Size of the persistent volume to be used by the ingester. + size: 50Gi + # -- (int) Number of replicas for the ingester component. Determines how many instances to run. + replicas: 5 + resources: + # -- (map) Resource limits for the ingester component. + limits: + # -- (string) Memory limit for the ingester pods. + memory: 12Gi + # -- (map) Resource requests for the ingester component. + requests: + # -- (string) CPU request for the ingester pods. Determines how much CPU is guaranteed for the pod. + cpu: 3.5 + # -- (string) Memory request for the ingester pods. Determines how much memory is guaranteed for the pod. + memory: 8Gi + # -- (map) Topology spread constraints for the ingester component. Empty by default. + topologySpreadConstraints: {} + affinity: + # -- (map) Affinity rules for scheduling ingester pods. + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (map) Zone-aware replication settings. Helps distribute data across zones. + zoneAwareReplication: + # -- (string) Topology key used for zone-aware replication. + topologyKey: 'kubernetes.io/hostname' + + overrides_exporter: + # -- (map) Affinity rules for scheduling overrides_exporter pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (int) Number of replicas for the overrides_exporter component. Determines how many instances to run. + replicas: 1 + resources: + # -- (map) Resource limits for the overrides_exporter component. + limits: + # -- (string) Memory limit for the overrides_exporter pods. + memory: 128Mi + # -- (map) Resource requests for the overrides_exporter component. + requests: + # -- (string) CPU request for the overrides_exporter pods. Determines how much CPU is guaranteed for the pod. + cpu: 100m + # -- (string) Memory request for the overrides_exporter pods. Determines how much memory is guaranteed for the pod. + memory: 128Mi + + querier: + # -- (map) Affinity rules for scheduling querier pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (int) Number of replicas for the querier component. Determines how many instances to run. + replicas: 3 + resources: + # -- (map) Resource limits for the querier component. + limits: + # -- (string) Memory limit for the querier pods. + memory: 8Gi + # -- (map) Resource requests for the querier component. + requests: + # -- (string) CPU request for the querier pods. Determines how much CPU is guaranteed for the pod. + cpu: 2 + # -- (string) Memory request for the querier pods. Determines how much memory is guaranteed for the pod. + memory: 6Gi + + query_scheduler: + # -- (map) Affinity rules for scheduling query_scheduler pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + + query_frontend: + # -- (map) Affinity rules for scheduling query_frontend pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (int) Number of replicas for the query_frontend component. Determines how many instances to run. + replicas: 2 + resources: + # -- (map) Resource limits for the query_frontend component. + limits: + # -- (string) Memory limit for the query_frontend pods. + memory: 3Gi + # -- (map) Resource requests for the query_frontend component. + requests: + # -- (string) CPU request for the query_frontend pods. Determines how much CPU is guaranteed for the pod. + cpu: 2 + # -- (string) Memory request for the query_frontend pods. Determines how much memory is guaranteed for the pod. + memory: 2Gi + + + ruler: + # -- (map) Affinity rules for scheduling ruler pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (int) Number of replicas for the ruler component. Determines how many instances to run. + replicas: 2 + resources: + # -- (map) Resource limits for the ruler component. + limits: + # -- (string) Memory limit for the ruler pods. + memory: 5Gi + # -- (map) Resource requests for the ruler component. + requests: + # -- (string) CPU request for the ruler pods. Determines how much CPU is guaranteed for the pod. + cpu: 1 + # -- (string) Memory request for the ruler pods. Determines how much memory is guaranteed for the pod. + memory: 4Gi + + store_gateway: + # -- (map) Affinity rules for scheduling store_gateway pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (map) Persistent volume configuration for the store_gateway component. + persistentVolume: + # -- (string) Size of the persistent volume to be used by the store_gateway. + size: 50Gi + # -- (int) Number of replicas for the store_gateway component. Determines how many instances to run. + replicas: 2 + resources: + # -- (map) Resource limits for the store_gateway component. + limits: + # -- (string) Memory limit for the store_gateway pods. + memory: 8Gi + # -- (map) Resource requests for the store_gateway component. + requests: + # -- (string) CPU request for the store_gateway pods. Determines how much CPU is guaranteed for the pod. + cpu: 1 + # -- (string) Memory request for the store_gateway pods. Determines how much memory is guaranteed for the pod. + memory: 6Gi + # -- (map) Topology spread constraints for the store_gateway component. Empty by default. + topologySpreadConstraints: {} + # -- (map) Zone-aware replication settings. Helps distribute data across zones. + zoneAwareReplication: + # -- (string) Topology key used for zone-aware replication. + topologyKey: 'kubernetes.io/hostname' + + nginx: + # -- (string) Affinity rules for scheduling nginx pods. Passed in as a multiline string. + affinity: | + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + image: + # -- (string) Container image registry for nginx. + registry: quay.io/nginx + # -- (string) Repository for nginx unprivileged image. + repository: nginx-unprivileged + # -- (int) Number of replicas for the nginx component. Determines how many instances to run. + replicas: 3 + resources: + # -- (map) Resource limits for the nginx component. + limits: + # -- (string) Memory limit for the nginx pods. + memory: 731Mi + # -- (map) Resource requests for the nginx component. + requests: + # -- (string) CPU request for the nginx pods. Determines how much CPU is guaranteed for the pod. + cpu: 1 + # -- (string) Memory request for the nginx pods. Determines how much memory is guaranteed for the pod. + memory: 512Mi + + gateway: + # -- (map) Affinity rules for scheduling gateway pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (int) Number of replicas for the gateway component. Determines how many instances to run. + replicas: 3 + resources: + # -- (map) Resource limits for the gateway component. + limits: + # -- (string) Memory limit for the gateway pods. + memory: 731Mi + # -- (map) Resource requests for the gateway component. + requests: + # -- (string) CPU request for the gateway pods. Determines how much CPU is guaranteed for the pod. + cpu: 1 + # -- (string) Memory request for the gateway pods. Determines how much memory is guaranteed for the pod. + memory: 512Mi + + + # -- (map) Loki configuration. + loki: + # -- (map) Persistence settings for loki. + persistence: + # -- (bool) Enable or disable persistence. + enabled: true + # -- (string) Service account configuration for loki. + serviceAccount: + # -- (string) Service account to use (will be created by default via this helm chart). + name: observability + gateway: + # -- (string) Affinity rules for scheduling gateway pods. Passed in as a multiline string. + affinity: | + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: topology.kubernetes.io/zone + operator: In + values: + - us-east-1a + # -- (map) Loki ingress configuration. + ingress: + # -- (map) Annotations to add to loki ingress. + annotations: {} + ## Recommended annotations for AWS ALB (Application Load Balancer). + # alb.ingress.kubernetes.io/certificate-arn: + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + # alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + # alb.ingress.kubernetes.io/scheme: internal + # alb.ingress.kubernetes.io/ssl-policy: + # alb.ingress.kubernetes.io/ssl-redirect: '443' + # alb.ingress.kubernetes.io/tags: Environment= + # alb.ingress.kubernetes.io/target-type: ip + # -- (bool) Enable or disable loki ingress. + enabled: true + # -- (string) Class name for ingress. + ingressClassName: "alb" + # -- (list) Hosts for loki ingress. + hosts: + # -- (string) Hostname for loki ingress. + - host: loki.example.com + paths: + # New data structure introduced + - path: / + # Newly added optional property + pathType: Prefix + + # -- (map) Scaling and configuring loki querier. + querier: + # -- (map) Resource requests and limits for querier. + resources: + # -- (map) Resource limits for the querier component. + limits: + # -- (string) Memory limit for the querier pods. + memory: 6Gi + # -- (map) Resource requests for the querier component. + requests: + # -- (string) CPU request for the querier pods. Determines how much CPU is guaranteed for the pod. + cpu: 2 + # -- (string) Memory request for the querier pods. Determines how much memory is guaranteed for the pod. + memory: 4Gi + # -- (string) Affinity rules for scheduling querier pods. Passed in as a multiline string. + affinity: | + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + + # -- (map) Scaling and configuring loki queryFrontend. + queryFrontend: + # -- (map) Resource requests and limits for queryFrontend. + resources: + # -- (map) Resource limits for the queryFrontend component. + limits: + # -- (string) Memory limit for the queryFrontend pods. + memory: 6Gi + # -- (map) Resource requests for the queryFrontend component. + requests: + # -- (string) CPU request for the queryFrontend pods. Determines how much CPU is guaranteed for the pod. + cpu: 2 + # -- (string) Memory request for the queryFrontend pods. Determines how much memory is guaranteed for the pod. + memory: 4Gi + # -- (map) Affinity rules for scheduling queryFrontend pods. Passed in as a multiline string. + affinity: | + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + + # -- (map) Scaling and configuring loki distributor. + distributor: + # -- (map) Affinity rules for scheduling distributor pods. Passed in as a multiline string. + affinity: | + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (int) Number of replicas for the distributor component. Determines how many instances to run. + replicas: 3 + # -- (int) Maximum number of unavailable replicas allowed during an update. + maxUnavailable: 2 + resources: + # -- (map) Resource limits for the distributor component. + limits: + # -- (string) Memory limit for the distributor pods. + memory: 6Gi + # -- (map) Resource requests for the distributor component. + requests: + # -- (string) CPU request for the distributor pods. Determines how much CPU is guaranteed for the pod. + cpu: 2 + # -- (string) Memory request for the distributor pods. Determines how much memory is guaranteed for the pod. + memory: 4Gi + + + # -- (map) Scaling and configuring loki ingester. Passed in as a multiline string. + ingester: + # -- (map) Affinity rules for scheduling ingester pods. + affinity: | + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (map) Persistent volume configuration for the ingester component. + persistentVolume: + # -- (string) Size of the persistent volume to be used by the ingester. + size: 50Gi + # -- (int) Number of replicas for the ingester component. Determines how many instances to run. + replicas: 3 + # -- (int) Maximum number of unavailable replicas allowed during an update. + maxUnavailable: 2 + resources: + # -- (map) Resource limits for the ingester component. + limits: + # -- (string) Memory limit for the ingester pods. + memory: 12Gi + # -- (map) Resource requests for the ingester component. + requests: + # -- (string) CPU request for the ingester pods. Determines how much CPU is guaranteed for the pod. + cpu: 3.5 + # -- (string) Memory request for the ingester pods. Determines how much memory is guaranteed for the pod. + memory: 8Gi + + + # -- (map) Loki configuration. + loki: + # -- (map) Loki image details. + image: + # -- (string) Container image registry for Loki. + registry: quay.io/cdis + # -- (string) Repository for the Loki image. + repository: loki + # -- (string) Tag for the Loki image version. + tag: master + + # -- (map) Schema configuration for Loki. + schemaConfig: + configs: + - from: 2024-04-01 + # -- (string) Storage engine used by Loki. + store: tsdb + # -- (string) Object store for Loki data (e.g., S3). + object_store: s3 + # -- (string) Schema version for Loki. + schema: v13 + # -- (map) Index configuration for Loki. + index: + # -- (string) Prefix for the Loki index. + prefix: loki_index_ + # -- (string) Index rotation period for Loki, in hours. + period: 24h + # -- (map) Structured configuration settings for Loki. + structuredConfig: + server: + # -- (string) Log level for Loki server. Options include 'info', 'debug', etc. + log_level: debug + limits_config: + # -- (int) Maximum number of series that can be queried at once. + max_query_series: 30000 + # -- (int) Maximum number of streams a single user can have. + max_streams_per_user: 100000 + # -- (int) Maximum number of log entries per query. + max_entries_limit_per_query: 100000000 + common: + # -- (string) Path prefix where Loki stores data. + path_prefix: /var/loki + storage: + # -- (null) Filesystem storage is disabled. + filesystem: null + s3: + # -- (string) AWS region for S3 storage. + region: us-east-1 + # # -- (string) S3 bucket names for Loki storage. + # bucketnames: + + # -- (map) Grafana configuration. + grafana: + # -- (bool) Deploy Grafana if enabled. See [upstream readme](https://github.com/grafana/helm-charts/tree/main/charts/grafana#configuration) for full values reference. + enabled: true + # -- (map) Affinity rules for scheduling Grafana pods. + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + # -- (string) Node label key for affinity. Ensures pods are scheduled on nodes in the specified zone. + - key: topology.kubernetes.io/zone + # -- (string) Operator to apply to the node selector. 'In' means the node must match one of the values. + operator: In + # -- (list) List of values for the node selector, representing allowed zones. + values: + - us-east-1a + # -- (map) Init container to chown data directories for Grafana. + initChownData: + image: + # -- (string) Container image registry for the init container. + registry: quay.io/cdis + # -- (string) Repository for the busybox image. + repository: busybox + # -- (string) Tag for the busybox image version. + tag: 1.32.0 + # -- (map) Image used to download Grafana dashboards. + downloadDashboardsImage: + # -- (string) Container image registry for the dashboard download image. + registry: quay.io/curl + # -- (string) Repository for the curl image. + repository: curl + # -- (string) Tag for the curl image version. + tag: 8.8.0 + + # -- (string) Reference a secret for environment variables. + envFromSecret: + ingress: + # -- (bool) Enable or disable ingress for Grafana. + enabled: true + # -- (map) Annotations for Grafana ingress. + annotations: {} + + ## Recommended annotations for AWS ALB (Application Load Balancer). + # alb.ingress.kubernetes.io/ssl-redirect: '443' + # alb.ingress.kubernetes.io/certificate-arn: + # alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' + # alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600 + # alb.ingress.kubernetes.io/scheme: internet-facing + # alb.ingress.kubernetes.io/ssl-policy: + # alb.ingress.kubernetes.io/tags: Environment= + # alb.ingress.kubernetes.io/target-type: 'ip' + # alb.ingress.kubernetes.io/inbound-cidrs: + # -- (list) Hostname(s) for Grafana ingress. + hosts: + - grafana.example.com + # -- (string) Ingress class name to be used (e.g., 'alb' for AWS Application Load Balancer). + ingressClassName: "alb" + tls: + # -- (list) TLS configuration for the ingress. Reference to a secret that contains the TLS certificate. + - secretName: + # -- (map) Persistence configuration for Grafana. + persistence: + # -- (bool) Enable or disable persistence for Grafana data. + enabled: true + + # -- (map) Image configuration for Grafana. + image: + # -- (string) Container image registry for Grafana. + registry: quay.io/cdis + # -- (string) Repository for the Grafana image. + repository: grafana + # -- (string) Pull policy for the Grafana image (e.g., 'Always'). + pullPolicy: Always + # -- (string) Tag for the Grafana image version. + tag: master + + # -- (map) Environment variables for Grafana. + env: + # -- (string) Root URL configuration for the Grafana server. + GF_SERVER_ROOT_URL: "https://grafana.example.com" + + # -- (map) Configuration for dashboard providers in Grafana. + dashboardProviders: + dashboardproviders.yaml: + # -- (int) API version for dashboard provider configuration. + apiVersion: 1 + # -- (list) List of dashboard providers. + providers: + - name: 'grafana-dashboards-kubernetes' + # -- (int) Organization ID in Grafana. + orgId: 1 + # -- (string) Folder where the dashboards will be placed in Grafana. + folder: 'Kubernetes' + # -- (string) Type of dashboard provider, usually 'file'. + type: file + # -- (bool) Prevent deletion of the provided dashboards. + disableDeletion: true + # -- (bool) Allow editing of the dashboards. + editable: true + # -- (map) Options for the dashboard provider. + options: + # -- (string) Path to the dashboard files. + path: /var/lib/grafana/dashboards/grafana-dashboards-kubernetes + + # -- (map) Dashboards configuration. URLs to fetch specific Kubernetes-related Grafana dashboards. + # Gen3 specific dashboards can be found here. https://github.com/uc-cdis/grafana-dashboards + dashboards: + grafana-dashboards-kubernetes: + k8s-system-api-server: + # -- (string) URL to the dashboard JSON file for the Kubernetes API server. + url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-api-server.json + # -- (string) Authentication token for accessing the dashboard URL (optional). + token: '' + k8s-system-coredns: + # -- (string) URL to the dashboard JSON file for CoreDNS in Kubernetes. + url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-system-coredns.json + token: '' + k8s-views-global: + # -- (string) URL to the dashboard JSON file for global views in Kubernetes. + url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-global.json + token: '' + k8s-views-namespaces: + # -- (string) URL to the dashboard JSON file for Kubernetes namespace views. + url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-namespaces.json + token: '' + k8s-views-nodes: + # -- (string) URL to the dashboard JSON file for Kubernetes node views. + url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-nodes.json + token: '' + k8s-views-pods: + # -- (string) URL to the dashboard JSON file for Kubernetes pod views. + url: https://raw.githubusercontent.com/dotdc/grafana-dashboards-kubernetes/master/dashboards/k8s-views-pods.json + token: '' + + grafana.ini: + # -- (map) Okta authentication settings in Grafana. + auth.okta: + # -- (bool) Enable or disable Okta authentication. + enabled: true + # -- (string) Icon used for Okta in the Grafana UI. + icon: okta + # -- (bool) Allow users to sign up automatically using Okta. + allow_sign_up: true + # -- (bool) Automatically log in users using Okta when visiting Grafana. + auto_login: true + # # -- (string) Okta client ID. + # client_id: + # # -- (string) Okta client secret. + # client_secret: + # # -- (string) Okta authorization URL. + # auth_url: + # # -- (string) Okta token URL. + # token_url: + # # -- (string) Okta API URL. + # api_url: + # -- (map) User configuration settings in Grafana. + users: + # -- (string) Auto-assign the specified role to new users upon login. Options: Viewer, Editor, Admin. + auto_assign_org_role: Editor + # -- (map) Logging configuration in Grafana. + log: + # -- (string) Logging level for Grafana. Options: debug, info, warn, error. + level: debug + # -- (map) Server configuration in Grafana. + server: + # -- (string) Domain name for the Grafana server. + domain: grafana.example.com + # -- (string) Root URL for Grafana, using the domain name. + root_url: "https://%(domain)s/" + # -- (map) Feature toggles in Grafana. + feature_toggles: + # -- (bool) Enable Single Sign-On (SSO) settings API. + ssoSettingsApi: true + # -- (bool) Enable support for transformations using variables in Grafana. + transformationsVariableSupport: true + # -- (list) Features to be enabled in Grafana. + enable: ssoSettingsAPI transformationsVariableSupport + + # -- (map) Gen3 built-in alerting configuration in Grafana. + alerting: + # -- (string) Alerting rules configuration file. + rules.yaml: + # -- (int) API version for the alerting rules configuration. + apiVersion: 1 + # -- (list) Groups of alerting rules. + groups: + - orgId: 1 + # -- (string) Name of the alert group. + name: Alerts + # -- (string) Folder where the alerts will be placed in Grafana. + folder: Alerts + # -- (string) Interval at which the alert rules are evaluated. + interval: 5m + # -- (list) List of alerting rules to be defined (add specific rules here). + rules: + - uid: edwb8zgcvq96oc + title: HTTP 500 errors detected + condition: A + data: + - refId: A + queryType: instant + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: loki + model: + datasource: + type: loki + uid: loki + editorMode: code + expr: sum by (cluster) (count_over_time({cluster=~".+"} | json | http_status_code="500" [1h])) > 0 + hide: false + intervalMs: 1000 + maxDataPoints: 43200 + queryType: instant + refId: A + noDataState: OK + execErrState: KeepLast + for: 5m + annotations: + summary: 'Alert: HTTP 500 errors detected in the environment: {{`{{ $labels.clusters }}`}}' + labels: {} + isPaused: false + notification_settings: + receiver: Slack + - uid: adwb9vhb7irr4b + title: Error Logs Detected in Usersync Job + condition: A + data: + - refId: A + queryType: instant + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: loki + model: + datasource: + type: loki + uid: loki + editorMode: code + expr: sum by (cluster, namespace) (count_over_time({ app="gen3job", job_name=~"usersync-.*"} |= "ERROR - could not revoke policies from user `N/A`" [5m])) > 1 + hide: false + intervalMs: 1000 + maxDataPoints: 43200 + queryType: instant + refId: A + noDataState: OK + execErrState: KeepLast + for: 5m + annotations: + description: Error in usersync job detected in cluster {{`{{ $labels.clusters }}`}}, namespace {{`{{ $labels.namespace }}`}}. + summary: Error Logs Detected in Usersync Job + labels: {} + isPaused: false + notification_settings: + receiver: Slack + - uid: ddwbc12l6wc8wf + title: Hatchery panic in {{`{{ env.name }}`}} + condition: A + data: + - refId: A + queryType: instant + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: loki + model: + datasource: + type: loki + uid: loki + editorMode: code + expr: sum by (cluster) (count_over_time({app="hatchery"} |= "panic" [5m])) > 1 + hide: false + intervalMs: 1000 + maxDataPoints: 43200 + queryType: instant + refId: A + noDataState: OK + execErrState: KeepLast + for: 5m + annotations: + description: Panic detected in app {{`{{ $labels.app }}`}} within cluster {{`{{ $labels.clusters }}`}}. + summary: Hatchery panic + labels: {} + isPaused: false + notification_settings: + receiver: Slack + - uid: cdwbcbphz1zb4a + title: Http status code 431 + condition: A + data: + - refId: A + queryType: instant + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: loki + model: + datasource: + type: loki + uid: loki + editorMode: code + expr: sum(count_over_time({cluster=~".+"} | json | http_status_code="431" [5m])) >= 2 + hide: false + intervalMs: 1000 + maxDataPoints: 43200 + queryType: instant + refId: A + noDataState: OK + execErrState: KeepLast + for: 5m + annotations: + description: Detected 431 HTTP status codes in the logs within the last 5 minutes. + summary: Http status code 431 + labels: {} + isPaused: false + notification_settings: + receiver: Slack + - uid: bdwbck1lgwdfka + title: Indexd is getting an excessive amount of traffic + condition: A + data: + - refId: A + queryType: instant + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: loki + model: + datasource: + type: loki + uid: loki + editorMode: code + expr: sum by (cluster) (count_over_time({cluster=~".+", app="indexd", status="info"} [5m])) > 50000 + hide: false + intervalMs: 1000 + maxDataPoints: 43200 + queryType: instant + refId: A + noDataState: OK + execErrState: KeepLast + for: 5m + annotations: + description: High number of info status logs detected in the indexd service in cluster {{`{{ $labels.clusters }}`}}. + summary: Indexd is getting an excessive amount of traffic + labels: {} + isPaused: false + notification_settings: + receiver: Slack + - uid: fdwbe5t439zpcd + title: Karpenter Resource Mismatch + condition: A + data: + - refId: A + queryType: instant + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: loki + model: + datasource: + type: loki + uid: loki + editorMode: code + expr: | + sum by (cluster) (count_over_time({namespace="karpenter", cluster=~".+"} |= "ERROR" |= "not found" |= "getting providerRef" [5m])) > 10 + hide: false + intervalMs: 1000 + maxDataPoints: 43200 + queryType: instant + refId: A + noDataState: OK + execErrState: KeepLast + for: 5m + annotations: + description: More than 10 errors detected in the karpenter namespace in cluster {{`{{ $labels.clusters }}`}} related to providerRef not found. + summary: Karpenter Resource Mismatch + labels: {} + isPaused: false + notification_settings: + receiver: Slack + - uid: fdwbeuftc7400c + title: Nginx is logging excessive " limiting requests, excess:" + condition: A + data: + - refId: A + queryType: instant + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: loki + model: + datasource: + type: loki + uid: loki + editorMode: code + expr: sum by (app, cluster) (count_over_time({app=~".+", cluster=~".+"} |= "status:error" |= "limiting requests, excess:" [5m])) > 1000 + hide: false + intervalMs: 1000 + maxDataPoints: 43200 + queryType: instant + refId: A + noDataState: OK + execErrState: KeepLast + for: 5m + annotations: + description: 'More than 1000 "limiting requests, excess" errors detected in service {{`{{ $labels.app }}`}} (cluster: {{`{{ $labels.clusters }}`}}) within the last 5 minutes.' + summary: Nginx is logging excessive " limiting requests, excess:" + labels: {} + isPaused: false + notification_settings: + receiver: Slack + contactpoints.yaml: + secret: + apiVersion: 1 + contactPoints: + - orgId: 1 + name: slack + receivers: + - uid: first_uid + type: Slack + settings: + url: https://hooks.slack.com/services/XXXXXXXXXX + group: slack + summary: | + {{ `{{ include "default.message" . }}` }} diff --git a/helm/peregrine/Chart.yaml b/helm/peregrine/Chart.yaml index 1755a4b6..f726f8ca 100644 --- a/helm/peregrine/Chart.yaml +++ b/helm/peregrine/Chart.yaml @@ -15,20 +15,19 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.6 +version: 0.1.17 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "2023.01" - +appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/peregrine/README.md b/helm/peregrine/README.md index b12469ed..d6357512 100644 --- a/helm/peregrine/README.md +++ b/helm/peregrine/README.md @@ -1,6 +1,6 @@ # peregrine -![Version: 0.1.6](https://img.shields.io/badge/Version-0.1.6-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2023.01](https://img.shields.io/badge/AppVersion-2023.01-informational?style=flat-square) +![Version: 0.1.17](https://img.shields.io/badge/Version-0.1.17-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Peregrine service @@ -8,7 +8,7 @@ A Helm chart for gen3 Peregrine service | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values @@ -22,41 +22,50 @@ A Helm chart for gen3 Peregrine service | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | env | list | `nil` | Environment variables to pass to the container | +| externalSecrets | map | `{"dbcreds":null}` | External Secrets settings. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any peregrine secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | | global.postgres.master.port | string | `"5432"` | Port for Postgres. | | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | -| global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | | image.pullPolicy | string | `"IfNotPresent"` | When to pull the image. | | image.repository | string | `"quay.io/cdis/peregrine"` | The Docker image repository for the fence service | -| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | +| image.tag | string | `"feat_jq-audience"` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | +| netPolicy | map | `{"egressApps":["pidgin"],"ingressApps":["pidgin"]}` | Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true | +| netPolicy.egressApps | array | `["pidgin"]` | List of apps that this app requires egress to | +| netPolicy.ingressApps | array | `["pidgin"]` | List of app labels that require ingress to this service | | nodeSelector | map | `{}` | Node Selector for the pods | +| partOf | string | `"Core-Service"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod | | podSecurityContext | map | `{}` | Security context for the pod | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | @@ -68,6 +77,7 @@ A Helm chart for gen3 Peregrine service | postgres.separate | string | `false` | Will create a Database for the individual service to help with developing it. | | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of desired replicas | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -76,7 +86,11 @@ A Helm chart for gen3 Peregrine service | resources.requests | map | `{"cpu":0.1,"memory":"12Mi"}` | The amount of resources that the container requests | | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information for External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | | securityContext | map | `{}` | Security context for the containers in the pod | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -85,8 +99,5 @@ A Helm chart for gen3 Peregrine service | serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | | serviceAccount.name | string | `""` | The name of the service account | | tolerations | list | `[]` | Tolerations for the pods | -| volumeMounts | list | `nil` | Volumes to mount to the container. | +| volumeMounts | list | `[{"mountPath":"/var/www/peregrine/settings.py","name":"config-volume","readOnly":true,"subPath":"settings.py"},{"mountPath":"peregrine/bin/settings.py","name":"config-volume","readOnly":true,"subPath":"settings.py"}]` | Volumes to mount to the container. | | volumes | list | `[{"emptyDir":{},"name":"shared-data"},{"name":"config-volume","secret":{"secretName":"peregrine-secret"}}]` | Volumes to attach to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/peregrine/templates/_helpers.tpl b/helm/peregrine/templates/_helpers.tpl index 1f786d38..4d1439e0 100644 --- a/helm/peregrine/templates/_helpers.tpl +++ b/helm/peregrine/templates/_helpers.tpl @@ -34,20 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "peregrine.labels" -}} -helm.sh/chart: {{ include "peregrine.chart" . }} -{{ include "peregrine.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "peregrine.selectorLabels" -}} -app.kubernetes.io/name: {{ include "peregrine.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -84,4 +90,4 @@ Define dictionaryUrl {{- else}} {{- .Values.dictionaryUrl }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/peregrine/templates/aws-config.yaml b/helm/peregrine/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/peregrine/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/peregrine/templates/db-init.yaml b/helm/peregrine/templates/db-init.yaml index abbefb6e..5ef14e87 100644 --- a/helm/peregrine/templates/db-init.yaml +++ b/helm/peregrine/templates/db-init.yaml @@ -1,6 +1,6 @@ -{{ include "common.db_setup_job" . }} ---- {{ include "common.db-secret" . }} --- -{{ include "common.db_setup_sa" . }} +{{ include "common.db_setup_job" . }} --- +{{ include "common.db_setup_sa" . }} +--- \ No newline at end of file diff --git a/helm/peregrine/templates/deployment.yaml b/helm/peregrine/templates/deployment.yaml index 378f016d..39edbd5d 100644 --- a/helm/peregrine/templates/deployment.yaml +++ b/helm/peregrine/templates/deployment.yaml @@ -13,12 +13,18 @@ spec: {{- include "peregrine.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: + public: "yes" + s3: "yes" {{- include "peregrine.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: {{- with .Values.volumes }} volumes: @@ -70,6 +76,12 @@ spec: optional: false - name: FLASK_SECRET_KEY value: "TODO: FIX THIS!!!" + - name: INDEXD_PASS + valueFrom: + secretKeyRef: + name: indexd-service-creds + key: sheepdog + optional: false - name: PGHOST valueFrom: secretKeyRef: @@ -138,11 +150,10 @@ spec: value: "False" - name: CONF_HOSTNAME value: {{ .Values.global.hostname }} + {{- with .Values.volumeMounts }} volumeMounts: - - name: "config-volume" - readOnly: true - mountPath: "/var/www/peregrine/wsgi.py" - subPath: "settings.py" + {{- toYaml . | nindent 10 }} + {{- end }} ports: - name: http containerPort: 80 diff --git a/helm/peregrine/templates/external-secret.yaml b/helm/peregrine/templates/external-secret.yaml new file mode 100644 index 00000000..70c278fe --- /dev/null +++ b/helm/peregrine/templates/external-secret.yaml @@ -0,0 +1 @@ +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/peregrine/templates/netpolicy.yaml b/helm/peregrine/templates/netpolicy.yaml new file mode 100644 index 00000000..93949e3a --- /dev/null +++ b/helm/peregrine/templates/netpolicy.yaml @@ -0,0 +1,9 @@ +{{ include "common.db_netpolicy" . }} + +--- + +{{ include "common.ingress_netpolicy" . }} + +--- + +{{ include "common.egress_netpolicy" . }} \ No newline at end of file diff --git a/helm/peregrine/templates/pdb.yaml b/helm/peregrine/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/peregrine/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/peregrine/templates/secret-store.yaml b/helm/peregrine/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/peregrine/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/peregrine/templates/tests/test-connection.yaml b/helm/peregrine/templates/tests/test-connection.yaml index 91f03791..56bcf4b5 100644 --- a/helm/peregrine/templates/tests/test-connection.yaml +++ b/helm/peregrine/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "peregrine.fullname" . }}-test-connection" + name: peregrine-test-connection labels: {{- include "peregrine.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "peregrine.fullname" . }}:{{ .Values.service.port }}'] + args: ['peregrine-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/peregrine/values.yaml b/helm/peregrine/values.yaml index 1c5bf907..c06c1c92 100644 --- a/helm/peregrine/values.yaml +++ b/helm/peregrine/values.yaml @@ -1,7 +1,7 @@ # Default values for peregrine. # This is a YAML-formatted file. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -13,10 +13,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -41,20 +43,37 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml - # -- (bool) Whether public datasets are enabled. - publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any peregrine secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: +# -- (map) Secret information for External Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -94,7 +113,7 @@ image: # -- (string) When to pull the image. pullPolicy: IfNotPresent # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: "" + tag: "feat_jq-audience" # -- (list) Docker image pull secrets. imagePullSecrets: [] @@ -119,11 +138,13 @@ serviceAccount: podAnnotations: {} # -- (map) Security context for the pod -podSecurityContext: {} +podSecurityContext: + {} # fsGroup: 2000 # -- (map) Security context for the containers in the pod -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -138,6 +159,16 @@ service: # -- (int) The port number that the service exposes. port: 80 +# -- (map) Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true +netPolicy: + # -- (array) List of app labels that require ingress to this service + ingressApps: + - pidgin + + # -- (array) List of apps that this app requires egress to + egressApps: + - pidgin + # -- (map) Resource requests and limits for the containers in the pod resources: # -- (map) The amount of resources that the container requests @@ -179,11 +210,31 @@ env: # -- (list) Volumes to attach to the container. volumes: -- name: shared-data - emptyDir: {} -- name: config-volume - secret: - secretName: "peregrine-secret" + - name: shared-data + emptyDir: {} + - name: config-volume + secret: + secretName: "peregrine-secret" # -- (list) Volumes to mount to the container. volumeMounts: + - name: "config-volume" + readOnly: true + mountPath: "/var/www/peregrine/settings.py" + subPath: "settings.py" + - name: "config-volume" + readOnly: true + mountPath: "peregrine/bin/settings.py" + subPath: "settings.py" + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Core-Service" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/pidgin/README.md b/helm/pidgin/README.md deleted file mode 100644 index 87cfce4e..00000000 --- a/helm/pidgin/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# pidgin - -![Version: 0.1.4](https://img.shields.io/badge/Version-0.1.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) - -A Helm chart for gen3 Pidgin Service - -## Requirements - -| Repository | Name | Version | -|------------|------|---------| -| file://../common | common | 0.1.4 | - -## Values - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| affinity | map | `{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["pidgin"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]}}` | Affinity to use for the deployment. | -| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution | map | `[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["pidgin"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]` | Option for scheduling to be required or preferred. | -| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0] | int | `{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["pidgin"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}` | Weight value for preferred scheduling. | -| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0] | list | `{"key":"app","operator":"In","values":["pidgin"]}` | Label key for match expression. | -| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].operator | string | `"In"` | Operation type for the match expression. | -| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values | list | `["pidgin"]` | Value for the match expression key. | -| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey | string | `"kubernetes.io/hostname"` | Value for topology key label. | -| automountServiceAccountToken | bool | `false` | Automount the default service account token | -| autoscaling | map | `{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Configuration for autoscaling the number of replicas | -| autoscaling.enabled | bool | `false` | Whether autoscaling is enabled | -| autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | -| autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | -| autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | -| dataDog | bool | `{"enabled":false,"env":"dev"}` | Whether Datadog is enabled. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | -| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | -| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | -| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | -| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | -| global.dev | bool | `true` | Whether the deployment is for development purposes. | -| global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | -| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | -| global.hostname | string | `"localhost"` | Hostname for the deployment. | -| global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | -| global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | -| global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | -| global.postgres.dbCreate | bool | `true` | Whether the database should be created. | -| global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | -| global.postgres.master.host | string | `nil` | hostname of postgres server | -| global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | -| global.postgres.master.port | string | `"5432"` | Port for Postgres. | -| global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | -| global.publicDataSets | bool | `true` | Whether public datasets are enabled. | -| global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | -| global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | -| image.pullPolicy | string | `"Always"` | When to pull the image. | -| image.repository | string | `"quay.io/cdis/pidgin"` | The Docker image repository for the fence service | -| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | -| postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | -| postgres.database | string | `nil` | Database name for postgres. This is a service override, defaults to - | -| postgres.dbCreate | bool | `nil` | Whether the database should be created. Default to global.postgres.dbCreate | -| postgres.host | string | `nil` | Hostname for postgres server. This is a service override, defaults to global.postgres.host | -| postgres.password | string | `nil` | Password for Postgres. Will be autogenerated if left empty. | -| postgres.port | string | `"5432"` | Port for Postgres. | -| postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | -| replicaCount | int | `1` | Number of desired replicas | -| resources | map | `nil` | Resource requests and limits for the containers in the pod | -| revisionHistoryLimit | int | `2` | Number of old revisions to retain | -| service | map | `{"port":[{"name":"http","port":80,"protocol":"TCP","targetPort":80},{"name":"https","port":443,"protocol":"TCP","targetPort":443}],"type":"ClusterIP"}` | Kubernetes service information. | -| service.port | list | `[{"name":"http","port":80,"protocol":"TCP","targetPort":80},{"name":"https","port":443,"protocol":"TCP","targetPort":443}]` | The port numbers that the service exposes. | -| service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | -| strategy | map | `{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"}` | Rolling update deployment strategy | -| strategy.rollingUpdate.maxSurge | int | `1` | Number of additional replicas to add during rollout. | -| strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/pidgin/templates/deployment.yaml b/helm/pidgin/templates/deployment.yaml deleted file mode 100644 index 19209962..00000000 --- a/helm/pidgin/templates/deployment.yaml +++ /dev/null @@ -1,58 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: pidgin-deployment -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "pidgin.selectorLabels" . | nindent 6 }} - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} - {{- with .Values.strategy }} - strategy: - {{- toYaml . | nindent 4 }} - {{- end }} - template: - metadata: - labels: - # gen3 networkpolicy labels - netnolimit: 'yes' - public: 'yes' - {{- if eq (include "pidgin.ddEnabled" . ) "true" }} - tags.datadoghq.com/service: "pidgin" - # TODO: move this to helpers so we can have this populated from a configmap - tags.datadoghq.com/env: {{ .Values.dataDog.env }} - tags.datadoghq.com/version: {{ .Values.image.tag | default .Chart.AppVersion }} - {{- end }} - {{- include "pidgin.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - env: - - name: GEN3_DEBUG - value: "False" - livenessProbe: - httpGet: - path: /_status - port: 80 - initialDelaySeconds: 30 - periodSeconds: 60 - timeoutSeconds: 30 - readinessProbe: - httpGet: - path: /_status - port: 80 - ports: - - containerPort: 80 - - containerPort: 443 - imagePullPolicy: {{ .Values.image.pullPolicy }} - resources: - {{- toYaml .Values.resources | nindent 12 }} \ No newline at end of file diff --git a/helm/pidgin/templates/service.yaml b/helm/pidgin/templates/service.yaml deleted file mode 100644 index 42f6936b..00000000 --- a/helm/pidgin/templates/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: "pidgin-service" - labels: - {{- include "pidgin.labels" . | nindent 4 }} -spec: - selector: - {{- include "pidgin.selectorLabels" . | nindent 4 }} - {{- with .Values.service.port }} - ports: - {{- toYaml . | nindent 8 }} - {{- end }} - type: {{ .Values.service.type }} \ No newline at end of file diff --git a/helm/portal/Chart.yaml b/helm/portal/Chart.yaml index 3e53cffc..f8da7e14 100644 --- a/helm/portal/Chart.yaml +++ b/helm/portal/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.3 +version: 0.1.22 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/portal/README.md b/helm/portal/README.md index ef590451..fc33d44a 100644 --- a/helm/portal/README.md +++ b/helm/portal/README.md @@ -1,9 +1,15 @@ # portal -![Version: 0.1.3](https://img.shields.io/badge/Version-0.1.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.22](https://img.shields.io/badge/Version-0.1.22-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 data-portal +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -21,30 +27,34 @@ A Helm chart for gen3 data-portal | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| extraImages | map | `nil` | Extra images to be mounted in the deployment. | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| gitops | map | `{"createdby":"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","css":"/* gitops default css */\n","favicon":"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","json":"{\n \"graphql\": {\n \"boardCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\",\n \"plural\": \"Cases\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\",\n \"plural\": \"Experiments\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\",\n \"plural\": \"Aliquots\"\n }\n ],\n \"chartCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\"\n }\n ],\n \"projectDetails\": \"boardCounts\"\n },\n \"components\": {\n \"appName\": \"Generic Data Commons Portal\",\n \"index\": {\n \"introduction\": {\n \"heading\": \"Data Commons\",\n \"text\": \"The Generic Data Commons supports the management, analysis and sharing of data for the research community.\",\n \"link\": \"/submission\"\n },\n \"buttons\": [\n {\n \"name\": \"Define Data Field\",\n \"icon\": \"data-field-define\",\n \"body\": \"The Generic Data Commons define the data in a general way. Please study the dictionary before you start browsing.\",\n \"link\": \"/DD\",\n \"label\": \"Learn more\"\n },\n {\n \"name\": \"Explore Data\",\n \"icon\": \"data-explore\",\n \"body\": \"The Exploration Page gives you insights and a clear overview under selected factors.\",\n \"link\": \"/explorer\",\n \"label\": \"Explore data\"\n },\n {\n \"name\": \"Access Data\",\n \"icon\": \"data-access\",\n \"body\": \"Use our selected tool to filter out the data you need.\",\n \"link\": \"/query\",\n \"label\": \"Query data\"\n },\n {\n \"name\": \"Submit Data\",\n \"icon\": \"data-submit\",\n \"body\": \"Submit Data based on the dictionary.\",\n \"link\": \"/submission\",\n \"label\": \"Submit data\"\n }\n ]\n },\n \"navigation\": {\n \"title\": \"Generic Data Commons\",\n \"items\": [\n {\n \"icon\": \"dictionary\",\n \"link\": \"/DD\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Dictionary\"\n },\n {\n \"icon\": \"exploration\",\n \"link\": \"/explorer\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Exploration\"\n },\n {\n \"icon\": \"query\",\n \"link\": \"/query\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Query\"\n },\n {\n \"icon\": \"workspace\",\n \"link\": \"/workspace\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Workspace\"\n },\n {\n \"icon\": \"profile\",\n \"link\": \"/identity\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Profile\"\n }\n ]\n },\n \"topBar\": {\n \"items\": [\n {\n \"icon\": \"upload\",\n \"link\": \"/submission\",\n \"name\": \"Submit Data\"\n },\n {\n \"link\": \"https://gen3.org/resources/user\",\n \"name\": \"Documentation\"\n }\n ]\n },\n \"login\": {\n \"title\": \"Generic Data Commons\",\n \"subTitle\": \"Explore, Analyze, and Share Data\",\n \"text\": \"This website supports the management, analysis and sharing of human disease data for the research community and aims to advance basic understanding of the genetic basis of complex traits and accelerate discovery and development of therapies, diagnostic tests, and other technologies for diseases like cancer.\",\n \"contact\": \"If you have any questions about access or the registration process, please contact \",\n \"email\": \"support@datacommons.io\"\n },\n \"certs\": {},\n \"footerLogos\": [\n {\n \"src\": \"/src/img/gen3.png\",\n \"href\": \"https://ctds.uchicago.edu/gen3\",\n \"alt\": \"Gen3 Data Commons\"\n },\n {\n \"src\": \"/src/img/createdby.png\",\n \"href\": \"https://ctds.uchicago.edu/\",\n \"alt\": \"Center for Translational Data Science at the University of Chicago\"\n }\n ]\n },\n \"requiredCerts\": [],\n \"featureFlags\": {\n \"explorer\": true,\n \"noIndex\": true,\n \"analysis\": false,\n \"discovery\": false,\n \"discoveryUseAggMDS\": false,\n \"studyRegistration\": false\n },\n \"dataExplorerConfig\": {\n \"charts\": {\n \"project_id\": {\n \"chartType\": \"count\",\n \"title\": \"Projects\"\n },\n \"case_id\": {\n \"chartType\": \"count\",\n \"title\": \"Cases\"\n },\n \"gender\": {\n \"chartType\": \"pie\",\n \"title\": \"Gender\"\n },\n \"race\": {\n \"chartType\": \"bar\",\n \"title\": \"Race\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"Case\",\n \"fields\":[\n \"project_id\",\n \"gender\",\n \"race\",\n \"ethnicity\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": false\n },\n \"dropdowns\": {},\n \"buttons\": [],\n \"guppyConfig\": {\n \"dataType\": \"case\",\n \"nodeCountTitle\": \"Cases\",\n \"fieldMapping\": [\n { \"field\": \"disease_type\", \"name\": \"Disease type\" },\n { \"field\": \"primary_site\", \"name\": \"Site where samples were collected\"}\n ],\n \"manifestMapping\": {\n \"resourceIndexType\": \"file\",\n \"resourceIdField\": \"object_id\",\n \"referenceIdFieldInResourceIndex\": \"case_id\",\n \"referenceIdFieldInDataIndex\": \"node_id\"\n },\n \"accessibleFieldCheckList\": [\"case_id\"],\n \"accessibleValidationField\": \"case_id\"\n }\n },\n \"fileExplorerConfig\": {\n \"charts\": {\n \"data_type\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Type\"\n },\n \"data_format\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Format\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"File\",\n \"fields\": [\n \"project_id\",\n \"data_type\",\n \"data_format\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": true,\n \"fields\": [\n \"project_id\",\n \"file_name\",\n \"file_size\",\n \"object_id\"\n ]\n },\n \"dropdowns\": {},\n \"guppyConfig\": {\n \"dataType\": \"file\",\n \"fieldMapping\": [\n { \"field\": \"object_id\", \"name\": \"GUID\" }\n ],\n \"nodeCountTitle\": \"Files\",\n \"manifestMapping\": {\n \"resourceIndexType\": \"case\",\n \"resourceIdField\": \"case_id\",\n \"referenceIdFieldInResourceIndex\": \"object_id\",\n \"referenceIdFieldInDataIndex\": \"object_id\"\n },\n \"accessibleFieldCheckList\": [\"case_id\"],\n \"accessibleValidationField\": \"case_id\",\n \"downloadAccessor\": \"object_id\"\n }\n }\n}\n","logo":"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","sponsors":null}` | GitOps configuration for portal | +| gitops | map | `{"createdby":"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","css":"/* gitops default css */\n","favicon":"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","gen3Bundle":"","json":"{\n \"graphql\": {\n \"boardCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\",\n \"plural\": \"Cases\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\",\n \"plural\": \"Experiments\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\",\n \"plural\": \"Aliquots\"\n }\n ],\n \"chartCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\"\n }\n ],\n \"projectDetails\": \"boardCounts\"\n },\n \"components\": {\n \"appName\": \"Generic Data Commons Portal\",\n \"index\": {\n \"introduction\": {\n \"heading\": \"Data Commons\",\n \"text\": \"The Generic Data Commons supports the management, analysis and sharing of data for the research community.\",\n \"link\": \"/submission\"\n },\n \"buttons\": [\n {\n \"name\": \"Define Data Field\",\n \"icon\": \"data-field-define\",\n \"body\": \"The Generic Data Commons define the data in a general way. Please study the dictionary before you start browsing.\",\n \"link\": \"/DD\",\n \"label\": \"Learn more\"\n },\n {\n \"name\": \"Explore Data\",\n \"icon\": \"data-explore\",\n \"body\": \"The Exploration Page gives you insights and a clear overview under selected factors.\",\n \"link\": \"/explorer\",\n \"label\": \"Explore data\"\n },\n {\n \"name\": \"Access Data\",\n \"icon\": \"data-access\",\n \"body\": \"Use our selected tool to filter out the data you need.\",\n \"link\": \"/query\",\n \"label\": \"Query data\"\n },\n {\n \"name\": \"Submit Data\",\n \"icon\": \"data-submit\",\n \"body\": \"Submit Data based on the dictionary.\",\n \"link\": \"/submission\",\n \"label\": \"Submit data\"\n }\n ]\n },\n \"navigation\": {\n \"title\": \"Generic Data Commons\",\n \"items\": [\n {\n \"icon\": \"dictionary\",\n \"link\": \"/DD\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Dictionary\"\n },\n {\n \"icon\": \"exploration\",\n \"link\": \"/explorer\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Exploration\"\n },\n {\n \"icon\": \"query\",\n \"link\": \"/query\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Query\"\n },\n {\n \"icon\": \"workspace\",\n \"link\": \"/workspace\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Workspace\"\n },\n {\n \"icon\": \"profile\",\n \"link\": \"/identity\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Profile\"\n }\n ]\n },\n \"topBar\": {\n \"items\": [\n {\n \"icon\": \"upload\",\n \"link\": \"/submission\",\n \"name\": \"Submit Data\"\n },\n {\n \"link\": \"https://gen3.org/resources/user\",\n \"name\": \"Documentation\"\n }\n ]\n },\n \"login\": {\n \"title\": \"Generic Data Commons\",\n \"subTitle\": \"Explore, Analyze, and Share Data\",\n \"text\": \"This website supports the management, analysis and sharing of human disease data for the research community and aims to advance basic understanding of the genetic basis of complex traits and accelerate discovery and development of therapies, diagnostic tests, and other technologies for diseases like cancer.\",\n \"contact\": \"If you have any questions about access or the registration process, please contact \",\n \"email\": \"support@gen3.org\"\n },\n \"certs\": {},\n \"footerLogos\": [\n {\n \"src\": \"/src/img/gen3.png\",\n \"href\": \"https://ctds.uchicago.edu/gen3\",\n \"alt\": \"Gen3 Data Commons\"\n },\n {\n \"src\": \"/src/img/createdby.png\",\n \"href\": \"https://ctds.uchicago.edu/\",\n \"alt\": \"Center for Translational Data Science at the University of Chicago\"\n }\n ]\n },\n \"requiredCerts\": [],\n \"featureFlags\": {\n \"explorer\": true,\n \"noIndex\": true,\n \"analysis\": false,\n \"discovery\": false,\n \"discoveryUseAggMDS\": false,\n \"studyRegistration\": false\n },\n \"dataExplorerConfig\": {\n \"charts\": {\n \"project_id\": {\n \"chartType\": \"count\",\n \"title\": \"Projects\"\n },\n \"_case_id\": {\n \"chartType\": \"count\",\n \"title\": \"Cases\"\n },\n \"gender\": {\n \"chartType\": \"pie\",\n \"title\": \"Gender\"\n },\n \"race\": {\n \"chartType\": \"bar\",\n \"title\": \"Race\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"Case\",\n \"fields\":[\n \"project_id\",\n \"gender\",\n \"race\",\n \"ethnicity\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": false\n },\n \"dropdowns\": {},\n \"buttons\": [],\n \"guppyConfig\": {\n \"dataType\": \"case\",\n \"nodeCountTitle\": \"Cases\",\n \"fieldMapping\": [\n { \"field\": \"disease_type\", \"name\": \"Disease type\" },\n { \"field\": \"primary_site\", \"name\": \"Site where samples were collected\"}\n ],\n \"manifestMapping\": {\n \"resourceIndexType\": \"file\",\n \"resourceIdField\": \"object_id\",\n \"referenceIdFieldInResourceIndex\": \"_case_id\",\n \"referenceIdFieldInDataIndex\": \"node_id\"\n },\n \"accessibleFieldCheckList\": [\"_case_id\"],\n \"accessibleValidationField\": \"_case_id\"\n }\n },\n \"fileExplorerConfig\": {\n \"charts\": {\n \"data_type\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Type\"\n },\n \"data_format\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Format\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"File\",\n \"fields\": [\n \"project_id\",\n \"data_type\",\n \"data_format\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": true,\n \"fields\": [\n \"project_id\",\n \"file_name\",\n \"file_size\",\n \"object_id\"\n ]\n },\n \"dropdowns\": {},\n \"guppyConfig\": {\n \"dataType\": \"file\",\n \"fieldMapping\": [\n { \"field\": \"object_id\", \"name\": \"GUID\" }\n ],\n \"nodeCountTitle\": \"Files\",\n \"manifestMapping\": {\n \"resourceIndexType\": \"case\",\n \"resourceIdField\": \"_case_id\",\n \"referenceIdFieldInResourceIndex\": \"object_id\",\n \"referenceIdFieldInDataIndex\": \"object_id\"\n },\n \"accessibleFieldCheckList\": [\"_case_id\"],\n \"accessibleValidationField\": \"_case_id\",\n \"downloadAccessor\": \"object_id\"\n }\n }\n}\n","logo":"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","sponsors":null}` | GitOps configuration for portal | | gitops.createdby | string | `"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"` | - createdby.png - base64 | | gitops.css | string | `"/* gitops default css */\n"` | - multiline string - gitops.css | | gitops.favicon | string | `"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"` | - favicon in base64 | -| gitops.json | string | `"{\n \"graphql\": {\n \"boardCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\",\n \"plural\": \"Cases\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\",\n \"plural\": \"Experiments\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\",\n \"plural\": \"Aliquots\"\n }\n ],\n \"chartCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\"\n }\n ],\n \"projectDetails\": \"boardCounts\"\n },\n \"components\": {\n \"appName\": \"Generic Data Commons Portal\",\n \"index\": {\n \"introduction\": {\n \"heading\": \"Data Commons\",\n \"text\": \"The Generic Data Commons supports the management, analysis and sharing of data for the research community.\",\n \"link\": \"/submission\"\n },\n \"buttons\": [\n {\n \"name\": \"Define Data Field\",\n \"icon\": \"data-field-define\",\n \"body\": \"The Generic Data Commons define the data in a general way. Please study the dictionary before you start browsing.\",\n \"link\": \"/DD\",\n \"label\": \"Learn more\"\n },\n {\n \"name\": \"Explore Data\",\n \"icon\": \"data-explore\",\n \"body\": \"The Exploration Page gives you insights and a clear overview under selected factors.\",\n \"link\": \"/explorer\",\n \"label\": \"Explore data\"\n },\n {\n \"name\": \"Access Data\",\n \"icon\": \"data-access\",\n \"body\": \"Use our selected tool to filter out the data you need.\",\n \"link\": \"/query\",\n \"label\": \"Query data\"\n },\n {\n \"name\": \"Submit Data\",\n \"icon\": \"data-submit\",\n \"body\": \"Submit Data based on the dictionary.\",\n \"link\": \"/submission\",\n \"label\": \"Submit data\"\n }\n ]\n },\n \"navigation\": {\n \"title\": \"Generic Data Commons\",\n \"items\": [\n {\n \"icon\": \"dictionary\",\n \"link\": \"/DD\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Dictionary\"\n },\n {\n \"icon\": \"exploration\",\n \"link\": \"/explorer\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Exploration\"\n },\n {\n \"icon\": \"query\",\n \"link\": \"/query\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Query\"\n },\n {\n \"icon\": \"workspace\",\n \"link\": \"/workspace\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Workspace\"\n },\n {\n \"icon\": \"profile\",\n \"link\": \"/identity\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Profile\"\n }\n ]\n },\n \"topBar\": {\n \"items\": [\n {\n \"icon\": \"upload\",\n \"link\": \"/submission\",\n \"name\": \"Submit Data\"\n },\n {\n \"link\": \"https://gen3.org/resources/user\",\n \"name\": \"Documentation\"\n }\n ]\n },\n \"login\": {\n \"title\": \"Generic Data Commons\",\n \"subTitle\": \"Explore, Analyze, and Share Data\",\n \"text\": \"This website supports the management, analysis and sharing of human disease data for the research community and aims to advance basic understanding of the genetic basis of complex traits and accelerate discovery and development of therapies, diagnostic tests, and other technologies for diseases like cancer.\",\n \"contact\": \"If you have any questions about access or the registration process, please contact \",\n \"email\": \"support@datacommons.io\"\n },\n \"certs\": {},\n \"footerLogos\": [\n {\n \"src\": \"/src/img/gen3.png\",\n \"href\": \"https://ctds.uchicago.edu/gen3\",\n \"alt\": \"Gen3 Data Commons\"\n },\n {\n \"src\": \"/src/img/createdby.png\",\n \"href\": \"https://ctds.uchicago.edu/\",\n \"alt\": \"Center for Translational Data Science at the University of Chicago\"\n }\n ]\n },\n \"requiredCerts\": [],\n \"featureFlags\": {\n \"explorer\": true,\n \"noIndex\": true,\n \"analysis\": false,\n \"discovery\": false,\n \"discoveryUseAggMDS\": false,\n \"studyRegistration\": false\n },\n \"dataExplorerConfig\": {\n \"charts\": {\n \"project_id\": {\n \"chartType\": \"count\",\n \"title\": \"Projects\"\n },\n \"case_id\": {\n \"chartType\": \"count\",\n \"title\": \"Cases\"\n },\n \"gender\": {\n \"chartType\": \"pie\",\n \"title\": \"Gender\"\n },\n \"race\": {\n \"chartType\": \"bar\",\n \"title\": \"Race\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"Case\",\n \"fields\":[\n \"project_id\",\n \"gender\",\n \"race\",\n \"ethnicity\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": false\n },\n \"dropdowns\": {},\n \"buttons\": [],\n \"guppyConfig\": {\n \"dataType\": \"case\",\n \"nodeCountTitle\": \"Cases\",\n \"fieldMapping\": [\n { \"field\": \"disease_type\", \"name\": \"Disease type\" },\n { \"field\": \"primary_site\", \"name\": \"Site where samples were collected\"}\n ],\n \"manifestMapping\": {\n \"resourceIndexType\": \"file\",\n \"resourceIdField\": \"object_id\",\n \"referenceIdFieldInResourceIndex\": \"case_id\",\n \"referenceIdFieldInDataIndex\": \"node_id\"\n },\n \"accessibleFieldCheckList\": [\"case_id\"],\n \"accessibleValidationField\": \"case_id\"\n }\n },\n \"fileExplorerConfig\": {\n \"charts\": {\n \"data_type\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Type\"\n },\n \"data_format\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Format\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"File\",\n \"fields\": [\n \"project_id\",\n \"data_type\",\n \"data_format\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": true,\n \"fields\": [\n \"project_id\",\n \"file_name\",\n \"file_size\",\n \"object_id\"\n ]\n },\n \"dropdowns\": {},\n \"guppyConfig\": {\n \"dataType\": \"file\",\n \"fieldMapping\": [\n { \"field\": \"object_id\", \"name\": \"GUID\" }\n ],\n \"nodeCountTitle\": \"Files\",\n \"manifestMapping\": {\n \"resourceIndexType\": \"case\",\n \"resourceIdField\": \"case_id\",\n \"referenceIdFieldInResourceIndex\": \"object_id\",\n \"referenceIdFieldInDataIndex\": \"object_id\"\n },\n \"accessibleFieldCheckList\": [\"case_id\"],\n \"accessibleValidationField\": \"case_id\",\n \"downloadAccessor\": \"object_id\"\n }\n }\n}\n"` | multiline string - gitops.json | +| gitops.json | string | `"{\n \"graphql\": {\n \"boardCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\",\n \"plural\": \"Cases\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\",\n \"plural\": \"Experiments\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\",\n \"plural\": \"Aliquots\"\n }\n ],\n \"chartCounts\": [\n {\n \"graphql\": \"_case_count\",\n \"name\": \"Case\"\n },\n {\n \"graphql\": \"_experiment_count\",\n \"name\": \"Experiment\"\n },\n {\n \"graphql\": \"_aliquot_count\",\n \"name\": \"Aliquot\"\n }\n ],\n \"projectDetails\": \"boardCounts\"\n },\n \"components\": {\n \"appName\": \"Generic Data Commons Portal\",\n \"index\": {\n \"introduction\": {\n \"heading\": \"Data Commons\",\n \"text\": \"The Generic Data Commons supports the management, analysis and sharing of data for the research community.\",\n \"link\": \"/submission\"\n },\n \"buttons\": [\n {\n \"name\": \"Define Data Field\",\n \"icon\": \"data-field-define\",\n \"body\": \"The Generic Data Commons define the data in a general way. Please study the dictionary before you start browsing.\",\n \"link\": \"/DD\",\n \"label\": \"Learn more\"\n },\n {\n \"name\": \"Explore Data\",\n \"icon\": \"data-explore\",\n \"body\": \"The Exploration Page gives you insights and a clear overview under selected factors.\",\n \"link\": \"/explorer\",\n \"label\": \"Explore data\"\n },\n {\n \"name\": \"Access Data\",\n \"icon\": \"data-access\",\n \"body\": \"Use our selected tool to filter out the data you need.\",\n \"link\": \"/query\",\n \"label\": \"Query data\"\n },\n {\n \"name\": \"Submit Data\",\n \"icon\": \"data-submit\",\n \"body\": \"Submit Data based on the dictionary.\",\n \"link\": \"/submission\",\n \"label\": \"Submit data\"\n }\n ]\n },\n \"navigation\": {\n \"title\": \"Generic Data Commons\",\n \"items\": [\n {\n \"icon\": \"dictionary\",\n \"link\": \"/DD\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Dictionary\"\n },\n {\n \"icon\": \"exploration\",\n \"link\": \"/explorer\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Exploration\"\n },\n {\n \"icon\": \"query\",\n \"link\": \"/query\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Query\"\n },\n {\n \"icon\": \"workspace\",\n \"link\": \"/workspace\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Workspace\"\n },\n {\n \"icon\": \"profile\",\n \"link\": \"/identity\",\n \"color\": \"#a2a2a2\",\n \"name\": \"Profile\"\n }\n ]\n },\n \"topBar\": {\n \"items\": [\n {\n \"icon\": \"upload\",\n \"link\": \"/submission\",\n \"name\": \"Submit Data\"\n },\n {\n \"link\": \"https://gen3.org/resources/user\",\n \"name\": \"Documentation\"\n }\n ]\n },\n \"login\": {\n \"title\": \"Generic Data Commons\",\n \"subTitle\": \"Explore, Analyze, and Share Data\",\n \"text\": \"This website supports the management, analysis and sharing of human disease data for the research community and aims to advance basic understanding of the genetic basis of complex traits and accelerate discovery and development of therapies, diagnostic tests, and other technologies for diseases like cancer.\",\n \"contact\": \"If you have any questions about access or the registration process, please contact \",\n \"email\": \"support@gen3.org\"\n },\n \"certs\": {},\n \"footerLogos\": [\n {\n \"src\": \"/src/img/gen3.png\",\n \"href\": \"https://ctds.uchicago.edu/gen3\",\n \"alt\": \"Gen3 Data Commons\"\n },\n {\n \"src\": \"/src/img/createdby.png\",\n \"href\": \"https://ctds.uchicago.edu/\",\n \"alt\": \"Center for Translational Data Science at the University of Chicago\"\n }\n ]\n },\n \"requiredCerts\": [],\n \"featureFlags\": {\n \"explorer\": true,\n \"noIndex\": true,\n \"analysis\": false,\n \"discovery\": false,\n \"discoveryUseAggMDS\": false,\n \"studyRegistration\": false\n },\n \"dataExplorerConfig\": {\n \"charts\": {\n \"project_id\": {\n \"chartType\": \"count\",\n \"title\": \"Projects\"\n },\n \"_case_id\": {\n \"chartType\": \"count\",\n \"title\": \"Cases\"\n },\n \"gender\": {\n \"chartType\": \"pie\",\n \"title\": \"Gender\"\n },\n \"race\": {\n \"chartType\": \"bar\",\n \"title\": \"Race\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"Case\",\n \"fields\":[\n \"project_id\",\n \"gender\",\n \"race\",\n \"ethnicity\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": false\n },\n \"dropdowns\": {},\n \"buttons\": [],\n \"guppyConfig\": {\n \"dataType\": \"case\",\n \"nodeCountTitle\": \"Cases\",\n \"fieldMapping\": [\n { \"field\": \"disease_type\", \"name\": \"Disease type\" },\n { \"field\": \"primary_site\", \"name\": \"Site where samples were collected\"}\n ],\n \"manifestMapping\": {\n \"resourceIndexType\": \"file\",\n \"resourceIdField\": \"object_id\",\n \"referenceIdFieldInResourceIndex\": \"_case_id\",\n \"referenceIdFieldInDataIndex\": \"node_id\"\n },\n \"accessibleFieldCheckList\": [\"_case_id\"],\n \"accessibleValidationField\": \"_case_id\"\n }\n },\n \"fileExplorerConfig\": {\n \"charts\": {\n \"data_type\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Type\"\n },\n \"data_format\": {\n \"chartType\": \"stackedBar\",\n \"title\": \"File Format\"\n }\n },\n \"filters\": {\n \"tabs\": [\n {\n \"title\": \"File\",\n \"fields\": [\n \"project_id\",\n \"data_type\",\n \"data_format\"\n ]\n }\n ]\n },\n \"table\": {\n \"enabled\": true,\n \"fields\": [\n \"project_id\",\n \"file_name\",\n \"file_size\",\n \"object_id\"\n ]\n },\n \"dropdowns\": {},\n \"guppyConfig\": {\n \"dataType\": \"file\",\n \"fieldMapping\": [\n { \"field\": \"object_id\", \"name\": \"GUID\" }\n ],\n \"nodeCountTitle\": \"Files\",\n \"manifestMapping\": {\n \"resourceIndexType\": \"case\",\n \"resourceIdField\": \"_case_id\",\n \"referenceIdFieldInResourceIndex\": \"object_id\",\n \"referenceIdFieldInDataIndex\": \"object_id\"\n },\n \"accessibleFieldCheckList\": [\"_case_id\"],\n \"accessibleValidationField\": \"_case_id\",\n \"downloadAccessor\": \"object_id\"\n }\n }\n}\n"` | multiline string - gitops.json | | gitops.logo | string | `"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"` | - logo in base64 | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.frontendRoot | string | `"portal"` | Which app will be served on /. Needs be set to portal for portal, or "gen3ff" for frontendframework. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -52,22 +62,20 @@ A Helm chart for gen3 data-portal | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private`. | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | +| global.tierAccessLimit | int | `"1000"` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | | image | map | `{"pullPolicy":"IfNotPresent","repository":"quay.io/cdis/data-portal","tag":"master"}` | Docker image information. | | image.pullPolicy | string | `"IfNotPresent"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/data-portal"` | Docker repository. | | image.tag | string | `"master"` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | -| labels | map | `{"app":"portal","public":"yes"}` | Labels for the portal service. | -| labels.app | string | `"portal"` | The application name. | -| labels.public | string | `"yes"` | Grants ingress from the revproxy service for pods labeled with public=yes | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | | nodeSelector | map | `{}` | Node selector to apply to the pod | +| partOf | string | `"Front-End"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod | | podSecurityContext | map | `{}` | Security context to apply to the pod | -| portalApp | string | `"gitops"` | | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"memory":"4096Mi"},"requests":{"cpu":2,"memory":"4096Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"memory":"4096Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -77,7 +85,7 @@ A Helm chart for gen3 data-portal | resources.requests.memory | string | `"4096Mi"` | The amount of memory requested | | revisionHistoryLimit | int | `2` | Number of old revisions to retain | | securityContext | map | `{}` | Security context to apply to the container | -| selectorLabels | map | `{"app":"portal"}` | Labels to use for selecting the deployment. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -89,6 +97,3 @@ A Helm chart for gen3 data-portal | strategy.rollingUpdate.maxSurge | int | `2` | Number of additional replicas to add during rollout. | | strategy.rollingUpdate.maxUnavailable | int | `"25%"` | Maximum amount of pods that can be unavailable during the update. | | tolerations | list | `[]` | Tolerations to apply to the pod | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/portal/defaults/gitops.json b/helm/portal/defaults/gitops.json index 2f2486e2..dfcd0dcb 100644 --- a/helm/portal/defaults/gitops.json +++ b/helm/portal/defaults/gitops.json @@ -112,7 +112,7 @@ "subTitle": "Cross Environment Datasets", "text": "The website combines open access datasets from multiple disciplines to create clean, easy to navigate visualizations for data-driven discovery within the fields of allergy and infectious diseases.", "contact": "If you have any questions about access or the registration process, please contact ", - "email": "support@datacommons.io" + "email": "support@gen3.org" }, "footerLogos": [ { diff --git a/helm/portal/templates/_helpers.tpl b/helm/portal/templates/_helpers.tpl index c9d82c78..391aa496 100644 --- a/helm/portal/templates/_helpers.tpl +++ b/helm/portal/templates/_helpers.tpl @@ -34,20 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "portal.labels" -}} -helm.sh/chart: {{ include "portal.chart" . }} -{{ include "portal.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "portal.selectorLabels" -}} -app.kubernetes.io/name: {{ include "portal.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* diff --git a/helm/portal/templates/deployment.yaml b/helm/portal/templates/deployment.yaml index 9229eda4..07d4da19 100644 --- a/helm/portal/templates/deployment.yaml +++ b/helm/portal/templates/deployment.yaml @@ -2,7 +2,12 @@ apiVersion: apps/v1 kind: Deployment metadata: name: portal-deployment + labels: + {{- include "portal.labels" . | nindent 4 }} spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} selector: matchLabels: {{- include "portal.selectorLabels" . | nindent 6 }} @@ -11,13 +16,23 @@ spec: {{- toYaml .Values.strategy | nindent 8 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: - {{- include "portal.labels" . | nindent 8 }} + public: "yes" + {{- include "portal.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} @@ -32,7 +47,33 @@ spec: secretName: "portal-sponsor-config" - name: privacy-policy configMap: - name: "privacy-policy" + name: "privacy-policy" + optional: true + {{- if .Values.extraImages }} + - name: extra-images-config + configMap: + name: portal-extra-images + - name: extra-images + emptyDir: {} + initContainers: + - name: init + # image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + image: "quay.io/prometheus/busybox:latest" + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + - name: extra-images-config + mountPath: /data-portal/custom/config.txt + subPath: config.txt + - name: extra-images + mountPath: /data-portal/custom/images + command: + - sh + - -c + - | + cd /data-portal/custom/images/; + cat /data-portal/custom/config.txt; + xargs -a /data-portal/custom/config.txt -I {} wget {} + {{- end }} containers: - name: portal image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" @@ -47,10 +88,14 @@ spec: # failureThreshold: 30 readinessProbe: httpGet: + {{- if eq "gen3ff" .Values.global.frontendRoot }} + path: /portal + {{- else }} path: / + {{- end }} port: 80 - initialDelaySeconds: 30 - periodSeconds: 60 + initialDelaySeconds: 5 + periodSeconds: 10 timeoutSeconds: 30 resources: {{- toYaml .Values.resources | nindent 12 }} @@ -70,10 +115,9 @@ spec: - name: NODE_ENV value: "dev" - name: APP - value: {{ .Values.portalApp | quote }} + value: {{ .Values.global.portalApp | quote }} - name: GEN3_BUNDLE - # optional: true - value: "" + value: {{ .Values.gitops.gen3Bundle | quote }} - name: LOGOUT_INACTIVE_USERS valueFrom: configMapKeyRef: @@ -90,12 +134,8 @@ spec: valueFrom: configMapKeyRef: name: manifest-global - # acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` key: tier_access_level - # for now making it optional so won't break anything optional: true -#needed to be adjusted to use the gen3 umbrella chart or local var ^ -#adding a var in helpers.tpl for later- Elise - name: TIER_ACCESS_LIMIT valueFrom: configMapKeyRef: @@ -158,11 +198,19 @@ spec: {{- with .Values.dataUploadBucket }} - name: DATA_UPLOAD_BUCKET value: {{ . }} + {{- end }} + {{- if eq "gen3ff" .Values.global.frontendRoot }} + - name: BASENAME + value: /portal {{- end }} # S3 bucket name for data upload, for setting up CSP #GEN3_DATA_UPLOAD_BUCKET|-value: ""-| # - name: BASENAME volumeMounts: + {{- if .Values.extraImages }} + - name: extra-images + mountPath: /data-portal/custom/images + {{- end }} - name: "config-volume" mountPath: "/data-portal/data/config/gitops.json" subPath: "gitops.json" diff --git a/helm/portal/templates/extra-images-conf.yaml b/helm/portal/templates/extra-images-conf.yaml new file mode 100644 index 00000000..a72d703a --- /dev/null +++ b/helm/portal/templates/extra-images-conf.yaml @@ -0,0 +1,11 @@ +{{- with .Values.extraImages }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: portal-extra-images +data: + config.txt: + {{- range . }} + {{ .url }} + {{ end }} + {{- end }} \ No newline at end of file diff --git a/helm/portal/templates/pdb.yaml b/helm/portal/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/portal/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/portal/templates/secret.yaml b/helm/portal/templates/secret.yaml index 16a7c037..2a669ee4 100644 --- a/helm/portal/templates/secret.yaml +++ b/helm/portal/templates/secret.yaml @@ -4,10 +4,10 @@ metadata: name: portal-config data: {{- if .Values.gitops.createdby }} - gitops-createdby: | + gitops-createdby.png: | {{- .Values.gitops.createdby | nindent 4 }} {{- else }} - gitops-createdby: | + gitops-createdby.png: | {{- (.Files.Get "defaults/gitops-createdby.png" | b64enc) | nindent 4 }} {{- end }} {{- if .Values.gitops.css }} @@ -19,10 +19,10 @@ data: {{- end }} {{- if .Values.gitops.favicon }} gitops-favicon.ico: | - {{- .Values.gitops.favicon | b64enc | nindent 4 }} + {{- .Values.gitops.favicon | nindent 4 }} {{- else }} gitops-favicon.ico: | - {{- (.Files.Get "defaults/gitops-favicon.ico" ) | nindent 4 }} + {{- (.Files.Get "defaults/gitops-favicon.ico" | b64enc) | nindent 4 }} {{- end }} {{- if .Values.gitops.json }} gitops.json: | diff --git a/helm/portal/templates/tests/test-connection.yaml b/helm/portal/templates/tests/test-connection.yaml index e23afb6e..2a138e30 100644 --- a/helm/portal/templates/tests/test-connection.yaml +++ b/helm/portal/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "portal.fullname" . }}:{{ .Values.service.port }}'] + args: ['portal-service:{{ .Values.service.port }}'] restartPolicy: Never diff --git a/helm/portal/values.yaml b/helm/portal/values.yaml index f7a9d4d2..622c2fc3 100644 --- a/helm/portal/values.yaml +++ b/helm/portal/values.yaml @@ -1,7 +1,8 @@ # Default values for portal. # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. + +# Global configuration global: # -- (map) AWS configuration aws: @@ -13,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -41,20 +44,26 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private`. tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. + tierAccessLimit: "1000" + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (string) Which app will be served on /. Needs be set to portal for portal, or "gen3ff" for frontendframework. + frontendRoot: "portal" + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false # -- (int) Number of replicas for the deployment. replicaCount: 1 @@ -91,11 +100,13 @@ serviceAccount: podAnnotations: {} # -- (map) Security context to apply to the pod -podSecurityContext: {} +podSecurityContext: + {} # fsGroup: 2000 # -- (map) Security context to apply to the container -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -128,10 +139,6 @@ nodeSelector: {} # -- (list) Tolerations to apply to the pod tolerations: [] -# -- (map) Labels to use for selecting the deployment. -selectorLabels: - app: portal - # -- (int) Number of old revisions to retain revisionHistoryLimit: 2 @@ -144,32 +151,25 @@ strategy: # -- (int) Maximum amount of pods that can be unavailable during the update. maxUnavailable: 25% -# -- (map) Labels for the portal service. -labels: - # -- (string) The application name. - app: portal - # -- (string) Grants ingress from the revproxy service for pods labeled with public=yes - public: "yes" - # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - portal - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - portal + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (bool) Automount the default service account token automountServiceAccountToken: false @@ -187,10 +187,26 @@ resources: # -- (string) The maximum amount of memory the container can use memory: 4096Mi -portalApp: "gitops" +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Front-End" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: + +# -- (map) Extra images to be mounted in the deployment. +extraImages: + # - url: https://raw.githubusercontent.com/uc-cdis/gen3-helm/master/docs/images/gen3-blue-dark.png # -- (map) GitOps configuration for portal gitops: + # -- (string) + gen3Bundle: "" # -- (string) multiline string - gitops.json json: | { @@ -320,7 +336,7 @@ gitops: "subTitle": "Explore, Analyze, and Share Data", "text": "This website supports the management, analysis and sharing of human disease data for the research community and aims to advance basic understanding of the genetic basis of complex traits and accelerate discovery and development of therapies, diagnostic tests, and other technologies for diseases like cancer.", "contact": "If you have any questions about access or the registration process, please contact ", - "email": "support@datacommons.io" + "email": "support@gen3.org" }, "certs": {}, "footerLogos": [ @@ -351,7 +367,7 @@ gitops: "chartType": "count", "title": "Projects" }, - "case_id": { + "_case_id": { "chartType": "count", "title": "Cases" }, @@ -392,11 +408,11 @@ gitops: "manifestMapping": { "resourceIndexType": "file", "resourceIdField": "object_id", - "referenceIdFieldInResourceIndex": "case_id", + "referenceIdFieldInResourceIndex": "_case_id", "referenceIdFieldInDataIndex": "node_id" }, - "accessibleFieldCheckList": ["case_id"], - "accessibleValidationField": "case_id" + "accessibleFieldCheckList": ["_case_id"], + "accessibleValidationField": "_case_id" } }, "fileExplorerConfig": { @@ -440,26 +456,23 @@ gitops: "nodeCountTitle": "Files", "manifestMapping": { "resourceIndexType": "case", - "resourceIdField": "case_id", + "resourceIdField": "_case_id", "referenceIdFieldInResourceIndex": "object_id", "referenceIdFieldInDataIndex": "object_id" }, - "accessibleFieldCheckList": ["case_id"], - "accessibleValidationField": "case_id", + "accessibleFieldCheckList": ["_case_id"], + "accessibleValidationField": "_case_id", "downloadAccessor": "object_id" } } } # -- (string) - favicon in base64 - favicon: - "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" + favicon: "AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQv3IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1MiCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwKg0Nd6yqf+8pi7D3rKp/96yqf/esqn/3rKp/76qNMPEpU2QxbFJNwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/7WfF3cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMWySQAAAAAA3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/TrIS0AAAAAL+nLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACxmAIAxrhKBregGtLesqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/2MyPCLGaCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAs5kJANqvn0vesqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/18l+GwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKuSAADq5L8H3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/z79qBca0SwAAAAAAAAAAAAAAAAAAAAAAAAAAAN6yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf+4oR3YAAAAAAAAAAAAAAAAAAAAAAAAAAC4oBlZ3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/AqC/N3rKp/96yqf+/rD3M3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf+4oyBkAAAAAAAAAAAAAAAAAAAAAN6yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf+9qDAqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzb1oH96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/8qoYv8AAAAAAAAAALefHQC4oB5X3rKp/96yqf/esqn/AAAAAAAAAADm3bsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOHbrAAAAAAA6ePTEd6yqf/esqn/3rKp/8CsNngAAAAAAAAAAN6yqf/esqn/3rKp/////xIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADq4bwA08V3EN6yqf/esqn/3rKp/wAAAAAAAAAA3rKp/96yqf+6nyfZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3rKp/96yqf/esqn/AAAAALyjJDbesqn/3rKp/7ihIc0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADFpE7l3rKp/96yqf/esqn/wq0+Wd6yqf/esqn/3rKp/wAAAADPwW4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC7pCAAAAAAAN6yqf/esqn/3rKp/8CsOVK6oyF63rKp/96yqf/esqn/uqQqxAAAAAC7oyQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtZ8WAAAAAADesqn/3rKp/96yqf/esqn/3rKp/7ukIHresqn/3rKp/96yqf/esqn/3rKp/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3rKp/96yqf/esqn/3rKp/96yqf/esqn/wK1BXN6yqf/esqn/3rKp/96yqf/esqn/uKAYUgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL+oO1Hesqn/3rKp/96yqf/esqn/3rKp/76pLXq3nx023rKp/96yqf/esqn/3rKp/96yqf/esqn/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt58l896yqf/esqn/3rKp/96yqf/esqn/3rKp/wAAAADesqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/xrRRVQAAAADYzYkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM67agAAAAAAybZYUt6yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/9+/UXAAAAAN6yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN6yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/wAAAACznRMAtJ4ZV96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADesqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/ArDZ4AAAAAAAAAAAAAAAA3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/yqdi/wAAAAAAAAAAAAAAAAAAAADHplZ93rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/6Ny8U+bauVDesqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf+5oyBkAAAAAAAAAAAAAAAAAAAAAAAAAADesqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/t6Ec1wAAAAAAAAAAAAAAAAAAAAAAAAAAs5sWAOHUlQfesqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/OxHUFxbRJAAAAAAAAAAAAAAAAAAAAAAAAAAAAsJkFAN6yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/29COIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAr5YBAN6yqf+7pSf43rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/uaMf+d2xp6MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyrhUAAAAAAC7pil73rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/7miH38AAAAAxrJDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADi150b2K6T4N6yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/96yqf/esqn/3rKp/7mjI5zUxHAaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOnftwAAAAAAAAAAAN6yqf/esqn/3rKp/7egG+e2nxf/uKAk/7mjIvPesqn/3rKp/7agGEAAAAAAAAAAANnOjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////wD///gAP//gAAf/wAAD/4AAAf8AAAD+AAAAfgAAAHwA/wA8f//+OP///xj///8Y////CP///xh///4IP//8CD///Bgf//gID//wGAP/wBwB/4A8AP8APgAYAH4AAAB/AAAA/wAAAf+AAAH/8AAP//" # -- (string) - multiline string - gitops.css css: | /* gitops default css */ # -- (string) - logo in base64 - logo: - "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" + logo: "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" # -- (string) - createdby.png - base64 - createdby: - "iVBORw0KGgoAAAANSUhEUgAAAfQAAACxCAYAAAAyNE/hAAAAAXNSR0IArs4c6QAAQABJREFUeAHtnQe8FcX1xwVFsHfsBcUudrErKvau2ILGHnuP0fw1xlhi7LG3REXsjSjYC1gRe0ssqFQVFHtB+v/7e9x9zJ03u3f3lvduOefz+b2dOXPmzMxv9+6Zmd17X7uZSpRp06YthYsNSnRj1RuTgQHt2rX7NW7oXFu6rpaMKy+gfwHfYwrYWLExYAwYA3XDwCxlGMmm+Li9DH7MReMx0IUhD08Y9smU7ZVQnlS0A4WPJRlUYxmTmJnpV3ewfe64MMfOYCEwGXwHvgSvg1fBI0xcxnI0MQYyM8D1tjKVTgQ6rgjmA7rGPgBXcG3142hSIwyUI6DXyFCtm8ZA9TLAjXVxencG2BfophqSDihnA4uBdcARYAp1B3L8JzffRziaGAMFGeCaaYeRrrczQUevgiaQwhBgAd0jp5qz7au5c9Y3Y6DeGeDGugC4lHF+Ao4CccE8jgqt6HuCAfh5GWiVZWIMFGJgPwzOBX4wL1TPyquYAVuhV/HJsa7VNwME37UY4X+A3kMph2yIkzfxewqr9evL4bDWfcDF6YxheWccz8HNbU6+4ZJw0olBX9BwA2+AAVtAb4CTbEOsPga4qWpr/WagLfRCMhGDr4C23OfPHTkEZXa01+F/K44HELx+C1o1jnInhrqxM9wppBs6oDN+7eiEJpH/y3Gj621d8DkwqSEG2jqgaxWhD5c+ZCb1zcDUwPC+COhclVZXF7qKDOmhGWxb1ZRgexAN3pLQqF5Kuh/ombje1v82sqWunn2uAvQyql4Y7AHaA196oZgf+x2oP8EvtHxDM7BsYPR6wXIzu1YCzDSSihtGb1CsbN1IXNlYjQE+KJuBCTEfmPHo/wHmTcsUtsuDu0CcHJfWVz3aQcqLHjE31uM4s4wJPv7pcaLsHll8mG11MhCa2bdmT/U1HBNjoCEY4Kapr+k9CGYNDHgUug1ZIZ0Ovg+UB1XYDgV6welA8ItnpDeUr/N0ljUGFg1QoJcyTWqcgbYO6DVOn3XfGMjEwGVYLxCo8Q669QjMbwfKUqmoexuGeiku+jEdvWy3D/pGnzTrBTCTfAZC9/3QI7H8WparegZCJ7bqO20dNAZqjQFW53oxa7dAv8ei25nAq2NJgo/3cKDHWHeAvclPKslhjVeG8w4MYdkaH4Z13xhIzUBbvxSXuqNmaAzUOAOhl/umMaZeBF5tt5dF8PU+jvYvxRmBUBN9BcJFgL4Xr5fyNOH4FP/qc9mFNvWyn34wR+3OBfRWvx4n/MAxs+T8nUNF9b/sgn/dO5cD+iW/ecA4MIb+DuNYEcmdlyVwLo70bYfnaU/tVo3QxznpzDJA14440o7RSPrZ/GIn+bIL7eociJclwTjae7nsjTSCQ4gs5aW4LRqBIxtjYzPAZ6QLCMm91cQMHdwY3Aq+DnUW3VegL9ggS7+xvxO856Ep0KJbEVwOxgBfJqF4HuwNFPATBZvzgdp5H/wAQvItSr8vhyY6dgqpuy24G3wPQjIa5U2gm1OtYBL7p4Dbr7eiSujXAtcD9d2VzSKbpCMV9G0H13eIm489G9mflOQ3KsNuFnAIeAKEXvicin4wOBVospZasP8dcPuu9AFywHF2cDx4FbjSP3UDZpjPACz2dpnMmLaAnk+n5eqQAT4Tx8V8LlaohuHSt6VAv5g+xqkfokCrsIKC3ZCAk8XR6Y3+iYGykEpvq+vnSGOF8j6hiil0p8U6zRXgY2XwTApfkYl+kvc2oJVjQcFuZFQxd5zEcQ6g3xSQr5CkDeidQ5VT6EK7Snljwcf24MMUviKTsST+kOckIYNt6LNzIvodwAgQkoYN6O0TuLQiY8AYKA8DOwXcvMW24McBfauquBvqRTp9Bzn0fD+pL7tQ+Br110kySijTd+wVSPWcO43oHQRNDLQt36pCm9vT4CtgywwN696qlaT6vHyGepGp6g8GR4KqvE8zrjPom85jlp8b1qTsBupqp6fYn509CB8DwFLAxGGgKi8Up3+WNAbqgYG1AoPQjbBNhRuqfpxmINBz4JBMQDkcxL1cp+e5T+OnmJ2GNaibVZahgra7W+3dH9ranTYVPOYGIfkV5XAwJVSITsFuIH5S7WY4PnRvzrRt79SteJLxXEMj54GCj0JiOqP3PB7Dz6wx5UlqXTvFtpvkt+bLWu2DUfNM2QCMgSIY4IY1M9UWCFR9N6BrNRX96kJjDwJ/laQgfim4BQxjF0HbvbpPdAVaLR4D3PvGvOQfxmYdbH8hXYzcQyVNcMTJz0A37J3BQcAXTUK08lX/fHkUhV7ei0QTjv2iTO6oNp7wdEO8fFOWMa1Joi/wFz4/oTsfqN8jGDemTYFJwftkcCBwA87i5PVIYxPxSTqrTKPCzeBp8B74EWhsH4E0oknHxY7hrqT9SZj49F+we86p05xkHCeSObpZMSOha0f97Ae0+zQZLA12BLp25geubEHmBnCwq8yY/hD764DOq9pUG/rMmRTDACfXnqEXQ5zVaQgG+HwsAkKS6vlnpUiiQ6HnwXoxatmkNilfBXwOfDkzrh6G2nYOiV7y2jOhnp6Tfheo+EFcHVdPvU0DdW90beLS1GsP9HKdL3oBa9G4etJTvgEIvTR3aFw97P1n6KiaRFxvFVevGD3+7pvuOu/vaml8UWNtMDmv5vTMGxy6xPmgTC/mxb2n8fuEeqFn6NNbnDbtahKzxdU1fREMQKgF9CJ4syqNwQCfDwXAkLTZdiqd2S7QIQVXf9UWPEnYrQ9+83wogM0VqoA+LqDvH7J3ddQ9EISk4HNpKpUS0A8JNKqgm2rrHLsdgXY3XFH94OoRfVxA7+nyUY40bZUS0EMTwVfwOUehvmHTHtwBfBmFIhiY0ccF9CcKtdeI5f5WUiNyYGM2BirJQNz3b5u+tlXJhhN8Hx8oO5XtYG1ZFhTstEV9pWc4D/ltPF1SVj7uSDJQGW314fBiwE5b75WU4wLOj6I/YwL6Firs9AjhTq9gSfLre7qk7EP4eTrJoDXLCK4b0t6WXps/kN+TfhZ83ILNVGwPA9omd2UJMnpMkVYmY3hSWuNGsrOA3khn28baFgx8TaO6kfkS9yKab1fWPDdlraT8m/L36O7K2FDIftsMPm7nBq9nw2lkYMBIwbEiAkfyrefnrgwn85irSJEulaO+KdpoTZPdAo1dznn8PKAPqrAdT8FfAoV6+TCtvIGf/6U1biQ79+WWRhq3jdUYaBUGuPFo21UvG3X2GtSLX/d5utbIrkcjHb2GFNAvpZ+eumBWb793cKwKboM7tkOddKGkXgTzZQFfUcb8JgFferHsmowcudxELivFUeS/ksfQhK2YScdDdFIvP87pdLYH3M7K52Wio4tLZrl24nzUpd4Cel2eVhtUlTHwFv3xb4Y7ojuzDfoZeqFrGfqht5BLFX/SkuTvk6RCryz02KKTZ1PObIijVWhAKFUqxVGp/UpTf2nPSD91+5mnK5iljn405zUM9ZZ7JLOSEO8jIkXC0QJ6DDm25R5DjKmNgTIy0D/ga01uaisG9JVWVXJlG3zhK2ZAevZarbJgBTuWlqPJBD7tClSFcK1qV2derzOp3ifw6kTZUN20j6F+jJzYMZ+Bmlmhc0E9Stf9VU7+aBo3twEffs14mwW+9F3QuZsV0xOPYHeZq8PucPL7ujqlsdvK1WGn2fPtrq7ItF708V+oKtJVzVTTD5NcHejteej2CugrqapkIP28kh1vRd96BFEpqVWO9HhlCnAnJAryxUqo7oRinVm96QzUTECnu9pNsB2F8JXbLqDeDJ1+ZMGV0PZYVwz8l6TcOlF69pR2kX3c0X/DNc6ubvRMYPSb088zIJ0TV3qh35TyF1xlqWl8zo7PuNXd2IB/9e2VgD6r6p2sFarUPsSRXogLPcvPOoQXs1aoBnuuJ/2Dla/pi/u1vcXRzUyZAn1WWSpQIbRqD5iZKo6BWgrocWMwvTFQCwycRicHBzqq7wSvx01xVKAsswpfmqA9yfFQfIbeDn+Xcr395k4CJ2Cr/plMZyA0MfnaOJrpA+hxA7p2ALuD0HU9ncnAX65NLTTW9oq+Ia8Jg0kJDNiKtwTyrKoxkJYBgoFWwA8E7BdG15+bnI4lCT664eA50AUMIL+F75B+aBX0hqffElv3Ru0VN1YWjt5mxP7W+G5wNFtjMdFitA+30Mw00wkBXSHVMRj4sWcAvE8tVNHKkxnwSU22tlJjwBgohYFTqBxahegrbPpJUR2LEuoeRkX9WMtiOQd6RBIM6ujvytlEBz0X1b8edZ+PRmXBI7b6AfPbwE5Bg+pQhr6Hl/ae53Ok1Wiqn42Nhg43+j/h/wEbRboaP2pCqmfpruh/1af+QSFsV6Tyqa6DXNrnO2BiqkIMNMKWu54l3leIiBov1/ecfdEqYx5POcLLKzsa+Cu2gNlMemEljV2orqsb6WYaKc0KRM/Sd2PMz4KO3tj1THEw5VdwvBDbVC9mYb+y7MHOwBcF9UOAv/V+LbqTwBIgEt2U/4m/42k7FAgjO/1WeScyN4H9wT7k9UthevGv2uTLQIeWD+hCKnH6B+C+WLo/Y/2QsZ4fquDqsNNnT0Fqe6AdkO2o97JrU2tp+q+faNW1c4LT93ak9R/wtqH8dUffIomNrreHwFxe4SDqPuHpLNsWDHCSWuW33GnncVCMlOXZZFtwa23WJwNcxAoMeskoTvS76vqf0bsAPW9sFvJaGa8M/gD0u9pJfvQsfbbmyk4CvQJxSF5GuZZj2pxErxXnnuAz4MoEMrErdcpCv+We+qth1N/KbSyXLrhaxq4D8P+RiPjq2TyohAR2J4KQ6F4UnBig7wgOAl8CV34kE7tSp2yka0zaXwkn9DRbEb5L+S33Bagf+uc8v6A/GgR3edDvGlNP/xNgzbgRUBb6LfcT4+wbXd8IK/RGP8c2/ipjgNXI7dyoxtOtPmCOQPfmQ6fVoaAVsXZHtFXfASjA61hIbsXgCNqaGDJEfw9+16Xsj175huRfo0yryfeBXoSaE3QB2gUIPWufFb0C+gBQNcIY9QMmmtAv43RKK8pH0d/C8V0gPrtj22KHA512LNahXDsRrmxL5n3KXtIRfAR0zrqCXXJpDnmiVakmEuK1ZgVOvmHcuzKA54E7WdRu0DXgz5Q/zHEo0KRkGbADWAWERC9vvh0qMF0bMMDJsxV6G/BuTdY+A3x2ugF/tYuqJNFK8IA07GDXHujZeamin0SNfTZNWZus0MUBbZ+RYnCaXAWFup3AIyl8JJloV+CvwQZySsprYoUejYH+7gy0Ki9WxIneKUkUbGyFnshQfmHshzDfzHLGgDFQbgZYmeh7zWuDfwC961GqDMLB2vjtm8YRdlPBgdjqn2UkPjeP8fcd+n3wcYx8xdi0tfoyOjC62E4wrt+oq1X3lUX6+IJ62+Lnb0XWr8pqjKc/HdsUfF5EB3+hjt67uLSIulYlgQEL6AnkWJExUGkGuKl9D/5MO8uCK8BXGdtUwLkfbIEf4ZOM9fWrgOdRZz0wKGVdPQLQM+zVqHtvyjptYkb/tPrWFrEmT0UJPqYAvQi2BXgjpZOfsdNkoht1n0pZp6bMGNebdHhVoAmprsNCoknjbWBl6vYrZGzl2RmYJXsVq2EMGAPlZoAb3Fh86iWskziuA7YD3cHCoDNYCOiZ5LdAq6LXwGDwJHV/5FiS4EOBagvaV9sKgApeiwO1r5u1+vc60Bv6/bD/gWNa+TeGT3rGWXYkhlFXkw5X1JdUQl/fzI3rGCpsAlYEmkBpXF8CPQsvKPgZhB9NfPRym1btm4HFgM7NL0C+XgVPg4exzzJGTebmBZFMiRIVOGoC+KHnN+tEsql67jrQc3NNXsTJTmAFsAjoAMaA4eAxoJ99/oxjFhGf/rkfksWB2WZggBNpz9Az8GWmxoAxYAwYA8ZAJRiwLfdKsGo+jQFjwBgwBoyBVmbAAnorE27NGQPGgDFgDBgDlWDAAnolWDWfxoAxYAwYA8ZAKzNgAb2VCbfmjAFjwBgwBoyBSjBgAb0SrJpPY8AYMAaMAWOglRmwgN7KhFtzxoAxYAwYA8ZAJRiwgF4JVs2nMWAMGAPGgDHQygxYQG9lwq05Y8AYMAaMAWOgEgxYQK8Eq+bTGDAGjAFjwBhoZQYsoLcy4dacMWAMGAPGgDFQCQYsoFeCVfNpDBgDxoAxYAy0MgPt+C12/debUv5Ji/6Bw1pF9vsV6o1LWXd97PRPELKK/gGD/lmCiTEgBr7iH0QcalQYA8aAMVBvDCig618hzlpvA7PxGAMxDAwnoHeJKTO1MWAMGAM1y4BtudfsqbOOGwPGgDFgDBgDMxiwgD6DC0sZA8aAMWAMGAM1y4AF9Jo9ddZxY8AYMAaMAWNgBgN6Ge5rUMoz9E7Un2uGy0ypH7CemLLGvNh1SGnrmk0l842rsHTVMzAHPZy96ntpHTQGjAFjoJ4Y4KW63qBY2SItFzTweJGNjErbhtlVBwOc53OLPNdpqg2rjlFaL4wBY8AYKC8DpXxdrbw9MW/GQB0zwExDOw6tsevwPW/xT65jKqt2aJzj+emc+xhzKufi26rtsHWsJhjgutI1pWvLlYlcWz+6CqUtoPuMWN4YqAwDf8Ht6ZVxned1E3Iv5Wks01oMfEpDejQYyVgSi0SZejkSYHoxlnWBdrv6EFj0Wx8mlWNgMVz7O836bZWt/SYtoPuMWN4YMAaMAWMgyADB/FYKDnQK/4BuQ4J62nehnKqWLDcDFtDLzaj5MwaMAWOgChkg8OqXNhdwujaBQJz6nRLqr0RdN5jL1dpgb3C7MiZty4D7vKdte2KtGwPGgDFgDFSSgT/i/AMH/TM2tkSM/ZIxelO3MgO2Qm9lwq25hmVgNCN/LcXo58Oma8DudXTTAnpf9ZOvsLwxUCYG3sHPeDCb52+wl7dsGzFgAb2NiLdmG4sBtjavYcRCorCtuTsGDwaMNsLHpIDeVMZAqzDA9fc116f+sdFNQL8Vod/4uAD9II4mVcCABfQqOAnWBWPAGDAGaoEBgvddBPVH6auep48gP6YW+t0ofbSA3ihn2sZpDBgDjc5AWd6ZIojrFz6HNDqZ1Tj+spzgahyY9ckYMAaMAWMgj4GF83KWqTsGLKDX3Sm1ARkDxoAxkM8A2+T6lcJt8rWWqzcGbMu93s6ojafhGeDmvRskzO0R0Zet0qa35CnvQtm+YA2wLFgQHET58xxjhXqLUrgTWBksB1RXPzOrf/Ckt/CfwsdAjqkFn/q1K/l1pR9+mt7Wp1y/tKa+6pfJ1J5+iU3tfQQeB49gO4FjUYL/1ai4F9gQqK12YCQYAfSLew9HfSFdUaEvM9PAlmAD0DUH/eSn/rnUMCBu1Z9xHBMFXytgID8Sff/898Bfoc+LnfSujMb/s67CTWO/K3n31/BUrGtLL8ilFvwsjfEuYDOg869r8DugZ/Lazn8In/qKXWrBZw+Ml/Iq9MeP/M5EeWcO+4DuQNeSvlEiLoeC6Fr6lXTRQhvyuTNYFegc6nOia0rtvAP0C29P0KcpHKtPGID9c5bqOy013SOuqYb95yyMfXcQkg5pTyqVPww46IBuefBIoEyq7eL8U7YcuA9MAYXkHQy2jfPl67F9MuBQ7S0KbgeTA+WuaiSZ3/l+C+WpsxB4wHUUk/4e/UmgfQqf33k+Ur0wJt/gePCFVz+U/QXlpSDx/wJQfkSocgrdw0njpP4HAR+p/1sndVcCDwd8hFQvoowmJUndairDtl/AyeroOoObwaRAuav6nMzBBRsKGFBvEfAvMBEUkk8w2C/gJqjCdomAw6dCxgUv0lAl0xkDxkDNMaAb1btghyw950ayP/ZvgV4gzf1ideweo965HIuVHan4P9AbaNWaJPpRkzto7zKglVBBwU6r17fBHgWNZ5ppHmwuAwOpp1VkWQWfi+PwGXAF0Eq1kCiQnwxepa5WmTUj9Pd0Ovse0Ao2jWyM0WDqXQUKXQdx/npS8F+g67/QjrR+M12B/7os7WGrybBW34eCNBPv5bC7k3qaABQ7Lly0lDQf0Ja1TGMMGAO1xsANdLhTlk5zs7kY+75griz1sFVgPZP6Z2WsF5kruPnbulFZ3PEkCs6MK4z09EmPIp4EunlnEW0Na1JTNqEva+DsddCjCKfa0n0KH9pGrmqhj7OAW+nkBaBQUA2N5ViUj+Kj0HU4LVD5UnRZJ2JHUuf8gK8WKvp0AspHQTHnQRMAfS7LJsWQW7bGzZExYAy0CQMTaVUrYK3YvwdLgG9As3Cj2prMH5sV+YmRZPXrYF8C3cgUmBRgfPkbfp7heeFLfkGG/GRsPwJa2X0HtAOwFghtOZ9Ne3o++SrlcaKgr+e3vmgV9zL4FawGegB39XQ2fq9HV065FWeLBBxOQvcc+AzoXK0ENgRzAFeWJaOAsLurdNJ+gAvtYPg2ft5xV3RSE7QDY2p/il7Xx9dgPrAe6AZ82QaFvgO/C+dhql+Yy4fG55rqufXHQNeS2ouupTlJ+/In2nqMtnQegkK5zsvlINSu/D8PRgNNTnVNrQN8ORQ/T9PO3X5Bm+TpjD1DbxPm67dRril7hg4JnnRIe8apF3qGHrnTqk4BvKBgd15UKXfU8/GtQYsbGLpeYCzw5cWkhjAOPUOPfLxBQi/g5Qm6hUH/yMg7Ppxn7GWwHePZK6ubd96YyOu55S1A8lfPTTCLXaZn6NjrefKXIJLfSGgStIDfALrFwIMgJAr2iUKlmQMV30+sFCjER6Zn6NjHPct/n7KtAk3o5bW1gZ6fh+TCUB3pMI7jR37eBQrgeYJuAXA/CIkehSQKlTS+qU7lz0jvCVosltFtCT4FvgxH0T6uIcp0LfryVJx9SXpasYBeEoNW2WeAa8oCuv/xnTatHAH9RtzmBS6fez/vnAutjlrcpFx7yjcDk4EvS7p2bhrDuIA+gLLEMVOu552+6Obqv83d1CR63bx9Ger2x09jXDBYRnWwzRTQVY86UVD/hvTaka/QkXIF5ReAL1oBJwoVWj2g06b41kuFvmj73N9tyOs/5dqm/7dfkbyurxaTPFVGH3opTi6eAR3zGvAylF8lw4B08UxbZKkTBfVnSRcal863Xmz0ZaMWjnMKDFMH9NhZQZxz0xsDxkBNMqAt9dPY2su0pYr9X6inZ32/J63t71ihXFuMDwUMYm9WAVupJoDj8aet5yTRy2Ha9ndFE5bNXYWTDvlLnDTQBz1aqJjg/0Oc9wB7kn4zqSHKtWV8asBm44CuGlRn0Il5vI7oZcRejOUXT5+XpVzX2uHgybyC6Y9BLvB0UTZ0bcvPcfjTNZUkemFvRMCgR0CXp8L3DSh6g91TjEvn+6Y8B9MzZTmHiTPuQKOmchhg5rQu2UccVZTUze+JKKMjtnqut7SrS0gvQ/3xUTl1teLQ885al38yrrgPY62Prdr7fync6xl0ZqHezRkqaRt3D89+US9fKHsrber5caJgo9VfH4x0M3ZlDTL3ugqlsf8R+29Jzu+ULY1O27hnUV7opu9UK1+Sdj/Cm5BGPsBIz5DdxVhWftO0U5INnKp/+wecHM549Z5CQcFOuy2HYTgUdHQq7IR+fsp1Ll3RZM6Xu7HT+yKJgo1Wzf/C6FzPcE0vH8xS/65gQVipZ/i+LOIrislbQC+GtRl19P1LvRTki3vxRWULkgjZRuXu0b8w9eFIW9f1U23pxO2oautsnfXnrXKOh5vf3PjTiz4u9DKTrnNfZvMVBfJZ+hq6OYb6EDWpl4+OjjK54584HsCYLud4OzfnL73yVs/SF33muwKXX6WXB24wJ9vi35lK19ayIR1YyOvEILh93dMlZrEfBReanB3gGOplxR1BX0enZGiFXuq11OJ9Bq/N2Cz9np3CVYF/DhcLVJJtyWIBvWQKzYExUBMMaJVTtHBzUnDZDWwBdINaClRKsvT100An5gnoItVFJPYF7ipdZVrlquxCxvoqx36gDwFlDMeKS+7mrze5dwJrgVVAJ1Crskag448FdGlUj2PkBnTV0crZD+jS+1LJaymvLc7hEih2BT2BJrfLAn9xhqpyYgG9ctyaZ2OgmhgYXUxnuEmtT70bwerF1C+yzqgM9fRc2ZfYmygBegRj0sTkfhDa9VJdjVk4F9sHOOpR0RCOZRf8K2hfAg4BWXcyyt6fMjoMPQYITb7SNBmql3aLumLXUtRxzuGKpP8NNo50bXX0t27aqh/WrjFgDFSWAT13zSTcqE6hwgsgbTDXi05fZGokbJy5r2E3YS3BWWPSLoNW5OPCVk1avTCn1fxguEj9S3QJ/vKK8LkCCu0GHAPSBHNtKQ8HoUkM6qqSOQO9+TmgS6MK1Qv5D/mq6LXEOexNo2+AtMF8ArZZJhmhMcXqbIUeS02qgq+w6hOwHBnQaabvP1MKmDWp9GamKxPJ6CbUGqKVid4NMGlgBrhR7cLwL4mh4Bv0HwSg6/44UPBrVNi0qRDUv6YDpzHOszjuDX4HtgSha1+r9pPAj+BsULLQriYLDwI9Y/VlEopPgM/xh+jGg1/BzKCaRfdGX9Le/9LUC/n361U0zzlchwZ0/w+dC10r/vlT/jOgz5Ye6ZRdLKCXQCk3BX3oDkrjAttT09iFbKirG+hmobJy67hIR+FTz4JMGpQBrgGtfq4ODP8edOdzPYZeRGsyp+5ygXpVq2IsWjH1Fej73Bx3AieA7sCXP2NzPXXK8Vz9jzj3g7n8ngz0n8YUtFsI7euzWQvP1kOPeNaj77e3GFRhher58rmvaM0852Fm2tOjKB1deYbMGeBVzmHoJT1946mrW6GcadtyLyeb5ssYqA8GtmcYS3pDuY8b1L4gNpjn7PUWdk0KY/sR3AnWZwA9gb8K1OpdW/DlkCM8J5pY9KDtu0AwmOfsa4Xfgd74lNV/EtTORFbZO1BBgbMtZV0aX9vrwMvkt+H8DQHBYJ6zr9g5tIDunRHLGgPGQNPzZZ+G63yFn+dmvQC6DX19Lea5IStgnBboe8mrK3iaC79Le76fpc2PPF0ou3NIWW06xjKaPr3h9UuTxKM8XWIWrvQCo4KnK+PIKHi2pawWaPwGxp34zJ7xdKTe1oG6ZVFZQC8LjebEGKgrBlYKjEbPdQvJ+RjMW8iorcu5qe4K9My8kIS21ucoVClFud6K9kXvySQKfdZ5OTbRKKaQQDOFovFe8fxevtzZiwMO9bXA0OOMFqbYLYvy3y0K+He2ufEEilpNVexn5BR62KVSvbSAXilmza8xULsM6IUeX47jBquXw1qI9OBICg5vUVhlCvqp7wnfB/qQ1j9l6ZDQxT0DZSMCuqyqEL896Yv/TL3ZL2XaGbgDJPW32T4mMdzTL4pffee9UnIvjod4zjuR1z8I2tbT52UpXwfFC8CfdIxC988847bJhM7hUfQ79FJlUw8p60Xi/yrZXQvolWTXfBsDtcnAoEC39RyzPzelDUAnoCC+DNBLZE8DbclX9f2EvkbBXEFxFnAh+C/6E8CSQGPSPzHpCq6m7DDgywBfUUReL9NqS9oVrfz1T0SOBp1VwHEuIL4VBN4Ba0tfggwL1L0P/5uC9kD/EKU78INooFphFatoPUc+AHznWevlw8dp516giYyCvMbbAWwIbiCricBi0juiXYzf4dffaXBMWi05KNDSpuj0D1q2AjqfGtPiYBugyY0mkk16lVVCdFGbGAPGgDHgMvAAmb+B5Vwl6R1z0I1aL3E13Yg5Vr1wQ9Vk4x/AX+Euj04rPkFj0lvLcfdF/X/s1ykvSfCh3yi/FCeXe44WJn+NQPlvHMvN72B87gBc0Tl+Hmjs4kY8aeLwLShZGOtQxqKV6WPAX73uhU5Q4PuZg4JdcBdIJkC/A/8ix2oQ9UN8buh1Rt9H1wRXY9LEYzalW0uqekbdWiRYO8aAMTCDAW6aCiZaWf06Q5uX0k03LtiMzrOskoyCKF3ZBnyU0KWOlMUFc221H55QN2vRlVR4NKFSHL9fU0fBtxjRpGVMTEWNvSLxAO6fxXdPMC6mbannBHHBXIFxb/zcJsNqEPqiCcaBQOcjTuKCecU+IxU5gXGjM70xYAzUBgPcsLT62B7EBQB/ILrpngrO8QuqJc+Y9PxVz2ZvyNinN7HfgvqfZ6wXa44vTTD0jP6eWKOWBc+jWg+I68xCm1oF6wdy0rzgmNl/UgXafoHy7uDJJLtA2WvoNqH+/YGyNlXRp6F0YCugRyhpZDJGF4Ej0hgXY2MBvRjWrI4xUDkGdKPXDdtHlha1gtMq20WW+k223LAUQFYH2hr+qUnZ8o9WXdeCVbC/hKOChduu0rqRxUmor1r9pBXx5benZ61BoY+/gCMpVGDvC34MGk5X6kZ9PNiAOsMS7KIi/5wlBl58/gb2pbK2nTVpCInGNwjsh+3mQDsF8uuPGVVhof7dWGlL/dUE6yT+1a4/zgRXM4poexjYFo3QH8Txo+vlWbAfWJ86cdxQnCc67z4vSWPJq0wm07WkyvTtPQ56sfBvQJ+FkOiz0weshf1pHEPXfNIkS2PwOZePFhK3xdHCME7Bc4LelN0eV15AvyUDHFjApqmYdh4noQshq4ymjSWzVmpUe3jWKmaJCo1fvzJ2ZiHf9OFcbAraFfITUz6cPnSJKTN1DAOcEz1fXRcsB+YBX4Jh4H34TLoZYVK9wrhmpne6PywLdF1okfMNeItxaXytJvRFnzutwDsDBSe1/wH9GMux7EJ7i+B0RbBCzrkC0pu0NyKXr+iB9menAU2sFgMLgO+Britxr3RNCePRtbMmWAnMB7QdH31G4iYvmJRP4p4Xla8F82QMGAM1zwA3WAXtwTnU/HiiATCuKaSH5xCp2+RIX/RstWLPV/1B0Z4epwjP+WWtkaf9X2nnhdZoqzXaYDxa4Ws3Ie2OQtm7pRmFiTFgDBgDxoAxYAzUOAMW0Gv8BFr3jQFjwBgwBowBMWAB3a4DY8AYMAaMAWOgDhiwgF4HJ9GGYAwYA8aAMWAMWEC3a8AYMAaMAWPAGKgDBiyg18FJtCEYA8aAMWAMGAMW0O0aMAaMAWPAGDAG6oAB/Xcd/YhBuxRjmcr37JJ+tzaFi2QT+jInFkJI/B/2D9mEdPrvSfoBhUaXcZy/yY1Ogo3fGDAGjIF6ZUA/LDMSdEwxwB+wmTeFXSkm+nUw/TReOWVRnOnXhxpdVocA/UyhiTFgDBgDxkAdMmBb7nV4Um1IxoAxYAwYA43HgAX0xjvnNmJjwBgwBoyBOmTAAnodnlQbkjFgDBgDxkDjMWABvfHOuY3YGDAGjAFjoA4Z0EtxpwIdC0nw/68WqpSx/CXsr4ipsxv6pWPKktQ/U/jvJIMqKVuWfuxcJX2xbhgDxoAxYAzUGAOz8FWmq6qlz/SlP30RWghfPdP/mC0moH+P3xNbOKwyBePbhS5ZQK+y82LdMQaMAWOgVhiwLfdaOVPWT2PAGDAGjAFjIIEBC+gJ5FiRMWAMGAPGgDFQKwykeXZeK2OxfhoDxoAx0KYM5H7tcj468QuP+r5t085Y4w3HgAX0hjvlNuC2YIAbfW/a3T1D25Ow/QqMBWPASwSIjziaVBEDnNfl6M5BYFOwHpgdNAllP5EYDoaCx8ED1Rrk6atiwZJgNH3UtWdSgwxYQK/Bk2ZdrkkGVqPXe5bSc266CgwDwPXcdD8uxZfVLY0BzoX+P8TVQJO0uEeXc1HWLYc9OP6Tejdy/Cvn70eOVSH0aR86cgOYB/xM/lj616cqOmedyMRA3IWYyYkZGwPGQKswsDytnAT+y033eqD/U2DSygzAew+afA9ogpblHqrVu75x8wE+9K2WNhf6oW8OKXgrmEv0z7FuQr9CU87+1BQDWS7GmhqYddYYqGMGtLN2BBjKjXe/ah4n/dsVDHLwWDX3t1DfGEd3bPqDBQvZJpQvRtl/8HV6gk1rFW1IQ/4/5+qAbpPW6oC1Uz4GbMu9fFyaJ2OgtRmYgwbvJDBoO/9MtkmntXYHUrS3ODabO3b6oaeaFHiejY7fDeL+xbPGpX8x/SmYGWj1q39PHRL9y+q98HkZ521iyKCVdHH/Elvvb5jUGAMW0GvshFl364oBPQePW2Hrs6lgoG11vXC1A1gAhOT/UC5LcPhdlQb1UJ9rUXcCne4S6PhkdLeCc+B/lFvOOdFKXuf4KLCyU/YW6a3bOJirO4PAy2AjEMmbJJ6IMnasHQYsoNfOubKe1h8D47mh6+ZZSPRMUyu+34HzwFKBCvuiGw7+HCgzVXkY6B1wMxVdL87jQ4GymdCPQ38V5+96jn8BZ4J3QE/K2vxrbfRhCn3rSX+OBtrp+RBcjd7edIeIWhML6LV2xqy/DcmAbrwMvC833/s46u3qQwNEnE75h9jaG8oBckpRwatW2gp4vmjLPBjMXcNcgDwLP1qZP0++zYN51D/6Mp70pVHejrXLgL0UV7vnznregAxw8/0NHMbQT4sZ/rUEDT23rhZJet5cLX1M0484TvWCXGrh3PUD36SuYIbGQAYGLKBnIMtMjYFqYYCgcBF9Ca2qZkd/brX0k36sWUV9KaUrcbuZVbPSLmVwVrc+GIi7SOtjdDYKY6C+GdAqfR3QwxvmgazSLyfov+fpE7PU0Qt4XcFyYC6gleRQ8Ca+tOWfWvDVCeNeYK/UlTIY4n9ezNVP9XcR8D0YDV6hr79wLLd8HuNwA/Tvx5RVVA0HegSg8eu86RsOGv9wxt8m/aHtmeiTzsmqYGGgbwXoLXpxN4R+TeBYdqFNfZtA/40zemHxK9Kv0d6ocjZGOzPjT583vcOia06PKsT5UNr6jGPJkmtjXRxFbehdBo3n41Y5r3SgNyhWtkjLAA08XmQjZT2pafub1Y6x7VLk+NJW65amTzgbldZhEXbnpezDuUX4TltlWJo+lNuGzl0Q6ODbpbaDzzXB1IDvK9L4pt4S4BLwecBHpPqexDVAN85YoXxr8GsOetkqTiKb6PhqrFOnAGezgxPBW3GO0U8EA8DGTtWSk/hrD34Evoi3BUtuIKUD2uoMdL6Ggzj5lIJzQNxjgrzWsBsNonOho35qOLVgPyf4C9AP5sTJLxQ8BDZM6xjb2YDbL6VfV32OM4M/gFdBnLxNwT6gXdo2Q3bUXwHcDL4GcfImBSeDuUM+CumotxLoA74BcaLzdDnQZKkygnML6GWgFh4toOd4hAsL6BmuKfi6H/hScOJChWPBeL9iQl431MPiukbZdgl1k4rejfMZ6am8GRiZ5CRQdh06/0dTIpeZj/i6JdCGVJ8AraoqJvhvB/4EQpMK1EHRz7ieARI5oNwPIj+kHQh19wdjQRZ5AGPtsCQKNgrovryPYjWgYJ1WtBjMHGip0wlcCSaBtDIGw4MTB+YUYqs2NFmeDNKKzuspjpvmpD1Db6bCEsZAzTJwb6Dny/ChXz2g1+pGK90HKbsKaGs8rWgL9Sbq6utXrSa0p0cLz4IlMzZ6JPZareuXz8ohN+BkWsCRtplfo52HgVaEmYNHwGezCn/iXef4QqBHIWllDgy1K/YMPhZIWymNHf60Y3Extn1B3I/nxLnS79oPof7ycQYJeu06vALWSLDxi7ZFMYj2ZvcL4vLYakv9OXAcmCXOLqDX6lmr+dvBrIHyZpXTxtEotZ2fVnReLwm1YQE9LYVmZwxULwNP0rXQM24978sTbgK6OT0Kds8ryM/oh1L07C5OtJ3bK66wnHraORt//wBxNzwF2N9AnPSk4Mq4wix6nmEqkPwzoc7OlN0NtDX7GDgSLJZgX7CI+goKj4EkvnWuks7XxpQroM3HsVyiyeAfY5z9iv5NMBB8GWOzAvon6FPWiYZW9gpovuiaDU22Iru1SFwbZZKO9GkhyvVjO90T7HTNTU0o702ZdiKCkwH0GocmDHFtjM2Vv8bxJxAStXGdWzALjjdDUUpgX9l1mDG9Ju0nnQTX3fxuJkO6I030yGBfCdMXuBmEbriVaMt8NhgDXFvfc43rxSO9SOOKVhl5gq229t5GuXleAf8whLxuDg+B0UD3BK08fw904/ZXG3qW9wj+xlMWyX9JHBFlcketKnXzikQvRh0fZXLHb728m32HjIKVu8qW/fVAwVMvC02gL0uQ3hr8HfjjPoLyf2H3BmWlyp9xIP/7JTgSV9vlcC1t67mveL2NPozimEU0Tv9cqb7O0UVAfuVT50vnX6vfY8EywJWlyXQB37nKYtKMRytKwRd9x/4koO/ZN9/XsVeAPAscCdwAp/7cA3qCYmQclfRDPYPBh0C8a+Wua/AA4MuB9OVK+qbJRlAol49+QH3z5V0Ul4DH8aFJm2y7gv3BH4A/OVkN3YJgDGgW6mlyeh9YoVk5PSHObgP6GWed32ahznpkLgObNCunJw6h7FXstXvU9HKBnouZVJaBgttkNG/P0KdfoNoStmfoOS7SHuAs9HLQFXH1sVdAjuQsEu6NNq8aZd2BgqYv++QZBjJUGOVVilttBGpPV1F/d6CX3STa1tZNMiiUzQs+Br7cFKxQpBLnR4OkFwn99pUXh3pksWyaZrHrBUKi3++fPc4HZVrE6DMUPZfVc/cNE+xTP0PHz+IgFDOuQK9JRaxQvh7Q819fdglVwij0DD2q+zSJhUP1pKNsLxB69n17XJ1cvbOjBrzj38jHjo+y+YFeaotkGAlNoloIer3M54uuDU1IEwUbfVZ90eRieowhETo5fgXLl8aABfTp/J2XeLXmCjG1gJ6GKMcGzp4NXKK3OCYtktgrqJ/RoiCgwO7MgP8+AdM8FXVKDuhyiB8F9UdA7MQjahibDYAv2sEoq9CAAqcC+wi/sQJ5BdrrQce4DlHWAQwFvvRF0S6unqvHbk+g3ZvYYC57yrME9H9h70t/FLHBzuuTAq0vr7s2URqjuID+A2WdI7u4IzYX+g2RV/AL8od+URCacJwe14avp74C7jAQF8z1/soXwJeDfV9xeSre7Vcmf1qTPQkL6AF2yqyygD6dUAvo+ReWtr7LIrh9J991U05bsomCVfDm5lfCbq2A/+d8Oz9PnbIEdPlN29ec7bhAf7VFWnahHQXfbcA1QF8tSisvYhhcaaPfO+BEAX62LAPAXo8HEgWbVAEdOwUj/1sR2jkp2IbbAez1kp4vemSSJxjEBfRT8wxjMrn6oetgpVAV7PVuiC+PhWzjdFTWtxGSdg5C5/XZOH8hPf47A00KXdFjh5KenYfaMp0xYAy0DQOhm+qXhbrCszc9twsKd4vFgALVKRgIvvjPDP3ysuYL9HUe+rkx0PPyq2hYzyl9KfY9HN9PXp5+TQJPgmMoWBLoRafzgd4pSJKNKbw0xmDXgP5c2hgf0MeqsM97fhtrmK6gJ2adPNOHimjjbs+HsgW3m506em+goOS4Ck2aF42pHOI81Q5W5I82p4GxUT5w3Dmguz6gi1Xh/ysKB3oG3bnu5ym4feVVsqwxYAxUGQN8kPVMNrQF+UWarlJfwW99sCbQizwR5iOdJKlW90kOiimjvytTTy8JRf3UUYG0kFS8v7qh0wm9mSzoMUVXjgeD40Bop05vwt9Ivbcod2VLN0N6EtDLWm0pukZ80XfC9S2LLDJnwLhLQBdS6eXiYaGCGJ0mVVt5ZS0mooxB72Ws7tl9wnmJfYHOs02bDXF4FO0fltZBzm45z16PPJZSQNdbrKHZrGcfm9Vbd8fGliYXnEtxoVls5OHPJNaIMhmO32J7dAp7bWXdksLOTIyBamNgh0CHFFieC+ibVdxENiVzKNgJtLjJNRtWQYK+LkM3tALWKmp5UBNCQPiEjp5B/7Vr8CAIPc8+CH1zQMdW9+WFgSv6L3o/uYo2SId2gbR9LZQqoQlpyOdIeNDkJq2EOOsQqLx4QKdJWbnFP6/y36NMjXSeBXLuL8VZ7uIrNqAPpP2BadqnHc1yiwnov9LGPYXawL9mzxbQCxFl5VXFANdtOzqkoOyLvsoyxlcqn/vMnkPydKD6VS30txcd/BeYp6o7mtA5nQvGsS0m/wP+8+Id0Z3gVF+ItH9exjnlbZV0v35Y7j5MTunw15R2kZkmtmkkNKH4Ok3FtDacfy2c505rX4TdZM0ETYwBY6B2Gdibrmur3Jek54x6hrmnX8HL6zngBw6Gke7v2VQ8y03wSBq5rkBDWoV9CBQso+N5pLuBqhGC+k+MR18lvNjrlB/gQ6vKObw6bZHVbmel5PNKOU7p9+eAXVk55/xP4fz/QDuVmph+bgE9cBZNZQzUAgPcHLR9d0mgr9+juyGgb/r6F/pQMH8MvSYB74EPuPl8x7FZaGvF5kwrJWhT26AXBZpTH+8EbwD1dbRvQ91rfF2V5DVJ8kVff5sv4pyjvjr1C0ZuQFkWHUXxLzH6TiuQD+34XEo7H5ehrefL4KMUF6GxdS3FYUxdteMGdE3e/hhjm1U90gJ6VsrM3hioAga4uesrWA8Af3Wn3p3PjT9uNaUbsC+/x76vr/TybfHc+q/0YS6vHzeTP4L+xm7Rwo3ehwk9E/VctUk2FCS0Lawbuytvk9nYUeilrXXBa46utZOvBhqcwLm4MaCvNdVIOqzPjPtNiA25luZhfFpVl0vEoTs51vWtf/X6VjkaaF8OJ+bDGDAGWo8BbjJ6lvkocG/4UQc+JXFVlHGP1FNQ6OLqSD/BzaRQMFeVNbx6rZHt7jWioHcc/Y0N5jn71Tn6z6A9V8Vl4bAH0LsHxYpeRPRlXGBMD/tG5E8N6BJV9DX0AlhinYTCZyj7zSvfr5g2iqnjtVvWLPxPwaE+U65o0ny8q0iTLjC2AQEfejE9s4TasYCemUarYAy0HQN8iBXkXgFbBXqhgLcbN6cJgTKpVg3o3w/o8lS0qVXLyXnK9BndKF3R/48oGGyx0QtEK7kVSX/G2H71dKHseSFlqTr61AMfj4ALSOtX9nTDTy3Y74bxHoEKzwV0d6Ob6On1U7C7e7rYLLadKdQPDm0da5ShIMf9fV4VTRCv9nSJWfqzDAb/5bh+omHrF/YJNHk6/dQEMZXkbDU2/9qN6mvS8HWUyR2Pw347T5eYxf5ADAZxzNvBsoCeSJsVGgNtzwAf2vZAP5qib2sMAe6WXdRBBc59uOkmBeg5I2PnuBF+YwNsruxi7BXUi5HRXqVO5EPbzp5Z0z9j6egpV6I/if2gvDd1enr1Ss7itwdOFMxnzzk7kePr6ENfQ8uZzDhgp2B+Gwhx/eAMy+kpzuNIUn6gVN3b8FVwfNgsh+1zYGWg378vS1DH11nAn2jot8kV+EJjw3yGYLMeuReBHuE8Sb5qgjqcP60+AVd0vvXTtuIxUbDZCINngMY2kHyLoE4bP1N2PnBFk1f9nOs6rjKUxka/RKdrT4+e1N4T5JuDugX0EGumMwZah4GF+TDqt59DOAe9fu9bL6qNBboJ7h3TrUnoD+VmoRfbkuR/gUIFpCtpZw6/DJ2CwiBwiF+WIa+3433R6laBPVYYi7Z2P/MMFOB1c13K0+tlvznBtej7+mWl5vGrG+Z/QBTMI5fdSLxMuQK7vmuuf7DUDcwF9M861gO9wcvY9QPNN97IAUdNwO518m7yPDI+f5qUPYrP80ALf+jmA3ok8C6IAoq4fgh9wYkAdonCeRmOgR+QVOcC8DxtBFez6DUZux6bV8DiQDI3qKqgTn8ULLXT5YquN53nY0AHt0BpdEuCK0i+ABaUDlkEDEQfnYMmZe7PdRzfcBWk9aLcEPkBSucJuvZgO5SDweUgit36/DYH9VnImBgDxkDbMKAP/d9KbFrfT96TG+3zhfxgM4ybguw282yPJb8PZVrRjQK64a4KVgSl3iOG4sOXHVG8T3tPcdQLYWuCC+hff46u3ErmHFdBWquSodTVakoBXxORVYD6G9qBQF2a0C993ewovPQBLW7o6LSyKri6wsaXiSiOwr92V1oI+u9od2cKNCFQ8ItEfTgDnEz5QI6fAt3glwU9wGzAF02G5vOVRebPpd4awH98sAm6t+iTzosmKpqMdAZa3a4NQqJ+VeS8hRorpIPzD+j/77DTRFqcRjIviauB/nHUsxxHAvV7ebApmBn4onJdn3lCGxPxsRvK14DuAZHIx/HgcMo1+RaHP4AlgXYyFgMh0QSg6XNa6oc15Nx0xoAx0DoMPE8zB3KDGJ6hueOwHQK0anNlITK9XEWZ0jfh50/Av7Eth06IJHQvuoTC3kATC1dmJbODq6h0Go7v4ib7Oe30BUuVob2p+DgAvy8m+aJcz2MV1PsB/3GDAncaHn7FTtfJ/RxLFvzQpWkH4EjBeEfPoYJg1xy8ohbZcWj2wt+gFiVtqKA/AxjfwXRB166uNVc0KdrTVcSkx6DfFV/+SrzJHL3+iY9W3HqMs3iTcsYfnde0k0RdP7vj7ztVd2cgypsYA8ZA9TOgmfvOfIg3B8OzdBd7bcXqhvRbhnrjsT0ig32zKe19SebiZkWGBHXV7jbA33Yu5OUaDDRpKavQH02gVgPXA+0sFCuaGGyDv7it9jy/uXa1QtOqLavofK+Lj7IE86hx/GmSsAu4DBTDxePUWx0/gzhWndCv2+hUTzC2iM5pbGvg49WkupS/Q3l3kGgX40Pf9DgL9MCPJkZNYgE9YsKOxkB1MzCU7l0OtgK6WQwotrvUfZS6ejlJW35Johu12ukGdIMrVs6n4hkg7u37WL/0dSSF2q69A2hVmyT/pXBr6hzLUTe8sgu+fwJH4Xh1cCv4EaQVrdr0iKUbPp5JW0l22H/CQY8mtMPyNSgkozE4BXSn7geFjIspx+9UoDZ0LQ1K6eN17PTy5vZAk72qFfr3Ap1bHvwdaAJTSHT97Q92oO5XhYxVjt0XHPQc/BDwOSgkekxzN1iLuvrve1PcCrO4GUsbA8ZAxRi4D88fZvCugKSbgoLAGD64aW7iqd3jT6t8/cvFzTnuCBQ0FwS6cQ0DWj3cg90IjnrxZ2YOByntyPdOOjaJD43l7/h4gGMvsGIOs3L8Fqitt0FQqK929qe+gqHqbwQWBe3AcPAx6Iedu9I5D93CwJVU/XUrxKVpS/wdTJ+O4CgOtdJaE6jNeYFutNoG/Qa8CQaDl6mnG3JRQt1JVLyaNm/gGJ23rqQXAbqX63p5AzwNBmFfaAKEWZMcw9+OubQOmfpIO2pzC/qlvuwCtJugPs0PNOEZBV4Bj2H7Ece0on4c5Bk3bS17uqTsgxR+4hmoL6mE/v6EoV54VFDfBmwHlgIan65rfT7l7/EcDySzCfV0nm6hDU2aNwY7gRWB2ugE9BnRpOwl8Aj2ZbuO8ZcvdEJvcRYrW+R7i8/RwONFNqKLqaDgW2+mVkrmKtQBGtYbspWUboX6oHI6MKqCndBNtqDQvl48qZQoWJkYA8aAMVB3DLSvuxHZgIwBY8AYMAaMgQZkwAJ6A550G7IxYAwYA8ZA/TFgAb3+zqmNyBgwBowBY6ABGbCA3oAn3YZsDBgDxoAxUH8MWECvv3NqIzIGjAFjwBhoQAYsoDfgSbchGwPGgDFgDNQfAxbQ6++c2oiMAWPAGDAGGpABC+gNeNJtyMaAMWAMGAP1x4AF9Po7pzYiY8AYMAaMgQZkwAJ6A550G7IxYAwYA8ZA/TFgAb3+zqmNyBgwBowBY6ABGbCA3oAn3YZsDBgDxoAxUH8MWECvv3NqIzIGjAFjwBhoQAYsoDfgSbchGwPGgDFgDNQfAxbQ6++c2oiMAWPAGDAGGpABC+gNeNJtyMaAMWAMGAP1x4AF9Po7pzYiY8AYMAaMgQZkwAJ6A550G7IxYAwYA8ZA/TEwS/0NyUZkDNQGA9OmTVuDns7Srl27N9weo5+d/KquLpD+jHrfuHrqrUx+TlfnpX+mzgfYLYF+UfAh+Z9cG8pmI78a+IqyESpD15nD0krHSLNtTLl8bETZcuAh/P7o21G+B7qZKbsvKkO3H+mO6G6NdO6R8mPIj6P8nkiPbkvS6v8d6L8hvzzp7cFr5AdzbCHY7IRyWdAXm+/IL0B6xzyMRcwAAA2jSURBVBaGMxRq89Eoi/1upOeO8rnj5xxfx+4HT9+Upc7WJHT+24HXwHPYTuPYLNj0JvMt+sekJK8+za90jOg8PIFd1P8vyD/t21K+DLrNwEeUD/HLQ3nqrIh+TaBz+Bl4m7ofcswT7OZBsUueMj8zlnpP5qta5vDTFe1aYAXwFXiZev/lmCfY6RrRdd0/r4AMZRtw0PnvT/n3UTn6XqSnoOsX6dwj5YuRXwesCn4Cb2H7MsdYoc4OFIr3ftj+HGtIAbbzcVgXrA6mgv+Bp6k3hWNQqDMXBeqT6uiaeQe8QR31r3xCQ71BsbJF2p7QwONFNjIqTRv4nqtI/2mq6WQkCk52SeOoBJtuiR3IFeJ/VAltFKp6Xso+nFvIUQnlw9L0odI29L8rmJobh4Jds6BbI6dPOhzYXCGXwHhwUgWVy5TjBTm7zQM+VsuV3RCVkT86p4s7XBvZxh2peGuusgJDC6FsOBjrFpB/O1fnBFcfpSmbCBQMm4X8Dbk6Tdc76YXABJBnF1VA3wkoiH8GdKMUP+uBJMnzheFHMcaaUGii0izk1d4zAXudO02ymoX8ZPBqpCD9OkiS52WLQXsg7jRuBeA8QfcU0LW3Xl5BIIPNguBuEJJ7UWqy1yzkVwkZOrpBzcaBBHbzgT6OvZtUDNBktFnI/wzebVY4CfTRtaAJXrOgHwNa3AfQzQp075kEfBGfazc7cRLoFwW6FiVHO0V5ScragT+AH4AvI1HsmFchl0HfC4z1K5D/Cuzt1rEVusuGpY2B1mPgKJpSANGsXDcBdwUwmrzKIzmdhG5kx0YKjqHV5kXoF87ZaJJwALgdvJTT5QXMnC7L4VaMhwQqtFg5BWxKUV3MjUsrbJejVP6o8zV1H8J4L47dyL/nVVTAnRdcQtk0r+xB8ld6OmVDq6If0bsr067k/w5uz7X7KWnJKUC7CA+AK8A3YC+gFbN8JMkRFM6ZM1BbJwO1Ea14f1AZ41CwVtkz4GKwB2gS9DuT6Aluxy5vYjLdYsZfbBcg9zZYHNwDbgVDgcamCaVWx5tgtwa+vibtyqNkdD360tRHX6k8fubm8AboAv4DbgIfAU109gFHghewW5H2JpIum+BTn0XthOjciJcLga4V9Wl78Eegtleh7RGkXTmcTAeglbk+y3ET3KsoOwZoMnE8eB3MDHTuTwP98b8V/geSbhLy+uxfAL4CR4FXgK7TDcHZ4B5slqfO+aRLF5zZCh0SCshchZimvq3QcyTBRV2v0BnfbOBb8DJ4APwGFoq7Rih7DWS6gWF/CJAc6vtFV+wKfX/fV9o8bd6qziBZV+hTqKPVz2jgrwYLrtDVP+ptDST/9PuL7mmglfBiURnpaIWugFtQsNcKfZxviO4gIGneYSCt8y1Zyrf389jkrdDdcsqOkBNEk7agUPZQk8W0aZvLgHQH8CH4BeStdEMOsLkPSJr779qhP66plDFFevLRCv1fkS7tkbrRNXJGqA7lPcGebhn5sqzQ8XMykNwBFGTzBN2K4KQ8JRl0swBdm/8DfwESBeg8QbddU8n0XbRoUtZsQ9kC4BzQ3DbptYCu8Y/Bgs3GuQQ61dG1J5u1pW7vG1neGDAGKs7AvrQwH7gRaBXSEbQIvOhMZpppAiScChYHd3Hjar7hZSDnaWyHAy0+Zo3qkV6GtFZkj7LC+SLSl/H4Xc7XHI7PD3LpU92+OOXlTIq3SeBy2tK9XqtHTai0GzGaY6xgr1VyLzAQ2+DEBr1WnNoF2AP75TgWLdTXhPZAMARcEHJEe3rO3Dx5CNmUoBNXWgUfTRtTfD/o9L7B5b6evHZKdG3qs3wzUF3x7Iv8TwUH4kcr+TxB9w04C7htn4iRVv6Ho28xYVQdyg7L2TRNNmYhY2IMGAOty4A+8D+Ae8FvQFt4WnFdxIdUH/rWEj2bW9NrrHml6umVPRT7zTz9lfT5fU9X1iz+r6DdjXCq54Xngv/L0gD1qT7t37m6ugHfn6t/MMd2IG41KX7WytlGh5vxd2uUcY5qpFMuL58KcH/L5Z/LHXW4GuwHjtWROgM49gMD8OvezFGVJvjTyu4avCgwaAtevGnichEoJNG4HylgqPKtgOw/dWw3pu3rnbySD9CnpzxdlI3aewybrJ+BxQNtye+mkfOkI3UXoVx4kLZ/SLINlOmzrEnnbdTVrpu27TXBWYT8GMden7OP0X3s6LTC10RLE/pI9LLexFxGnPwEno8K/SO2egygPjfxZwHdZ8jyxkAFGeDDtx7u1wXX8mH8VU2hU7A5B+wAdINvLVFQySI9MBZcUTB631VUKH0oflcHp8PXYLjrn7GdW7A/GxwC7sdHe44HAQW4uKCllfWiwJU53YyTnp/0eCcfJa+gry9FGdJ6IUs39yOAArtWpYK2TrelfATpcoquq9+Di3NOT6KNX1I0oAAnGTv9EPs3ClqRfWS4EgnBlU/IxAX0iOfIn1uvUFrci89ipai2OV8r0uCW4E44/TbX+E0cdwKHgfOkw64Th/nAe8p7ovvBK45uIGn5lIhTfXNhWlMu/o/OURP/uqhNjAFjoPUY0IxeMi8f9L8JpJdo0oS36nJFFTkcjNduHvZIaEmBR4uAZnCz0YokjfyWM9LNLSTSRzYtymnnZ5R7Ak2CboO3ZVsYJSio/znFj4Jtqast0p5gKXALZXEr4z6U6YUjF1dTJyTq+zU5PJMzuIy6Wh3nCbqfgLa916FgaaB6Cg43grIKbWjbX9eY5A1wW1Oq8J8o+KxWwDQqj+wj81tINF8nufSlUWHgGNWP/AVMYlWaUGpr2oeCaxrRY5DJIGvbekmtHViBa0ovp91DWp8RiXbcZlaCc6BrYyhYVXlPFIyvBaFzL0664Md9ZJNXnbLZUCwHmvizgJ5Hj2WMgcoxwIdPK4l9cy38juNZOfwhp1OwyRSocvWKPQzjZvO+CxzpxhMnU7HVlmAz4gwD+uiG3d0vY8wKrAuDyMY3acrT7v9IHA7mBfcD3UyziG7wuucdBA4F08DNoBzyC/07VsDZnmAc0NZrxyTn2I/M1dHqdaMk2xLKXszV1c6GxpxG3sboR6CXKxcKVUC/IPrDwE/gLc+mxbVSoG0FZa1yD8Dv4p6vpix6PdbQufdlGr4n+8Ao1Vipp4D7KtAb+5v4zqM8ZVplNwnp2UkoeGsyqEC9Uw7bcZwANEnfGUTyPIkFqSe+moW2h4NjUJzQrJyRUB1dr6fOULVIqUwTB9naS3EiwcQYaCUGtCLWSlQfXgUxF73J68N7JKhHGcCgfgZ6k1criibJ3RgVaCV3TT/E/+XmJ5urgJ4ZagWYRbRC1xa7JlC7gmfw9xnHsgo+f8ChVsXLgJNBszDerfybOvnOGCg4ftVs2MYJxqAgrd0FBfN+9HFpt0vkde32A+r7ydgr+Bct1J9IZa145wH/wX/zNSKn5OfmcCcYlEtLXU5R21ql34n/vIkVeX074FzK9CZ711yjmpBrcnEqfZ/DBbrlgQJ9tBtHcqY/A63GL8NHLyk82crLK3sR+AicQZ0jpHAFna7jM8HH4EKVZf1AqI6JMWAMZGSAD59WkwrW34N/cQP41XVB+d3k/w60ItLbrlo1VJvoJbEVA536jP7eEtA3qygfQd3jUPwb6Ec6FAwUNLYHXcBd2NzBMY2cgtF6YIM0xpEN/vUVOPXzjJzuX1FZzHEH7EOrxfH4OiCmTqS+noTG+39qE3s9O9c1cD5Yn7SuhZeAJnF7AgWHs0HVCH2+hX52o0MK7O+TfpbjJ0BBbQswJ9A7AoV4xKyw4Ode2tB7Egp+7+baU0BbFOwIFOyvAZoYllVoW+0pQF4L9KLZQI7vAU0ktgW6Dp4AY4BEwVqf0T7KuIKvUdR/FN1OHFcgr5fhxpHeF50+5/o64BCOr4PJYH2ga1kTQQXxJqHOeOz2IfMguJ602nylqXC6vbgaDvbBtul+Uo6ArllH03KfY1bRzS2tiNzZ0ho7dl876aSkZlTFjiPJr8rku5B8g0Gl2lfbvxTqQK5cF9qwlLZZzYanrCC7SnERfSBTdqVsZtvgSTdCvRWeF8zVAjr9GMiNJHXD3xvcBqpNdqVDgi+6+SlQJgpjvJUxDsXoYrAXmBUoQOhGpQDoy5coWnzm8TMJP6rv3mCjuvocjQATI4V31IRCwfgnoElFSPR5VbkeA+i8+eIHFOU1lmahj/oO+Z9Q9AV/BiegQ9X007Snkz8QHA+mgeHgKMp9DrTq9dtC1SST+KtyHQvJVAw0nvGFDP1y+nQyff4P+r+AjcAuQBy/Cs6jfBBHV8Sd+vWbq0ybxt+ZtDcA+7NBd6CtbPV7MLiccpW5onHF3dtUT+UavytBLvDdh7ZfwPACsAHQqln8vgvOBv/GBpOm73wvR14/Ffwtx5Bch3Jz8Htwpgyw1e7CKiT/DjQhOkpqMApcAVp8nZA671BHgfuvYAdwCJBoVX4p+Cs2zeOXMxNjwBioMAN8KOeiCUG/zR282WGjoLAg0PNYzdabBL10HdB9mVMVPFBndoy06vueenkTCMrmRq/V1TjK8gIfZZrkdwbNfUA3B3mtjuJkAn50k08tuXZm9fuW2kEdGOZ41bPmzIG2rYZPn+emvwrYrSK0p+vuR9rUxKdVhbb1ef2VtjVJKbvgX59Rnf/g/SDUIHU6Sk+dCaHy/weN5Lia9jbZjQAAAABJRU5ErkJggg==" + createdby: "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" sponsors: diff --git a/helm/requestor/Chart.yaml b/helm/requestor/Chart.yaml index 10573f1a..becb737c 100644 --- a/helm/requestor/Chart.yaml +++ b/helm/requestor/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 +version: 0.1.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,12 +23,11 @@ version: 0.1.5 # It is recommended to use it with quotes. appVersion: "master" - dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/requestor/README.md b/helm/requestor/README.md index 2106030f..dd01b6f4 100644 --- a/helm/requestor/README.md +++ b/helm/requestor/README.md @@ -1,6 +1,6 @@ # requestor -![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.16](https://img.shields.io/badge/Version-0.1.16-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Requestor Service @@ -8,7 +8,7 @@ A Helm chart for gen3 Requestor Service | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values @@ -23,7 +23,7 @@ A Helm chart for gen3 Requestor Service | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values | list | `["requestor"]` | Value for the match expression key. | | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey | string | `"kubernetes.io/hostname"` | Value for topology key label. | | arboristUrl | string | `"http://arborist-service"` | Arborist service URL. | -| args | list | `["-c","/env/bin/alembic upgrade head\n"]` | Arguments to pass to the init container. | +| args | list | `["-c","# Managing virtual environments via poetry instead of python since the AL base image update, but retaining backwards compatibility\npoetry run alembic upgrade head || /env/bin/alembic upgrade head\n"]` | Arguments to pass to the init container. | | automountServiceAccountToken | bool | `false` | Automount the default service account token | | autoscaling | map | `{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Configuration for autoscaling the number of replicas | | autoscaling.enabled | bool | `false` | Whether autoscaling is enabled | @@ -31,24 +31,32 @@ A Helm chart for gen3 Requestor Service | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | | command | list | `["/bin/sh"]` | Command to run for the init container. | -| dataDog | bool | `{"enabled":false,"env":"dev"}` | Whether Datadog is enabled. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"false"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| externalSecrets | map | `{"dbcreds":null}` | External Secrets settings. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | +| global.addDbgap | bool | `false` | Force attempting a dbgap sync if "true", falls back on user.yaml | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any requestor secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.onlyDbgap | bool | `false` | Forces ONLY a dbgap sync if "true", IGNORING user.yaml | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -56,9 +64,12 @@ A Helm chart for gen3 Requestor Service | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | +| global.slack_send_dbgap | bool | `false` | Will echo what files we are seeing on dbgap ftp to Slack. | +| global.slack_webhook | string | `"None"` | Slack webhook endpoint used with certain jobs. | | global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | +| global.userYamlS3Path | string | `"s3://cdis-gen3-users/helm-test/user.yaml"` | Path to the user.yaml file in S3. | +| global.usersync | bool | `false` | Whether to run Fence usersync or not. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/requestor","tag":"master"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/requestor"` | Docker repository. | @@ -68,6 +79,8 @@ A Helm chart for gen3 Requestor Service | initResources.limits | map | `{"cpu":0.8,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | | initResources.limits.cpu | string | `0.8` | The maximum amount of CPU the container can use | | initResources.limits.memory | string | `"512Mi"` | The maximum amount of memory the container can use | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| partOf | string | `"Authentication"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | | postgres.database | string | `nil` | Database name for postgres. This is a service override, defaults to - | | postgres.dbCreate | bool | `nil` | Whether the database should be created. Default to global.postgres.dbCreate | @@ -78,6 +91,7 @@ A Helm chart for gen3 Requestor Service | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | releaseLabel | string | `"production"` | | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | @@ -88,6 +102,10 @@ A Helm chart for gen3 Requestor Service | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | | revisionHistoryLimit | int | `2` | Number of old revisions to retain | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Secret information for External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":[{"name":"http","port":80,"protocol":"TCP","targetPort":80}],"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `[{"name":"http","port":80,"protocol":"TCP","targetPort":80}]` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -95,6 +113,3 @@ A Helm chart for gen3 Requestor Service | strategy.rollingUpdate.maxSurge | int | `1` | Number of additional replicas to add during rollout. | | strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | | volumeMounts | list | `nil` | Volumes to mount to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/requestor/templates/_helpers.tpl b/helm/requestor/templates/_helpers.tpl index 36ae7f51..899b723c 100644 --- a/helm/requestor/templates/_helpers.tpl +++ b/helm/requestor/templates/_helpers.tpl @@ -34,22 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "requestor.labels" -}} -helm.sh/chart: {{ include "requestor.chart" . }} -{{ include "requestor.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "requestor.selectorLabels" -}} -app.kubernetes.io/name: {{ include "requestor.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ include "requestor.name" . }} -release: {{ .Values.releaseLabel }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -63,17 +67,6 @@ Create the name of the service account to use {{- end }} {{- end }} -{{/* -Define ddEnabled -*/}} -{{- define "requestor.ddEnabled" -}} -{{- if .Values.global }} -{{- .Values.global.ddEnabled }} -{{- else}} -{{- .Values.dataDog.enabled }} -{{- end }} -{{- end }} - {{/* Postgres Password lookup */}} diff --git a/helm/requestor/templates/aws-config.yaml b/helm/requestor/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/requestor/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/requestor/templates/db-init.yaml b/helm/requestor/templates/db-init.yaml index abbefb6e..5ef14e87 100644 --- a/helm/requestor/templates/db-init.yaml +++ b/helm/requestor/templates/db-init.yaml @@ -1,6 +1,6 @@ -{{ include "common.db_setup_job" . }} ---- {{ include "common.db-secret" . }} --- -{{ include "common.db_setup_sa" . }} +{{ include "common.db_setup_job" . }} --- +{{ include "common.db_setup_sa" . }} +--- \ No newline at end of file diff --git a/helm/requestor/templates/deployment.yaml b/helm/requestor/templates/deployment.yaml index 3e52124b..34ced42f 100644 --- a/helm/requestor/templates/deployment.yaml +++ b/helm/requestor/templates/deployment.yaml @@ -2,6 +2,8 @@ apiVersion: apps/v1 kind: Deployment metadata: name: requestor-deployment + labels: + {{- include "requestor.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} @@ -17,17 +19,16 @@ spec: template: metadata: labels: + {{- include "requestor.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} # gen3 networkpolicy labels netnolimit: 'yes' public: 'yes' dbrequestor: 'yes' - {{- if eq (include "requestor.ddEnabled" . ) "true" }} - tags.datadoghq.com/service: "requestor" - # TODO: move this to helpers so we can have this populated from a configmap - tags.datadoghq.com/env: {{ .Values.dataDog.env }} - tags.datadoghq.com/version: {{ .Values.image.tag | default .Chart.AppVersion }} - {{- end }} - {{- include "requestor.selectorLabels" . | nindent 8 }} + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: {{- with .Values.affinity }} affinity: @@ -151,4 +152,4 @@ spec: args: - "-c" - | - /env/bin/alembic upgrade head \ No newline at end of file + poetry run alembic upgrade head || /env/bin/alembic upgrade head \ No newline at end of file diff --git a/helm/requestor/templates/external-secret.yaml b/helm/requestor/templates/external-secret.yaml new file mode 100644 index 00000000..70c278fe --- /dev/null +++ b/helm/requestor/templates/external-secret.yaml @@ -0,0 +1 @@ +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/requestor/templates/netpolicy.yaml b/helm/requestor/templates/netpolicy.yaml new file mode 100644 index 00000000..70a5c3b5 --- /dev/null +++ b/helm/requestor/templates/netpolicy.yaml @@ -0,0 +1 @@ +{{ include "common.db_netpolicy" . }} \ No newline at end of file diff --git a/helm/requestor/templates/pdb.yaml b/helm/requestor/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/requestor/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/requestor/templates/secret-store.yaml b/helm/requestor/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/requestor/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/requestor/templates/tests/test-connection.yaml b/helm/requestor/templates/tests/test-connection.yaml index 8d9970ec..244fd6ed 100644 --- a/helm/requestor/templates/tests/test-connection.yaml +++ b/helm/requestor/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "requestor.fullname" . }}:{{ .Values.service.port }}'] + args: ['requestor-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/requestor/values.yaml b/helm/requestor/values.yaml index 9d2697f1..af8d89bf 100644 --- a/helm/requestor/values.yaml +++ b/helm/requestor/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -44,18 +46,51 @@ global: logsBucket: logs-gen3 # -- (bool) Whether to sync data from dbGaP. syncFromDbgap: false + # -- (bool) Force attempting a dbgap sync if "true", falls back on user.yaml + addDbgap: false + # -- (bool) Forces ONLY a dbgap sync if "true", IGNORING user.yaml + onlyDbgap: false # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml + userYamlS3Path: s3://cdis-gen3-users/helm-test/user.yaml + # -- (bool) Whether to run Fence usersync or not. + usersync: false + # -- (string) Slack webhook endpoint used with certain jobs. + slack_webhook: None + # -- (bool) Will echo what files we are seeing on dbgap ftp to Slack. + slack_send_dbgap: false # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any requestor secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: +# -- (map) Secret information for External Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -113,30 +148,25 @@ strategy: # -- (int) Maximum amount of pods that can be unavailable during the update. maxUnavailable: 0 -# -- (bool) Whether Datadog is enabled. -dataDog: - enabled: false - env: dev - # -- (map) Affinity to use for the deployment. affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - requestor - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - requestor + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (bool) Automount the default service account token automountServiceAccountToken: false @@ -160,6 +190,11 @@ volumeMounts: # readOnly: true # mountPath: "/src/requestor-config.yaml" # subPath: "requestor-config.yaml" + # Added an additional volume mount for new images using the / directory, while retaining the 'src' mount for backward compatibility. + # - name: "config-volume" + # readOnly: true + # mountPath: "/requestor/requestor-config.yaml" + # subPath: "requestor-config.yaml" # -- (map) Resource requests and limits for the containers in the pod resources: @@ -193,7 +228,8 @@ command: ["/bin/sh"] args: - "-c" - | - /env/bin/alembic upgrade head + # Managing virtual environments via poetry instead of python since the AL base image update, but retaining backwards compatibility + poetry run alembic upgrade head || /env/bin/alembic upgrade head # Service and Pod # -- (map) Kubernetes service information. @@ -206,3 +242,15 @@ service: port: 80 targetPort: 80 name: http + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "false" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Authentication" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/revproxy/Chart.yaml b/helm/revproxy/Chart.yaml index d2ab3af6..e186696b 100644 --- a/helm/revproxy/Chart.yaml +++ b/helm/revproxy/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 +version: 0.1.19 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/revproxy/README.md b/helm/revproxy/README.md index b2a380e2..057cc4e4 100644 --- a/helm/revproxy/README.md +++ b/helm/revproxy/README.md @@ -1,9 +1,15 @@ # revproxy -![Version: 0.1.5](https://img.shields.io/badge/Version-0.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.19](https://img.shields.io/badge/Version-0.1.19-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 revproxy +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -14,24 +20,29 @@ A Helm chart for gen3 revproxy | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","tls":{"cert":null,"key":null},"userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | -| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false,"wafv2":{"enabled":false,"wafAclArn":null}}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | +| global.aws.wafv2 | map | `{"enabled":false,"wafAclArn":null}` | WAF configuration | +| global.aws.wafv2.enabled | bool | `false` | Set to true if using AWS WAFv2 | +| global.aws.wafv2.wafAclArn | string | `nil` | ARN for the WAFv2 ACL. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -39,22 +50,28 @@ A Helm chart for gen3 revproxy | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | +| global.tierAccessLimit | int | `"1000"` | Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. | +| global.tls.cert | string | `nil` | | +| global.tls.key | string | `nil` | | | image | map | `{"pullPolicy":"Always","repository":"nginx","tag":"stable-perl"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"nginx"` | Docker repository. | | image.tag | string | `"stable-perl"` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | -| ingress | map | `{"annotations":{},"className":"","enabled":false,"hosts":[{"host":"chart-example.local","paths":[{"path":"/","pathType":"ImplementationSpecific"}]}],"tls":[]}` | Configuration for revproxy ingress. | +| ingress | map | `{"annotations":{},"className":"","enabled":false,"hosts":[{"host":"chart-example.local","paths":[{"path":"/","pathType":"Prefix"}]}],"tls":[]}` | Configuration for revproxy ingress. | | ingress.annotations | map | `{}` | Annotations to add to the ingress. | | ingress.className | string | `""` | The ingress class name. | | ingress.enabled | bool | `false` | Whether to create the ingress | -| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/","pathType":"ImplementationSpecific"}]}]` | Where to route the traffic. | +| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/","pathType":"Prefix"}]}]` | Where to route the traffic. | | ingress.tls | list | `[]` | To secure an Ingress by specifying a secret that contains a TLS private key and certificate. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | +| netPolicy | map | `{"egressApps":["portal","sowerjob"],"ingressApps":["portal","sowerjob"]}` | Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true | +| netPolicy.egressApps | array | `["portal","sowerjob"]` | List of apps that this app requires egress to | +| netPolicy.ingressApps | array | `["portal","sowerjob"]` | List of app labels that require ingress to this service | | nodeSelector | map | `{}` | Node selector labels. | +| partOf | string | `"Front-End"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod. | | podSecurityContext | map | `{}` | Pod-level security context. | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | @@ -64,6 +81,7 @@ A Helm chart for gen3 revproxy | postgres.password | string | `nil` | Password for Postgres. Will be autogenerated if left empty. | | postgres.port | string | `"5432"` | Port for Postgres. | | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -75,6 +93,7 @@ A Helm chart for gen3 revproxy | revisionHistoryLimit | int | `2` | Number of old revisions to retain | | revproxyElb | map | `{"gen3SecretsFolder":"Gen3Secrets","sslCert":"","targetPortHTTP":80,"targetPortHTTPS":443}` | Configuration for depricated revproxy service ELB. | | securityContext | map | `{}` | Container-level security context. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"NodePort"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"NodePort"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -87,6 +106,3 @@ A Helm chart for gen3 revproxy | strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | | tolerations | list | `[]` | Tolerations to use for the deployment. | | userhelperEnabled | bool | `false` | | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/revproxy/gen3.nginx.conf/fence-service-ga4gh.conf b/helm/revproxy/gen3.nginx.conf/fence-service-ga4gh.conf index b017919f..522fad15 100644 --- a/helm/revproxy/gen3.nginx.conf/fence-service-ga4gh.conf +++ b/helm/revproxy/gen3.nginx.conf/fence-service-ga4gh.conf @@ -3,8 +3,8 @@ location ~ \/ga4gh\/drs\/v1\/objects\/(.*)\/access { return 403 "failed csrf check"; } - set $proxy_service "presigned_url_fence"; - set $upstream http://presigned_url_fence-service$des_domain; + set $proxy_service "presigned-url-fence"; + set $upstream http://presigned-url-fence-service$des_domain; rewrite ^/user/(.*) /$1 break; proxy_pass $upstream; } diff --git a/helm/revproxy/gen3.nginx.conf/fence-service.conf b/helm/revproxy/gen3.nginx.conf/fence-service.conf index 5a792424..dccbfa83 100644 --- a/helm/revproxy/gen3.nginx.conf/fence-service.conf +++ b/helm/revproxy/gen3.nginx.conf/fence-service.conf @@ -46,8 +46,8 @@ location /user/data/download { return 403 "failed csrf check"; } - set $proxy_service "presigned_url_fence"; - set $upstream http://presigned_url_fence-service$des_domain; + set $proxy_service "presigned-url-fence"; + set $upstream http://presigned-url-fence-service$des_domain; rewrite ^/user/(.*) /$1 break; proxy_pass $upstream; } diff --git a/helm/revproxy/gen3.nginx.conf/gen3ff-as-root/frontend-framework-service.conf b/helm/revproxy/gen3.nginx.conf/gen3ff-as-root/frontend-framework-service.conf index ac2cb75f..1e2259b9 100644 --- a/helm/revproxy/gen3.nginx.conf/gen3ff-as-root/frontend-framework-service.conf +++ b/helm/revproxy/gen3.nginx.conf/gen3ff-as-root/frontend-framework-service.conf @@ -6,3 +6,9 @@ set $upstream http://frontend-framework-service.$namespace.svc.cluster.local; proxy_pass $upstream; } + + location /api/auth/ { + set $proxy_service "frontend-framework"; + set $upstream http://frontend-framework-service.$namespace.svc.cluster.local; + proxy_pass $upstream; + } diff --git a/helm/revproxy/gen3.nginx.conf/portal-as-root/frontend-framework-service.conf b/helm/revproxy/gen3.nginx.conf/portal-as-root/frontend-framework-service.conf index dbb24e4b..d3e13507 100644 --- a/helm/revproxy/gen3.nginx.conf/portal-as-root/frontend-framework-service.conf +++ b/helm/revproxy/gen3.nginx.conf/portal-as-root/frontend-framework-service.conf @@ -11,3 +11,9 @@ set $upstream http://frontend-framework-service.$namespace.svc.cluster.local; proxy_pass $upstream; } + + location /ff/api/auth/ { + set $proxy_service "frontend-framework"; + set $upstream http://frontend-framework-service.$namespace.svc.cluster.local; + proxy_pass $upstream; + } diff --git a/helm/revproxy/gen3.nginx.conf/portal-service.conf b/helm/revproxy/gen3.nginx.conf/portal-service.conf deleted file mode 100644 index 35d64cca..00000000 --- a/helm/revproxy/gen3.nginx.conf/portal-service.conf +++ /dev/null @@ -1,12 +0,0 @@ - location / { - if ($csrf_check !~ ^ok-\S.+$) { - return 403 "failed csrf check"; - } - - - set $proxy_service "portal"; - # $upstream is written to the logs - set $upstream http://portal-service.$namespace.svc.cluster.local; - - proxy_pass $upstream; - } diff --git a/helm/revproxy/logrotate-nginx.conf b/helm/revproxy/logrotate-nginx.conf deleted file mode 100644 index fc6b7e3c..00000000 --- a/helm/revproxy/logrotate-nginx.conf +++ /dev/null @@ -1,9 +0,0 @@ -# nginx log rotation -/var/log/nginx { - weekly - size 10M - postrotate - [ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid` - endscript - rotate 5 -} diff --git a/helm/revproxy/nginx/nginx.conf b/helm/revproxy/nginx/nginx.conf index d2be4bd3..0d352578 100644 --- a/helm/revproxy/nginx/nginx.conf +++ b/helm/revproxy/nginx/nginx.conf @@ -320,12 +320,6 @@ http { if ($document_url_env != "") { include /etc/nginx/gen3.conf/documentation-site/*.conf; } - if ($frontend_root_service = "portal") { - include /etc/nginx/gen3.conf/portal-as-root/*.conf; - } - if ($frontend_root_service = "gen3ff") { - include /etc/nginx/gen3.conf/gen3ff-as-root/*.conf; - } location @errorworkspace { # if ($frontend_root_service = "gen3ff") { diff --git a/helm/revproxy/templates/_helpers.tpl b/helm/revproxy/templates/_helpers.tpl index 2019b5ad..24011557 100644 --- a/helm/revproxy/templates/_helpers.tpl +++ b/helm/revproxy/templates/_helpers.tpl @@ -34,10 +34,12 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "revproxy.labels" -}} -helm.sh/chart: {{ include "revproxy.chart" . }} -{{ include "revproxy.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} {{- end }} @@ -45,10 +47,13 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} Selector labels */}} {{- define "revproxy.selectorLabels" -}} -app.kubernetes.io/name: {{ include "revproxy.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: revproxy -#GEN3_DATE_LABEL +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* diff --git a/helm/revproxy/templates/configMaps.yaml b/helm/revproxy/templates/configMaps.yaml index ad7441f9..306617cc 100644 --- a/helm/revproxy/templates/configMaps.yaml +++ b/helm/revproxy/templates/configMaps.yaml @@ -1,19 +1,30 @@ apiVersion: v1 kind: ConfigMap -metadata: +metadata: name: revproxy-nginx-subconf data: {{- range $path, $bytes := .Files.Glob "gen3.nginx.conf/*.conf" }} {{ ($a := split "/" $path)._1 }}: | - {{- $bytes | toString | nindent 4 }} + {{- $bytes | toString | nindent 4 }} {{- end}} +{{- if eq "gen3ff" .Values.global.frontendRoot }} + {{ "frontend-framework-service.conf" }}: | + {{- .Files.Get "gen3.nginx.conf/gen3ff-as-root/frontend-framework-service.conf" | nindent 4}} + {{ "portal-service.conf" }}: | + {{- .Files.Get "gen3.nginx.conf/gen3ff-as-root/portal-service.conf" | nindent 4}} +{{- else }} + {{ "frontend-framework-service.conf" }}: | + {{- .Files.Get "gen3.nginx.conf/portal-as-root/frontend-framework-service.conf"| nindent 4}} + {{ "portal-service.conf" }}: | + {{- .Files.Get "gen3.nginx.conf/portal-as-root/portal-service.conf" | nindent 4}} +{{- end }} --- apiVersion: v1 kind: ConfigMap -metadata: +metadata: name: revproxy-nginx-conf data: {{- range $path, $bytes := .Files.Glob "nginx/*" }} {{ ($a := split "/" $path)._1 }}: | - {{- $bytes | toString | nindent 4 }} + {{- $bytes | toString | nindent 4 }} {{- end}} \ No newline at end of file diff --git a/helm/revproxy/templates/deployment.yaml b/helm/revproxy/templates/deployment.yaml index 3294cac2..996acd78 100644 --- a/helm/revproxy/templates/deployment.yaml +++ b/helm/revproxy/templates/deployment.yaml @@ -20,12 +20,17 @@ spec: {{- end }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print $.Template.BasePath "/configMaps.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: {{- include "revproxy.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} {{- if .Values.userhelperEnabled }} userhelper: "yes" {{- end}} @@ -69,7 +74,7 @@ spec: path: /_status port: 80 initialDelaySeconds: 5 - periodSeconds: 30 + periodSeconds: 3000 readinessProbe: httpGet: path: /_status @@ -116,7 +121,7 @@ spec: secretKeyRef: name: gateway-g3auto key: base64Authz.txt - optional: true + optional: true - name: MDS_AUTHZ valueFrom: secretKeyRef: @@ -183,7 +188,7 @@ spec: echo "modsecurity on;" >> /etc/nginx/gen3_server_modsec.conf echo "modsecurity_rules_file /etc/nginx/modsec/main.conf;" >> /etc/nginx/gen3_server_modsec.conf fi - + exec nginx -g 'daemon off;' {{- with .Values.nodeSelector }} nodeSelector: diff --git a/helm/revproxy/templates/ingress.yaml b/helm/revproxy/templates/ingress_aws.yaml similarity index 72% rename from helm/revproxy/templates/ingress.yaml rename to helm/revproxy/templates/ingress_aws.yaml index d67032eb..612dad43 100644 --- a/helm/revproxy/templates/ingress.yaml +++ b/helm/revproxy/templates/ingress_aws.yaml @@ -1,24 +1,21 @@ +{{- if .Values.global.aws.enabled }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: revproxy + name: revproxy-alb annotations: - {{- if .Values.global.aws.enabled }} alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/tags: Environment={{ .Values.global.environment }} alb.ingress.kubernetes.io/certificate-arn: {{ .Values.global.revproxyArn }} alb.ingress.kubernetes.io/group.name: {{ .Values.global.environment }} alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' - {{- end }} + alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-Res-FIPS-2023-04 + {{- if .Values.global.aws.wafv2.enabled }} + alb.ingress.kubernetes.io/wafv2-acl-arn: {{ .Values.global.aws.wafv2.wafAclArn }} + {{- end }} spec: - {{- if .Values.global.aws.enabled }} ingressClassName: alb - {{- end }} - {{- if .Values.global.dev }} - tls: - - secretName: gen3-certs - {{- end }} rules: - host: {{ default .Values.global.hostname .Values.hostname }} http: @@ -29,4 +26,5 @@ spec: service: name: revproxy-service port: - number: 80 \ No newline at end of file + number: 80 +{{- end }} \ No newline at end of file diff --git a/helm/elasticsearch/templates/ingress.yaml b/helm/revproxy/templates/ingress_default.yaml similarity index 90% rename from helm/elasticsearch/templates/ingress.yaml rename to helm/revproxy/templates/ingress_default.yaml index 3f8cc2aa..18efa92a 100644 --- a/helm/elasticsearch/templates/ingress.yaml +++ b/helm/revproxy/templates/ingress_default.yaml @@ -1,5 +1,5 @@ {{- if .Values.ingress.enabled -}} -{{- $fullName := include "elasticsearch.fullname" . -}} +{{- $fullName := include "revproxy.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} @@ -17,7 +17,7 @@ kind: Ingress metadata: name: {{ $fullName }} labels: - {{- include "elasticsearch.labels" . | nindent 4 }} + {{- include "revproxy.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} @@ -49,11 +49,11 @@ spec: backend: {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} service: - name: {{ $fullName }} + name: revproxy-service port: number: {{ $svcPort }} {{- else }} - serviceName: {{ $fullName }} + serviceName: revproxy-service servicePort: {{ $svcPort }} {{- end }} {{- end }} diff --git a/helm/revproxy/templates/ingress_dev.yaml b/helm/revproxy/templates/ingress_dev.yaml new file mode 100644 index 00000000..df2ea60c --- /dev/null +++ b/helm/revproxy/templates/ingress_dev.yaml @@ -0,0 +1,22 @@ +{{- if and (eq .Values.global.dev true) (eq .Values.global.aws.enabled false) }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: revproxy-dev +spec: +{{- if .Values.global.dev }} + tls: + - secretName: gen3-certs + {{- end }} + rules: + - host: {{ default .Values.global.hostname .Values.hostname }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: revproxy-service + port: + number: 80 + {{- end }} diff --git a/helm/revproxy/templates/netpolicy.yaml b/helm/revproxy/templates/netpolicy.yaml new file mode 100644 index 00000000..1c7bd36c --- /dev/null +++ b/helm/revproxy/templates/netpolicy.yaml @@ -0,0 +1,61 @@ +{{ include "common.ingress_netpolicy" . }} + +--- + +{{ include "common.egress_netpolicy" . }} + +--- + +{{- if .Values.global.netPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: revproxy-netpolicy +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - revproxy + - ambassador-gen3 + - auspice + - ohdsi-atlas + - ohdsi-webapi + - superset + - superset-worker + - superset-redis-master + ingress: + - from: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - port: 80 + - port: 4000 + - port: 8080 + - port: 81 + - port: 82 + - port: 443 + - port: 8088 + - port: 9090 + egress: + - to: + - namespaceSelector: + matchLabels: + app: prometheus + - to: + - namespaceSelector: + matchLabels: + app: grafana + - to: + - namespaceSelector: + matchLabels: + app: argo + - to: + - namespaceSelector: + matchLabels: + app: argocd + policyTypes: + - Ingress + - Egress +{{- end }} \ No newline at end of file diff --git a/helm/revproxy/templates/pdb.yaml b/helm/revproxy/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/revproxy/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/revproxy/templates/tests/test-connection.yaml b/helm/revproxy/templates/tests/test-connection.yaml index c73c44bd..9266de3f 100644 --- a/helm/revproxy/templates/tests/test-connection.yaml +++ b/helm/revproxy/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "revproxy.fullname" . }}-test-connection" + name: "revproxy-test-connection" labels: {{- include "revproxy.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "revproxy.fullname" . }}:{{ .Values.service.port }}'] + args: ['revproxy-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/revproxy/values.yaml b/helm/revproxy/values.yaml index 8c206519..49c045fd 100644 --- a/helm/revproxy/values.yaml +++ b/helm/revproxy/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: tls: cert: @@ -15,12 +15,20 @@ global: awsAccessKeyId: # -- (string) Credentials for AWS stuff. awsSecretAccessKey: + # -- (map) WAF configuration + wafv2: + # -- (bool) Set to true if using AWS WAFv2 + enabled: false + # -- (string) ARN for the WAFv2 ACL. + wafAclArn: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -45,20 +53,24 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (int) Only relevant if tireAccessLevel is set to "regular". Summary charts below this limit will not appear for aggregated data. + tierAccessLimit: "1000" + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -112,11 +124,13 @@ serviceAccount: podAnnotations: {} # -- (map) Pod-level security context. -podSecurityContext: {} +podSecurityContext: + {} # fsGroup: 2000 # -- (map) Container-level security context. -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -131,6 +145,18 @@ service: # -- (int) The port number that the service exposes. port: 80 +# -- (map) Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true +netPolicy: + # -- (array) List of app labels that require ingress to this service + ingressApps: + - portal + - sowerjob + + # -- (array) List of apps that this app requires egress to + egressApps: + - portal + - sowerjob + # -- (map) Configuration for revproxy ingress. ingress: # -- (bool) Whether to create the ingress @@ -138,7 +164,8 @@ ingress: # -- (string) The ingress class name. className: "" # -- (map) Annotations to add to the ingress. - annotations: {} + annotations: + {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" # -- (list) Where to route the traffic. @@ -146,7 +173,7 @@ ingress: - host: chart-example.local paths: - path: / - pathType: ImplementationSpecific + pathType: Prefix # -- (list) To secure an Ingress by specifying a secret that contains a TLS private key and certificate. tls: [] # - secretName: chart-example-tls @@ -210,3 +237,15 @@ revproxyElb: targetPortHTTP: 80 gen3SecretsFolder: Gen3Secrets + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Front-End" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/sheepdog/Chart.yaml b/helm/sheepdog/Chart.yaml index 8e13a69a..5512b98d 100644 --- a/helm/sheepdog/Chart.yaml +++ b/helm/sheepdog/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.6 +version: 0.1.20 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -24,10 +24,10 @@ version: 0.1.6 appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/sheepdog/README.md b/helm/sheepdog/README.md index 5e76e8f3..ce1f501c 100644 --- a/helm/sheepdog/README.md +++ b/helm/sheepdog/README.md @@ -1,6 +1,6 @@ # sheepdog -![Version: 0.1.6](https://img.shields.io/badge/Version-0.1.6-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.20](https://img.shields.io/badge/Version-0.1.20-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 Sheepdog Service @@ -8,7 +8,7 @@ A Helm chart for gen3 Sheepdog Service | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values @@ -23,33 +23,40 @@ A Helm chart for gen3 Sheepdog Service | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values | list | `["sheepdog"]` | Value for the match expression key. | | affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey | string | `"kubernetes.io/hostname"` | Value for topology key label. | | arboristUrl | string | `"http://arborist-service"` | URL for the arborist service | -| authNamespace | string | `"default"` | | +| authNamespace | string | `""` | | | automountServiceAccountToken | bool | `false` | Automount the default service account token | | autoscaling | map | `{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Configuration for autoscaling the number of replicas | | autoscaling.enabled | bool | `false` | Whether autoscaling is enabled | | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | dataDog | bool | `{"enabled":false,"env":"dev"}` | Whether Datadog is enabled. | | dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | +| externalSecrets | map | `{"dbcreds":null}` | External Secrets settings. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | | fenceUrl | string | `"http://fence-service"` | URL for the fence service | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any sheepdog secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -57,14 +64,14 @@ A Helm chart for gen3 Sheepdog Service | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | -| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/sheepdog","tag":"helm-test"}` | Docker image information. | +| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/sheepdog","tag":"bug_auth-audience"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/sheepdog"` | Docker repository. | -| image.tag | string | `"helm-test"` | Overrides the image tag whose default is the chart appVersion. | +| image.tag | string | `"bug_auth-audience"` | Overrides the image tag whose default is the chart appVersion. | | indexdUrl | string | `"http://indexd-service"` | URL for the indexd service | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| partOf | string | `"Core-Service"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{"gen3.io/network-ingress":"sheepdog"}` | Annotations to add to the pod | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | | postgres.database | string | `nil` | Database name for postgres. This is a service override, defaults to - | @@ -76,6 +83,7 @@ A Helm chart for gen3 Sheepdog Service | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | | postgresql.primary.persistence.enabled | bool | `false` | Option to persist the dbs data. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | releaseLabel | string | `"production"` | | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"512Mi"},"requests":{"cpu":0.3,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | @@ -86,20 +94,10 @@ A Helm chart for gen3 Sheepdog Service | resources.requests.cpu | string | `0.3` | The amount of CPU requested | | resources.requests.memory | string | `"12Mi"` | The amount of memory requested | | revisionHistoryLimit | int | `2` | Number of old revisions to retain | -| secrets | map | `{"fence":{"database":"fence","host":"postgres-postgresql.postgres.svc.cluster.local","password":"postgres","user":"postgres"},"gdcapi":{"secretKey":null},"indexd":{"password":"postgres"},"sheepdog":{"database":"sheepdog","host":"postgres-postgresql.postgres.svc.cluster.local","password":"postgres","user":"postgres"}}` | Values for sheepdog secret. | -| secrets.fence | map | `{"database":"fence","host":"postgres-postgresql.postgres.svc.cluster.local","password":"postgres","user":"postgres"}` | Values for sheepdog's access to the fence database. | -| secrets.fence.database | string | `"fence"` | Database name for fence's db. | -| secrets.fence.host | string | `"postgres-postgresql.postgres.svc.cluster.local"` | Host for fence's db. | -| secrets.fence.password | string | `"postgres"` | Password to fence's db. | -| secrets.fence.user | string | `"postgres"` | User for fence's db. | -| secrets.gdcapi.secretKey | string | `nil` | GDCAPI token. | -| secrets.indexd | map | `{"password":"postgres"}` | Values for sheepdog's access to indexd database. | -| secrets.indexd.password | string | `"postgres"` | Password to indexd's db. | -| secrets.sheepdog | map | `{"database":"sheepdog","host":"postgres-postgresql.postgres.svc.cluster.local","password":"postgres","user":"postgres"}` | Values for sheepdog's database. | -| secrets.sheepdog.database | string | `"sheepdog"` | Database name for sheepdog's db. | -| secrets.sheepdog.host | string | `"postgres-postgresql.postgres.svc.cluster.local"` | Host for sheepdog's db. | -| secrets.sheepdog.password | string | `"postgres"` | Password to sheepdog's db. | -| secrets.sheepdog.user | string | `"postgres"` | User for sheepdog's db. | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Values for sheepdog secret. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID to access the db restore job S3 bucket. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID to access the db restore job S3 bucket. Overrides global key. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -107,7 +105,4 @@ A Helm chart for gen3 Sheepdog Service | strategy.rollingUpdate.maxSurge | int | `1` | Number of additional replicas to add during rollout. | | strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | | terminationGracePeriodSeconds | int | `50` | sheepdog transactions take forever - try to let the complete before termination | -| volumeMounts | list | `[{"mountPath":"/var/www/sheepdog/wsgi.py","name":"config-volume","readOnly":true,"subPath":"wsgi.py"}]` | Volumes to mount to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) +| volumeMounts | list | `[{"mountPath":"/var/www/sheepdog/settings.py","name":"config-volume","readOnly":true,"subPath":"settings.py"},{"mountPath":"sheepdog/bin/settings.py","name":"config-volume","readOnly":true,"subPath":"settings.py"}]` | Volumes to mount to the container. | diff --git a/helm/sheepdog/sheepdog-secret/wsgi.py b/helm/sheepdog/sheepdog-secret/settings.py similarity index 97% rename from helm/sheepdog/sheepdog-secret/wsgi.py rename to helm/sheepdog/sheepdog-secret/settings.py index 2d5b5637..2818d169 100644 --- a/helm/sheepdog/sheepdog-secret/wsgi.py +++ b/helm/sheepdog/sheepdog-secret/settings.py @@ -18,7 +18,7 @@ config['INDEX_CLIENT'] = { 'host': environ.get('INDEX_CLIENT_HOST') or 'http://indexd-service', 'version': 'v0', - 'auth': (environ.get( "INDEXD_USER", 'gdcapi'), environ.get( "INDEXD_PASS") ), + 'auth': (environ.get( "INDEXD_USER", 'sheepdog'), environ.get( "INDEXD_PASS") ), } config["PSQLGRAPH"] = { diff --git a/helm/sheepdog/templates/_helpers.tpl b/helm/sheepdog/templates/_helpers.tpl index e98750a2..1c935c37 100644 --- a/helm/sheepdog/templates/_helpers.tpl +++ b/helm/sheepdog/templates/_helpers.tpl @@ -34,22 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "sheepdog.labels" -}} -helm.sh/chart: {{ include "sheepdog.chart" . }} -{{ include "sheepdog.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "sheepdog.selectorLabels" -}} -app.kubernetes.io/name: {{ include "sheepdog.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ include "sheepdog.name" . }} -release: {{ .Values.releaseLabel }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -87,17 +91,6 @@ Create the name of the service account to use {{- end }} {{- end }} -{{/* -Define ddEnabled -*/}} -{{- define "sheepdog.ddEnabled" -}} -{{- if .Values.global }} -{{- .Values.global.ddEnabled }} -{{- else}} -{{- .Values.dataDog.enabled }} -{{- end }} -{{- end }} - {{/* Define dictionaryUrl */}} diff --git a/helm/sheepdog/templates/aws-config.yaml b/helm/sheepdog/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/sheepdog/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/sheepdog/templates/deployment.yaml b/helm/sheepdog/templates/deployment.yaml index a7035eed..ac6c4722 100644 --- a/helm/sheepdog/templates/deployment.yaml +++ b/helm/sheepdog/templates/deployment.yaml @@ -24,16 +24,15 @@ spec: metadata: labels: # gen3 networkpolicy labels - netnolimit: 'yes' - public: 'yes' - s3: 'yes' - {{- if eq (include "sheepdog.ddEnabled" . ) "true" }} - tags.datadoghq.com/service: "sheepdog" - # TODO: move this to helpers so we can have this populated from a configmap - tags.datadoghq.com/env: {{ .Values.dataDog.env }} - tags.datadoghq.com/version: {{ .Values.image.tag | default .Chart.AppVersion }} - {{- end }} + public: "yes" + netnolimit: "yes" + s3: "yes" {{- include "sheepdog.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: {{- with .Values.affinity }} affinity: @@ -50,6 +49,7 @@ spec: initContainers: - name: sheepdog-init image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} env: - name: DICTIONARY_URL value: {{ .Values.dictionaryUrl }} @@ -94,11 +94,17 @@ spec: # sheepdog sets up core data model now, # but suffers from a race condition doing it ... # - echo datamodel_postgres_admin create-all -U "${PGUSER}" -P XXXXXXX -H "${PGHOST}" -D "${PGDB}" - datamodel_postgres_admin create-all -U "${PGUSER}" -P "${PGPASSWORD}" -H "${PGHOST}" -D "${PGDB}" - - echo python /sheepdog/bin/setup_transactionlogs.py --user "${PGUSER}" --password XXXXX --host "${PGHOST}" --database "${PGDB}" - python /sheepdog/bin/setup_transactionlogs.py --user "${PGUSER}" --password "${PGPASSWORD}" --host "${PGHOST}" --database "${PGDB}" + if command -v python &> /dev/null; then + echo datamodel_postgres_admin create-all -U "${PGUSER}" -P XXXXXXX -H "${PGHOST}" -D "${PGDB}" + datamodel_postgres_admin create-all -U "${PGUSER}" -P "${PGPASSWORD}" -H "${PGHOST}" -D "${PGDB}" + echo python /sheepdog/bin/setup_transactionlogs.py --user "${PGUSER}" --password XXXXX --host "${PGHOST}" --database "${PGDB}" + python /sheepdog/bin/setup_transactionlogs.py --user "${PGUSER}" --password "${PGPASSWORD}" --host "${PGHOST}" --database "${PGDB}" + else + echo poetry run datamodel_postgres_admin create-all -U "${PGUSER}" -P XXXXXXX -H "${PGHOST}" -D "${PGDB}" + poetry run datamodel_postgres_admin create-all -U "${PGUSER}" -P "${PGPASSWORD}" -H "${PGHOST}" -D "${PGDB}" + echo poetry run python /sheepdog/bin/setup_transactionlogs.py --user "${PGUSER}" --password XXXXX --host "${PGHOST}" --database "${PGDB}" + poetry run python /sheepdog/bin/setup_transactionlogs.py --user "${PGUSER}" --password "${PGPASSWORD}" --host "${PGHOST}" --database "${PGDB}" + fi containers: - name: sheepdog image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" @@ -197,7 +203,7 @@ spec: valueFrom: secretKeyRef: name: indexd-service-creds - key: gdcapi + key: sheepdog optional: false - name: GEN3_UWSGI_TIMEOUT value: "600" diff --git a/helm/sheepdog/templates/external-secrets.yaml b/helm/sheepdog/templates/external-secrets.yaml new file mode 100644 index 00000000..70c278fe --- /dev/null +++ b/helm/sheepdog/templates/external-secrets.yaml @@ -0,0 +1 @@ +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/sheepdog/templates/netpolicy.yaml b/helm/sheepdog/templates/netpolicy.yaml new file mode 100644 index 00000000..70a5c3b5 --- /dev/null +++ b/helm/sheepdog/templates/netpolicy.yaml @@ -0,0 +1 @@ +{{ include "common.db_netpolicy" . }} \ No newline at end of file diff --git a/helm/sheepdog/templates/pdb.yaml b/helm/sheepdog/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/sheepdog/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/sheepdog/templates/secret-store.yaml b/helm/sheepdog/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/sheepdog/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/sheepdog/templates/tests/test-connection.yaml b/helm/sheepdog/templates/tests/test-connection.yaml index e508fe6e..dc94a171 100644 --- a/helm/sheepdog/templates/tests/test-connection.yaml +++ b/helm/sheepdog/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "sheepdog.fullname" . }}:{{ .Values.service.port }}'] + args: ['sheepdog-service:{{ .Values.service.port }}/_status?timeout=2'] restartPolicy: Never diff --git a/helm/sheepdog/values.yaml b/helm/sheepdog/values.yaml index e8914320..e3706f94 100644 --- a/helm/sheepdog/values.yaml +++ b/helm/sheepdog/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -42,20 +44,33 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any sheepdog secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -126,20 +141,20 @@ affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - sheepdog - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - sheepdog + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (bool) Automount the default service account token automountServiceAccountToken: false @@ -154,7 +169,7 @@ image: # -- (string) Docker pull policy. pullPolicy: Always # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: "helm-test" + tag: "bug_auth-audience" # Environment Variables # -- (string) URL of the data dictionary. @@ -165,14 +180,18 @@ indexdUrl: http://indexd-service fenceUrl: http://fence-service # -- (string) URL for the arborist service arboristUrl: http://arborist-service -authNamespace: default +authNamespace: "" # -- (list) Volumes to mount to the container. volumeMounts: - name: "config-volume" readOnly: true - mountPath: "/var/www/sheepdog/wsgi.py" - subPath: "wsgi.py" + mountPath: "/var/www/sheepdog/settings.py" + subPath: "settings.py" + - name: "config-volume" + readOnly: true + mountPath: "sheepdog/bin/settings.py" + subPath: "settings.py" # -- (map) Resource requests and limits for the containers in the pod resources: @@ -200,30 +219,19 @@ service: # Secrets # -- (map) Values for sheepdog secret. secrets: - # -- (map) Values for sheepdog's access to the fence database. - fence: - # -- (string) Host for fence's db. - host: postgres-postgresql.postgres.svc.cluster.local - # -- (string) User for fence's db. - user: postgres - # -- (string) Password to fence's db. - password: postgres - # -- (string) Database name for fence's db. - database: fence - # -- (map) Values for sheepdog's database. - sheepdog: - # -- (string) Host for sheepdog's db. - host: postgres-postgresql.postgres.svc.cluster.local - # -- (string) Password to sheepdog's db. - password: postgres - # -- (string) User for sheepdog's db. - user: postgres - # -- (string) Database name for sheepdog's db. - database: sheepdog - gdcapi: - # -- (string) GDCAPI token. - secretKey: - # -- (map) Values for sheepdog's access to indexd database. - indexd: - # -- (string) Password to indexd's db. - password: postgres + # -- (str) AWS access key ID to access the db restore job S3 bucket. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID to access the db restore job S3 bucket. Overrides global key. + awsSecretAccessKey: + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Core-Service" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/sower/.helmignore b/helm/sower/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/helm/sower/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/pidgin/Chart.yaml b/helm/sower/Chart.yaml similarity index 88% rename from helm/pidgin/Chart.yaml rename to helm/sower/Chart.yaml index be4d4801..c683d22b 100644 --- a/helm/pidgin/Chart.yaml +++ b/helm/sower/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: pidgin -description: A Helm chart for gen3 Pidgin Service +name: sower +description: A Helm chart for gen3 sower # A chart can be either an 'application' or a 'library' chart. # @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.4 +version: 0.1.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -23,8 +23,7 @@ version: 0.1.4 # It is recommended to use it with quotes. appVersion: "master" - dependencies: -- name: common - version: 0.1.4 - repository: file://../common + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/sower/README.md b/helm/sower/README.md new file mode 100644 index 00000000..3aa7dc4a --- /dev/null +++ b/helm/sower/README.md @@ -0,0 +1,188 @@ +# sower + +![Version: 0.1.16](https://img.shields.io/badge/Version-0.1.16-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) + +A Helm chart for gen3 sower + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | map | `{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["sower"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]}}` | Affinity to use for the deployment. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution | map | `[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["sower"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}]` | Option for scheduling to be required or preferred. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0] | int | `{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["sower"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100}` | Weight value for preferred scheduling. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0] | list | `{"key":"app","operator":"In","values":["sower"]}` | Label key for match expression. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].operator | string | `"In"` | Operation type for the match expression. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values | list | `["sower"]` | Value for the match expression key. | +| affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey | string | `"kubernetes.io/hostname"` | Value for topology key label. | +| automountServiceAccountToken | bool | `true` | Automount the default service account token | +| autoscaling | map | `{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Configuration for autoscaling the number of replicas | +| autoscaling.enabled | bool | `false` | Whether autoscaling is enabled | +| autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | +| autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage | +| awsRegion | string | `"us-east-1"` | AWS region to be used. | +| awsStsRegionalEndpoints | string | `"regional"` | AWS STS to issue temporary credentials to users and roles that make an AWS STS request. Values regional or global. | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"false"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| externalSecrets | map | `{"createK8sPelicanServiceSecret":false,"createK8sSowerJobsSecret":false,"pelicanserviceG3auto":null,"sowerjobsG3auto":null}` | External Secrets settings. | +| externalSecrets.createK8sPelicanServiceSecret | string | `false` | Will create the Helm "pelicanservice-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.createK8sSowerJobsSecret | string | `false` | Will create the Helm "sower-jobs-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. | +| externalSecrets.pelicanserviceG3auto | string | `nil` | Will override the name of the aws secrets manager secret. Default is "pelicanservice-g3auto" | +| externalSecrets.sowerjobsG3auto | string | `nil` | Will override the name of the aws secrets manager secret. Default is "sower-jobs-g3auto" | +| fullnameOverride | string | `""` | Override the full name of the deployment. | +| gen3Namespace | string | `"default"` | Namespace to deploy the job. | +| global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | +| global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | +| global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | +| global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | +| global.dev | bool | `true` | Whether the deployment is for development purposes. | +| global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | +| global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any sower secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | +| global.hostname | string | `"localhost"` | Hostname for the deployment. | +| global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | +| global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.portalApp | string | `"gitops"` | Portal application name. | +| global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | +| global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | +| global.postgres.master.host | string | `nil` | hostname of postgres server | +| global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | +| global.postgres.master.port | string | `"5432"` | Port for Postgres. | +| global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | +| global.publicDataSets | bool | `true` | Whether public datasets are enabled. | +| global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | +| global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | +| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/sower","tag":""}` | Docker image information. | +| image.pullPolicy | string | `"Always"` | Docker pull policy. | +| image.repository | string | `"quay.io/cdis/sower"` | Docker repository. | +| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | +| imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | +| nameOverride | string | `""` | Override the name of the chart. | +| netPolicy | map | `{"egressApps":["pidgin"],"ingressApps":["pidgin"]}` | Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true | +| netPolicy.egressApps | array | `["pidgin"]` | List of apps that this app requires egress to | +| netPolicy.ingressApps | array | `["pidgin"]` | List of app labels that require ingress to this service | +| nodeSelector | map | `{}` | Node Selector for the pods | +| partOf | string | `"Core-Service"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | +| podSecurityContext | map | `{"fsGroup":1000,"runAsUser":1000}` | Security context to apply to the pod | +| podSecurityContext.fsGroup | int | `1000` | Group that Kubernetes will change the permissions of all files in volumes to when volumes are mounted by a pod. | +| podSecurityContext.runAsUser | int | `1000` | User that all the processes will run under in the container. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | +| replicaCount | int | `1` | Number of replicas for the deployment. | +| resources | map | `{"limits":{"memory":"400Mi"},"requests":{"cpu":"100m","memory":"20Mi"}}` | Resource requests and limits for the containers in the pod | +| resources.limits | map | `{"memory":"400Mi"}` | The maximum amount of resources that the container is allowed to use | +| resources.limits.memory | string | `"400Mi"` | The maximum amount of memory the container can use | +| resources.requests | map | `{"cpu":"100m","memory":"20Mi"}` | The amount of resources that the container requests | +| resources.requests.cpu | string | `"100m"` | The amount of CPU requested | +| resources.requests.memory | string | `"20Mi"` | The amount of memory requested | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null}` | Values for sower secrets and keys for External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS access key ID. Overrides global key. | +| securityContext | map | `{}` | Security context for the containers in the pod | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | +| service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | +| service.port | int | `80` | The port number that the service exposes. | +| service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | +| serviceAccount | map | `{"annotations":{},"create":true,"name":"sower-service-account"}` | Service account to use or create. | +| serviceAccount.annotations | map | `{}` | Annotations to add to the service account. | +| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | +| serviceAccount.name | string | `"sower-service-account"` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | +| sowerConfig[0].action | string | `"export"` | | +| sowerConfig[0].container.cpu-limit | string | `"1"` | | +| sowerConfig[0].container.env[0].name | string | `"DICTIONARY_URL"` | | +| sowerConfig[0].container.env[0].valueFrom.configMapKeyRef.key | string | `"dictionary_url"` | | +| sowerConfig[0].container.env[0].valueFrom.configMapKeyRef.name | string | `"manifest-global"` | | +| sowerConfig[0].container.env[1].name | string | `"GEN3_HOSTNAME"` | | +| sowerConfig[0].container.env[1].valueFrom.configMapKeyRef.key | string | `"hostname"` | | +| sowerConfig[0].container.env[1].valueFrom.configMapKeyRef.name | string | `"manifest-global"` | | +| sowerConfig[0].container.env[2].name | string | `"ROOT_NODE"` | | +| sowerConfig[0].container.env[2].value | string | `"subject"` | | +| sowerConfig[0].container.env[3].name | string | `"DB_HOST"` | | +| sowerConfig[0].container.env[3].valueFrom.secretKeyRef.key | string | `"host"` | | +| sowerConfig[0].container.env[3].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[0].container.env[4].name | string | `"DB_DATABASE"` | | +| sowerConfig[0].container.env[4].valueFrom.secretKeyRef.key | string | `"database"` | | +| sowerConfig[0].container.env[4].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[0].container.env[5].name | string | `"DB_USER"` | | +| sowerConfig[0].container.env[5].valueFrom.secretKeyRef.key | string | `"username"` | | +| sowerConfig[0].container.env[5].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[0].container.env[6].name | string | `"DB_PASS"` | | +| sowerConfig[0].container.env[6].valueFrom.secretKeyRef.key | string | `"password"` | | +| sowerConfig[0].container.env[6].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[0].container.env[7].name | string | `"SHEEPDOG"` | | +| sowerConfig[0].container.env[7].valueFrom.secretKeyRef.key | string | `"sheepdog"` | | +| sowerConfig[0].container.env[7].valueFrom.secretKeyRef.name | string | `"indexd-service-creds"` | | +| sowerConfig[0].container.image | string | `"quay.io/cdis/pelican-export:master"` | | +| sowerConfig[0].container.memory-limit | string | `"12Gi"` | | +| sowerConfig[0].container.name | string | `"job-task"` | | +| sowerConfig[0].container.pull_policy | string | `"Always"` | | +| sowerConfig[0].container.volumeMounts[0].mountPath | string | `"/pelican-creds.json"` | | +| sowerConfig[0].container.volumeMounts[0].name | string | `"pelican-creds-volume"` | | +| sowerConfig[0].container.volumeMounts[0].readOnly | bool | `true` | | +| sowerConfig[0].container.volumeMounts[0].subPath | string | `"config.json"` | | +| sowerConfig[0].name | string | `"pelican-export"` | | +| sowerConfig[0].restart_policy | string | `"Never"` | | +| sowerConfig[0].volumes[0].name | string | `"pelican-creds-volume"` | | +| sowerConfig[0].volumes[0].secret.secretName | string | `"pelicanservice-g3auto"` | | +| sowerConfig[1].action | string | `"export-files"` | | +| sowerConfig[1].container.cpu-limit | string | `"1"` | | +| sowerConfig[1].container.env[0].name | string | `"DICTIONARY_URL"` | | +| sowerConfig[1].container.env[0].valueFrom.configMapKeyRef.key | string | `"dictionary_url"` | | +| sowerConfig[1].container.env[0].valueFrom.configMapKeyRef.name | string | `"manifest-global"` | | +| sowerConfig[1].container.env[1].name | string | `"GEN3_HOSTNAME"` | | +| sowerConfig[1].container.env[1].valueFrom.configMapKeyRef.key | string | `"hostname"` | | +| sowerConfig[1].container.env[1].valueFrom.configMapKeyRef.name | string | `"manifest-global"` | | +| sowerConfig[1].container.env[2].name | string | `"ROOT_NODE"` | | +| sowerConfig[1].container.env[2].value | string | `"file"` | | +| sowerConfig[1].container.env[3].name | string | `"EXTRA_NODES"` | | +| sowerConfig[1].container.env[3].value | string | `""` | | +| sowerConfig[1].container.env[4].name | string | `"DB_HOST"` | | +| sowerConfig[1].container.env[4].valueFrom.secretKeyRef.key | string | `"host"` | | +| sowerConfig[1].container.env[4].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[1].container.env[5].name | string | `"DB_DATABASE"` | | +| sowerConfig[1].container.env[5].valueFrom.secretKeyRef.key | string | `"database"` | | +| sowerConfig[1].container.env[5].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[1].container.env[6].name | string | `"DB_USER"` | | +| sowerConfig[1].container.env[6].valueFrom.secretKeyRef.key | string | `"username"` | | +| sowerConfig[1].container.env[6].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[1].container.env[7].name | string | `"DB_PASS"` | | +| sowerConfig[1].container.env[7].valueFrom.secretKeyRef.key | string | `"password"` | | +| sowerConfig[1].container.env[7].valueFrom.secretKeyRef.name | string | `"peregrine-dbcreds"` | | +| sowerConfig[1].container.env[8].name | string | `"SHEEPDOG"` | | +| sowerConfig[1].container.env[8].valueFrom.secretKeyRef.key | string | `"sheepdog"` | | +| sowerConfig[1].container.env[8].valueFrom.secretKeyRef.name | string | `"indexd-service-creds"` | | +| sowerConfig[1].container.image | string | `"quay.io/cdis/pelican-export:master"` | | +| sowerConfig[1].container.memory-limit | string | `"12Gi"` | | +| sowerConfig[1].container.name | string | `"job-task"` | | +| sowerConfig[1].container.pull_policy | string | `"Always"` | | +| sowerConfig[1].container.volumeMounts[0].mountPath | string | `"/pelican-creds.json"` | | +| sowerConfig[1].container.volumeMounts[0].name | string | `"pelican-creds-volume"` | | +| sowerConfig[1].container.volumeMounts[0].readOnly | bool | `true` | | +| sowerConfig[1].container.volumeMounts[0].subPath | string | `"config.json"` | | +| sowerConfig[1].container.volumeMounts[1].mountPath | string | `"/peregrine-creds.json"` | | +| sowerConfig[1].container.volumeMounts[1].name | string | `"peregrine-creds-volume"` | | +| sowerConfig[1].container.volumeMounts[1].readOnly | bool | `true` | | +| sowerConfig[1].container.volumeMounts[1].subPath | string | `"creds.json"` | | +| sowerConfig[1].name | string | `"pelican-export-files"` | | +| sowerConfig[1].restart_policy | string | `"Never"` | | +| sowerConfig[1].volumes[0].name | string | `"pelican-creds-volume"` | | +| sowerConfig[1].volumes[0].secret.secretName | string | `"pelicanservice-g3auto"` | | +| sowerjobsG3auto | string | `"{\n \"index-object-manifest\": {\n \"job_requires\": {\n \"arborist_url\": \"http://arborist-service\",\n \"job_access_req\": []\n },\n \"bucket\": \"$bucketName\",\n \"indexd_user\": \"diirm\",\n \"indexd_password\": \"$indexdPassword\"\n },\n \"download-indexd-manifest\": {\n \"job_requires\": {\n \"arborist_url\": \"http://arborist-service\",\n \"job_access_req\": []\n },\n \"bucket\": \"$bucketName\"\n },\n \"get-dbgap-metadata\": {\n \"job_requires\": {\n \"arborist_url\": \"http://arborist-service\",\n \"job_access_req\": []\n },\n \"bucket\": \"$bucketName\"\n },\n \"ingest-metadata-manifest\": {\n \"job_requires\": {\n \"arborist_url\": \"http://arborist-service\",\n \"job_access_req\": []\n },\n \"bucket\": \"$bucketName\"\n }\n}\n"` | Additional configuration for Sower Jobs Passed in as a multiline string. This secret can be mounted in sowerConfig. | +| strategy | map | `{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"}` | Rolling update deployment strategy | +| strategy.rollingUpdate.maxSurge | int | `1` | Number of additional replicas to add during rollout. | +| strategy.rollingUpdate.maxUnavailable | int | `0` | Maximum amount of pods that can be unavailable during the update. | +| tolerations | list | `[]` | Tolerations for the pods | +| volumeMounts | list | `[{"mountPath":"/sower_config.json","name":"sower-config","readOnly":true,"subPath":"sower_config.json"}]` | Volumes to mount to the container. | +| volumes | list | `[{"configMap":{"items":[{"key":"json","path":"sower_config.json"}],"name":"manifest-sower"},"name":"sower-config"}]` | Volumes to attach to the container. | diff --git a/helm/sower/templates/NOTES.txt b/helm/sower/templates/NOTES.txt new file mode 100644 index 00000000..c1e7e1ae --- /dev/null +++ b/helm/sower/templates/NOTES.txt @@ -0,0 +1 @@ +{{ .Chart.Name }} has been deployed successfully. diff --git a/helm/sower/templates/_helpers.tpl b/helm/sower/templates/_helpers.tpl new file mode 100644 index 00000000..8f5b72b8 --- /dev/null +++ b/helm/sower/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "sower.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "sower.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "sower.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "sower.labels" -}} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "sower.selectorLabels" -}} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "sower.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "sower.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* + Pelicanservice g3 Auto Secrets Manager Name +*/}} +{{- define "pelicanservice-g3auto" -}} +{{- default "pelicanservice-g3auto" .Values.externalSecrets.pelicanserviceG3auto }} +{{- end }} + +{{/* + Sowerjobs g3 Auto Secrets Manager Name +*/}} +{{- define "sower-jobs-g3auto" -}} +{{- default "sower-jobs-g3auto" .Values.externalSecrets.sowerjobsG3auto }} +{{- end }} \ No newline at end of file diff --git a/helm/sower/templates/aws-config.yaml b/helm/sower/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/sower/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/sower/templates/deployment.yaml b/helm/sower/templates/deployment.yaml new file mode 100644 index 00000000..456c6cca --- /dev/null +++ b/helm/sower/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sower + annotations: + gen3.io/network-ingress: "pidgin" + labels: + {{- include "sower.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "sower.selectorLabels" . | nindent 8 }} + revisionHistoryLimit: 2 + strategy: + {{- toYaml .Values.strategy | nindent 8 }} + template: + metadata: + labels: + netnolimit: "yes" + public: "yes" + {{- include "sower.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "sower.serviceAccountName" . }} + volumes: + {{- toYaml .Values.volumes | nindent 8 }} + containers: + - name: sower + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + {{- toYaml .Values.volumeMounts | nindent 12 }} + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: DICTIONARY_URL + valueFrom: + configMapKeyRef: + name: manifest-global + key: dictionary_url + optional: true + - name: GEN3_HOSTNAME + value: {{ .Values.global.hostname }} + ports: + - name: http + containerPort: 8000 + protocol: TCP + livenessProbe: + httpGet: + path: /_status + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 60 + timeoutSeconds: 30 + readinessProbe: + httpGet: + path: /_status + port: 8000 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/sower/templates/external-secret.yaml b/helm/sower/templates/external-secret.yaml new file mode 100644 index 00000000..5296a3d0 --- /dev/null +++ b/helm/sower/templates/external-secret.yaml @@ -0,0 +1,37 @@ +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: pelicanservice-g3auto +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: pelicanservice-g3auto + creationPolicy: Owner + data: + - secretKey: config.json + remoteRef: + #name of secret in secrets manager + key: {{include "pelicanservice-g3auto" .}} +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: sower-jobs-g3auto +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: sower-jobs-g3auto + creationPolicy: Owner + data: + - secretKey: config.json + remoteRef: + #name of secret in secrets manager + key: {{include "sower-jobs-g3auto" .}} +{{- end }} \ No newline at end of file diff --git a/helm/pidgin/templates/hpa.yaml b/helm/sower/templates/hpa.yaml similarity index 85% rename from helm/pidgin/templates/hpa.yaml rename to helm/sower/templates/hpa.yaml index d16ecf05..cf898b78 100644 --- a/helm/pidgin/templates/hpa.yaml +++ b/helm/sower/templates/hpa.yaml @@ -2,14 +2,14 @@ apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: {{ include "pidgin.fullname" . }} + name: {{ include "sower.fullname" . }} labels: - {{- include "pidgin.labels" . | nindent 4 }} + {{- include "sower.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment - name: {{ include "pidgin.fullname" . }} + name: {{ include "sower.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: diff --git a/helm/sower/templates/manifest-sower.yaml b/helm/sower/templates/manifest-sower.yaml new file mode 100644 index 00000000..8c70a330 --- /dev/null +++ b/helm/sower/templates/manifest-sower.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: manifest-sower +data: + json: |- + {{ .Values.sowerConfig | toJson | nindent 4 }} diff --git a/helm/sower/templates/netpolicy.yaml b/helm/sower/templates/netpolicy.yaml new file mode 100644 index 00000000..227f111c --- /dev/null +++ b/helm/sower/templates/netpolicy.yaml @@ -0,0 +1,22 @@ +{{ include "common.ingress_netpolicy" . }} + +--- + +{{ include "common.egress_netpolicy" . }} + +--- + +{{- if .Values.global.netPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: sowerjob-netpolicy +spec: + podSelector: + matchLabels: + app: sowerjob + egress: + - {} + policyTypes: + - Egress +{{- end }} diff --git a/helm/sower/templates/pelican-creds.yaml b/helm/sower/templates/pelican-creds.yaml new file mode 100644 index 00000000..0d3420f5 --- /dev/null +++ b/helm/sower/templates/pelican-creds.yaml @@ -0,0 +1,17 @@ +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sPelicanServiceSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: pelicanservice-g3auto +type: Opaque +{{- if .Values.global.aws.enabled }} +stringData: + config.json: | + { + "manifest_bucket_name": "{{ .Values.pelican.bucket }}", + "hostname": "{{ .Values.global.hostname }}", + "aws_access_key_id": "{{ .Values.secrets.awsAccessKeyId | default .Values.global.aws.awsAccessKeyId }}", + "aws_secret_access_key": "{{ .Values.secrets.awsSecretAccessKey | default .Values.global.aws.awsSecretAccessKey }}" + } +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/sower/templates/role-binding.yaml b/helm/sower/templates/role-binding.yaml new file mode 100644 index 00000000..94d7e189 --- /dev/null +++ b/helm/sower/templates/role-binding.yaml @@ -0,0 +1,12 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: sower-binding +subjects: +- kind: ServiceAccount + name: {{ include "sower.serviceAccountName" . }} + apiGroup: "" +roleRef: + kind: ClusterRole + name: admin + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/helm/sower/templates/secret-store.yaml b/helm/sower/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/sower/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/elasticsearch/templates/service.yaml b/helm/sower/templates/service.yaml similarity index 50% rename from helm/elasticsearch/templates/service.yaml rename to helm/sower/templates/service.yaml index 794cb991..eb027642 100644 --- a/helm/elasticsearch/templates/service.yaml +++ b/helm/sower/templates/service.yaml @@ -1,14 +1,16 @@ apiVersion: v1 kind: Service metadata: - name: elasticsearch + name: sower-service labels: - {{- include "elasticsearch.labels" . | nindent 4 }} + {{- include "sower.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} - targetPort: 9200 + targetPort: http protocol: TCP + name: http selector: - {{- include "elasticsearch.selectorLabels" . | nindent 4 }} + {{- include "sower.selectorLabels" . | nindent 4 }} + \ No newline at end of file diff --git a/helm/sower/templates/serviceaccount.yaml b/helm/sower/templates/serviceaccount.yaml new file mode 100644 index 00000000..a3bedfee --- /dev/null +++ b/helm/sower/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "sower.serviceAccountName" . }} + labels: + {{- include "sower.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + {{- if .Values.global.aws.enabled }} + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::{{ .Values.global.aws.account }}:role/{{ .Values.global.aws.sower_role }} + {{- end }} +{{- end }} diff --git a/helm/sower/templates/sower-jobs-g3auto.yaml b/helm/sower/templates/sower-jobs-g3auto.yaml new file mode 100644 index 00000000..0a789e88 --- /dev/null +++ b/helm/sower/templates/sower-jobs-g3auto.yaml @@ -0,0 +1,10 @@ +{{- if or (not .Values.global.externalSecrets.deploy) (and .Values.global.externalSecrets.deploy .Values.externalSecrets.createK8sSowerJobsSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: sower-jobs-g3auto +type: Opaque +stringData: + config.json: | + {{ .Values.sowerjobsG3auto | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/pidgin/templates/tests/test-connection.yaml b/helm/sower/templates/tests/test-connection.yaml similarity index 54% rename from helm/pidgin/templates/tests/test-connection.yaml rename to helm/sower/templates/tests/test-connection.yaml index 0f00775d..6890bcd7 100644 --- a/helm/pidgin/templates/tests/test-connection.yaml +++ b/helm/sower/templates/tests/test-connection.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Pod metadata: - name: "pidgin-test-connection" + name: "sower-test-connection" labels: - {{- include "pidgin.labels" . | nindent 4 }} + {{- include "sower.labels" . | nindent 4 }} annotations: "helm.sh/hook": test spec: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "pidgin.fullname" . }}:{{ .Values.service.port }}'] + args: ['sower-service:{{ .Values.service.port }}/_status'] restartPolicy: Never diff --git a/helm/sower/values.yaml b/helm/sower/values.yaml new file mode 100644 index 00000000..d3b7d5a9 --- /dev/null +++ b/helm/sower/values.yaml @@ -0,0 +1,399 @@ +# Default values for sower. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Global configuration +global: + # -- (map) AWS configuration + aws: + # -- (bool) Set to true if deploying to AWS. Controls ingress annotations. + enabled: false + # -- (string) Credentials for AWS stuff. + awsAccessKeyId: + # -- (string) Credentials for AWS stuff. + awsSecretAccessKey: + # -- (bool) Whether the deployment is for development purposes. + dev: true + + postgres: + # -- (bool) Whether the database should be created. + dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" + # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres + master: + # -- (string) hostname of postgres server + host: + # -- (string) username of superuser in postgres. This is used to create or restore databases + username: postgres + # -- (string) password for superuser in postgres. This is used to create or restore databases + password: + # -- (string) Port for Postgres. + port: "5432" + # -- (string) Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. + environment: default + # -- (string) Hostname for the deployment. + hostname: localhost + # -- (string) ARN of the reverse proxy certificate. + revproxyArn: arn:aws:acm:us-east-1:123456:certificate + # -- (string) URL of the data dictionary. + dictionaryUrl: https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json + # -- (string) Portal application name. + portalApp: gitops + # -- (string) S3 bucket name for Kubernetes manifest files. + kubeBucket: kube-gen3 + # -- (string) S3 bucket name for log files. + logsBucket: logs-gen3 + # -- (bool) Whether public datasets are enabled. + publicDataSets: true + # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` + tierAccessLevel: libre + # -- (map) Controls network policy settings + netPolicy: + enabled: false + # -- (int) Number of dispatcher jobs. + dispatcherJobNum: "10" + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any sower secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will create the Helm "pelicanservice-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sPelicanServiceSecret: false + # -- (string) Will override the name of the aws secrets manager secret. Default is "pelicanservice-g3auto" + pelicanserviceG3auto: + # -- (string) Will create the Helm "sower-jobs-g3auto" secret even if Secrets Manager is enabled. This is helpful if you are wanting to use External Secrets for some, but not all secrets. + createK8sSowerJobsSecret: false + # -- (string) Will override the name of the aws secrets manager secret. Default is "sower-jobs-g3auto" + sowerjobsG3auto: + +# -- (map) Values for sower secrets and keys for External Secrets. +secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS access key ID. Overrides global key. + awsSecretAccessKey: + +# -- (int) Number of replicas for the deployment. +replicaCount: 1 + +# -- (map) Docker image information. +image: + # -- (string) Docker repository. + repository: quay.io/cdis/sower + # -- (string) Docker pull policy. + pullPolicy: Always + # -- (string) Overrides the image tag whose default is the chart appVersion. + tag: "" + +# -- (list) Docker image pull secrets. +imagePullSecrets: [] + +# -- (string) Override the name of the chart. +nameOverride: "" + +# -- (string) Override the full name of the deployment. +fullnameOverride: "" + +# -- (map) Security context for the containers in the pod +securityContext: + {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# -- (map) Kubernetes service information. +service: + # -- (string) Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". + type: ClusterIP + # -- (int) The port number that the service exposes. + port: 80 + +# -- (map) Configuration for network policies created by this chart. Only relevant if "global.netPolicy.enabled" is set to true +netPolicy: + # -- (array) List of app labels that require ingress to this service + ingressApps: + - pidgin + + # -- (array) List of apps that this app requires egress to + egressApps: + - pidgin + +# -- (map) Configuration for autoscaling the number of replicas +autoscaling: + # -- (bool) Whether autoscaling is enabled + enabled: false + # -- (int) The minimum number of replicas to scale down to + minReplicas: 1 + # -- (int) The maximum number of replicas to scale up to + maxReplicas: 100 + # -- (int) Target CPU utilization percentage + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# -- (map) Node Selector for the pods +nodeSelector: {} + +# -- (list) Tolerations for the pods +tolerations: [] + +# -- (map) Security context to apply to the pod +podSecurityContext: + # -- (int) User that all the processes will run under in the container. + runAsUser: 1000 + # -- (int) Group that Kubernetes will change the permissions of all files in volumes to when volumes are mounted by a pod. + fsGroup: 1000 + +# -- (map) Affinity to use for the deployment. +affinity: + podAntiAffinity: + # -- (map) Option for scheduling to be required or preferred. + preferredDuringSchedulingIgnoredDuringExecution: + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - sower + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" + +# -- (list) Volumes to attach to the container. +volumes: + - name: sower-config + configMap: + name: manifest-sower + items: + - key: json + path: sower_config.json +# -- (list) Volumes to mount to the container. +volumeMounts: + - name: sower-config + readOnly: true + mountPath: /sower_config.json + subPath: sower_config.json + +# -- (string) AWS region to be used. +awsRegion: us-east-1 +# -- (string) AWS STS to issue temporary credentials to users and roles that make an AWS STS request. Values regional or global. +awsStsRegionalEndpoints: regional +# -- (string) Namespace to deploy the job. +gen3Namespace: default + +# -- (map) Resource requests and limits for the containers in the pod +resources: + # -- (map) The amount of resources that the container requests + requests: + # -- (string) The amount of CPU requested + cpu: 100m + # -- (string) The amount of memory requested + memory: 20Mi + # -- (map) The maximum amount of resources that the container is allowed to use + limits: + # -- (string) The maximum amount of memory the container can use + memory: 400Mi + +# -- (map) Rolling update deployment strategy +strategy: + type: RollingUpdate + rollingUpdate: + # -- (int) Number of additional replicas to add during rollout. + maxSurge: 1 + # -- (int) Maximum amount of pods that can be unavailable during the update. + maxUnavailable: 0 + +# -- (bool) Automount the default service account token +automountServiceAccountToken: true + +sowerConfig: + - name: pelican-export + action: export + container: + name: job-task + image: quay.io/cdis/pelican-export:master + pull_policy: Always + env: + - name: DICTIONARY_URL + valueFrom: + configMapKeyRef: + name: manifest-global + key: dictionary_url + - name: GEN3_HOSTNAME + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname + - name: ROOT_NODE + value: subject + - name: DB_HOST + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: host + - name: DB_DATABASE + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: database + - name: DB_USER + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: password + - name: SHEEPDOG + valueFrom: + secretKeyRef: + name: indexd-service-creds + key: sheepdog + volumeMounts: + - name: pelican-creds-volume + readOnly: true + mountPath: "/pelican-creds.json" + subPath: config.json + cpu-limit: "1" + memory-limit: 12Gi + volumes: + - name: pelican-creds-volume + secret: + secretName: pelicanservice-g3auto + restart_policy: Never + - name: pelican-export-files + action: export-files + container: + name: job-task + image: quay.io/cdis/pelican-export:master + pull_policy: Always + env: + - name: DICTIONARY_URL + valueFrom: + configMapKeyRef: + name: manifest-global + key: dictionary_url + - name: GEN3_HOSTNAME + valueFrom: + configMapKeyRef: + name: manifest-global + key: hostname + - name: ROOT_NODE + value: file + - name: EXTRA_NODES + value: "" + - name: DB_HOST + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: host + - name: DB_DATABASE + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: database + - name: DB_USER + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: peregrine-dbcreds + key: password + - name: SHEEPDOG + valueFrom: + secretKeyRef: + name: indexd-service-creds + key: sheepdog + volumeMounts: + - name: pelican-creds-volume + readOnly: true + mountPath: "/pelican-creds.json" + subPath: config.json + - name: peregrine-creds-volume + readOnly: true + mountPath: "/peregrine-creds.json" + subPath: creds.json + cpu-limit: "1" + memory-limit: 12Gi + volumes: + - name: pelican-creds-volume + secret: + secretName: pelicanservice-g3auto + restart_policy: Never + +# -- (string) Additional configuration for Sower Jobs Passed in as a multiline string. This secret can be mounted in sowerConfig. +sowerjobsG3auto: | + { + "index-object-manifest": { + "job_requires": { + "arborist_url": "http://arborist-service", + "job_access_req": [] + }, + "bucket": "$bucketName", + "indexd_user": "diirm", + "indexd_password": "$indexdPassword" + }, + "download-indexd-manifest": { + "job_requires": { + "arborist_url": "http://arborist-service", + "job_access_req": [] + }, + "bucket": "$bucketName" + }, + "get-dbgap-metadata": { + "job_requires": { + "arborist_url": "http://arborist-service", + "job_access_req": [] + }, + "bucket": "$bucketName" + }, + "ingest-metadata-manifest": { + "job_requires": { + "arborist_url": "http://arborist-service", + "job_access_req": [] + }, + "bucket": "$bucketName" + } + } + +# -- (map) Service account to use or create. +serviceAccount: + # -- (bool) Specifies whether a service account should be created. + create: true + # -- (map) Annotations to add to the service account. + annotations: {} + # -- (string) The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "sower-service-account" + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "false" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Core-Service" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/helm/ssjdispatcher/Chart.yaml b/helm/ssjdispatcher/Chart.yaml index 4f471ede..f56d5b40 100644 --- a/helm/ssjdispatcher/Chart.yaml +++ b/helm/ssjdispatcher/Chart.yaml @@ -15,10 +15,15 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.2 +version: 0.1.15 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "master" + +dependencies: + - name: common + version: 0.1.16 + repository: file://../common diff --git a/helm/ssjdispatcher/README.md b/helm/ssjdispatcher/README.md index 1c631f51..e5b9890e 100644 --- a/helm/ssjdispatcher/README.md +++ b/helm/ssjdispatcher/README.md @@ -1,9 +1,15 @@ # ssjdispatcher -![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.15](https://img.shields.io/badge/Version-0.1.15-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 ssjdispatcher +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../common | common | 0.1.16 | + ## Values | Key | Type | Default | Description | @@ -23,26 +29,33 @@ A Helm chart for gen3 ssjdispatcher | autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage | | awsRegion | string | `"us-east-1"` | AWS region to be used. | | awsStsRegionalEndpoints | string | `"regional"` | AWS STS to issue temporary credentials to users and roles that make an AWS STS request. Values regional or global. | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | | dispatcherJobNum | string | `"10"` | Ssjdispater job number. | +| externalSecrets | map | `{"credsFile":null}` | External secrets configuration | +| externalSecrets.credsFile | string | `nil` | Will override the name of the aws secrets manager secret. Default is "credentials.json" | | fullnameOverride | string | `""` | Override the full name of the deployment. | | gen3Namespace | string | `"default"` | Namespace to deploy the job. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any indexd secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -50,23 +63,21 @@ A Helm chart for gen3 ssjdispatcher | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | -| image | map | `{"pullPolicy":"IfNotPresent","repository":"nginx","tag":""}` | Docker image information. | -| image.pullPolicy | string | `"IfNotPresent"` | Docker pull policy. | -| image.repository | string | `"nginx"` | Docker repository. | -| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | +| image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/ssjdispatcher","tag":"2022.08"}` | Docker image information. | +| image.pullPolicy | string | `"Always"` | Docker pull policy. | +| image.repository | string | `"quay.io/cdis/ssjdispatcher"` | Docker repository. | +| image.tag | string | `"2022.08"` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | | indexing | string | `"707767160287.dkr.ecr.us-east-1.amazonaws.com/gen3/indexs3client:2022.08"` | Image to use for the "indexing" job. | -| labels | map | `{"netnolimit":"yes","public":"yes"}` | Labels for the ssjdispatcher service. | -| labels.netnolimit | string | `"yes"` | Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs | -| labels.public | string | `"yes"` | Grants ingress from the revproxy service for pods labeled with public=yes | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | | nodeSelector | map | `{}` | Node Selector for the pods | +| partOf | string | `"Workspace-Tab"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podSecurityContext | map | `{"fsGroup":1000,"runAsUser":1000}` | Security context to apply to the pod | | podSecurityContext.fsGroup | int | `1000` | Group that Kubernetes will change the permissions of all files in volumes to when volumes are mounted by a pod. | | podSecurityContext.runAsUser | int | `1000` | User that all the processes will run under in the container. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | | resources | map | `{"limits":{"cpu":1,"memory":"2400Mi"},"requests":{"cpu":0.1,"memory":"128Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":1,"memory":"2400Mi"}` | The maximum amount of resources that the container is allowed to use | @@ -76,7 +87,7 @@ A Helm chart for gen3 ssjdispatcher | resources.requests.cpu | string | `0.1` | The amount of CPU requested | | resources.requests.memory | string | `"128Mi"` | The amount of memory requested | | securityContext | map | `{}` | Security context for the containers in the pod | -| selectorLabels | map | `{"app":"ssjdispatcher","release":"production"}` | Labels to use for selecting the deployment. | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"port":80,"type":"ClusterIP"}` | Kubernetes service information. | | service.port | int | `80` | The port number that the service exposes. | | service.type | string | `"ClusterIP"` | Type of service. Valid values are "ClusterIP", "NodePort", "LoadBalancer", "ExternalName". | @@ -102,6 +113,3 @@ A Helm chart for gen3 ssjdispatcher | tolerations | list | `[]` | Tolerations for the pods | | volumeMounts | list | `[{"mountPath":"/credentials.json","name":"ssjdispatcher-creds-volume","readOnly":true,"subPath":"credentials.json"}]` | Volumes to mount to the container. | | volumes | list | `[{"name":"ssjdispatcher-creds-volume","secret":{"secretName":"ssjdispatcher-creds"}}]` | Volumes to attach to the container. | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/ssjdispatcher/templates/_helpers.tpl b/helm/ssjdispatcher/templates/_helpers.tpl index 2fbe0deb..aee3241b 100644 --- a/helm/ssjdispatcher/templates/_helpers.tpl +++ b/helm/ssjdispatcher/templates/_helpers.tpl @@ -34,20 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "ssjdispatcher.labels" -}} -helm.sh/chart: {{ include "ssjdispatcher.chart" . }} -{{ include "ssjdispatcher.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "ssjdispatcher.selectorLabels" -}} -app.kubernetes.io/name: {{ include "ssjdispatcher.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -60,3 +66,10 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* + ssjdispatcher credentials.json Secrets Manager Name +*/}} +{{- define "creds-file" -}} +{{- default "credentials.json" .Values.externalSecrets.credsFile }} +{{- end }} diff --git a/helm/ssjdispatcher/templates/deployment.yaml b/helm/ssjdispatcher/templates/deployment.yaml index f9c8baba..7d03f7fa 100644 --- a/helm/ssjdispatcher/templates/deployment.yaml +++ b/helm/ssjdispatcher/templates/deployment.yaml @@ -2,24 +2,26 @@ apiVersion: apps/v1 kind: Deployment metadata: name: ssjdispatcher + labels: + {{- include "ssjdispatcher.labels" . | nindent 4 }} spec: selector: - {{- with .Values.selectorLabels }} matchLabels: - {{- toYaml . | nindent 8 }} - {{- end }} + {{- include "ssjdispatcher.selectorLabels" . | nindent 8 }} revisionHistoryLimit: 2 strategy: {{- toYaml .Values.strategy | nindent 8 }} template: metadata: - {{- with .Values.labels }} labels: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.selectorLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} + netnolimit: "yes" + public: "yes" + {{- include "ssjdispatcher.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} + annotations: + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} @@ -33,8 +35,8 @@ spec: {{- toYaml .Values.volumes | nindent 8 }} containers: - name: ssjdispatcher - image: "quay.io/cdis/ssjdispatcher:2022.08" - imagePullPolicy: Always + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} volumeMounts: {{- toYaml .Values.volumeMounts | nindent 12 }} env: diff --git a/helm/ssjdispatcher/templates/external-secret.yaml b/helm/ssjdispatcher/templates/external-secret.yaml new file mode 100644 index 00000000..70fe6bc3 --- /dev/null +++ b/helm/ssjdispatcher/templates/external-secret.yaml @@ -0,0 +1,19 @@ +{{ if .Values.global.externalSecrets.deploy }} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: ssjdispatcher-creds +spec: + refreshInterval: 5m + secretStoreRef: + name: {{include "common.SecretStore" .}} + kind: SecretStore + target: + name: ssjdispatcher-creds + creationPolicy: Owner + data: + - secretKey: credentials.json + remoteRef: + #name of secret in secrets manager + key: {{include "credsFile" .}} +{{- end }} \ No newline at end of file diff --git a/helm/ssjdispatcher/templates/netpolicy.yaml b/helm/ssjdispatcher/templates/netpolicy.yaml new file mode 100644 index 00000000..2e8a29a0 --- /dev/null +++ b/helm/ssjdispatcher/templates/netpolicy.yaml @@ -0,0 +1,14 @@ +{{- if .Values.global.netPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: ssjdispatcherjob-netpolicy +spec: + podSelector: + matchLabels: + app: ssjdispatcherjob + egress: + - {} + policyTypes: + - Egress +{{- end }} \ No newline at end of file diff --git a/helm/ssjdispatcher/templates/pdb.yaml b/helm/ssjdispatcher/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/ssjdispatcher/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/ssjdispatcher/templates/role-binding.yaml b/helm/ssjdispatcher/templates/role-binding.yaml new file mode 100644 index 00000000..d6f2ff02 --- /dev/null +++ b/helm/ssjdispatcher/templates/role-binding.yaml @@ -0,0 +1,12 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ssjdispatcher-binding +subjects: +- kind: ServiceAccount + name: {{ include "ssjdispatcher.serviceAccountName" . }} + apiGroup: "" +roleRef: + kind: ClusterRole + name: admin + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/helm/ssjdispatcher/templates/secret-store.yaml b/helm/ssjdispatcher/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/ssjdispatcher/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/ssjdispatcher/templates/serviceaccount.yaml b/helm/ssjdispatcher/templates/serviceaccount.yaml index ac52270f..f8a93321 100644 --- a/helm/ssjdispatcher/templates/serviceaccount.yaml +++ b/helm/ssjdispatcher/templates/serviceaccount.yaml @@ -10,3 +10,10 @@ metadata: eks.amazonaws.com/role-arn: arn:aws:iam::{{ .Values.global.aws.account }}:role/{{ .Values.global.aws.hatchery_role }} {{- end }} {{- end }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ssjdispatcher-job-sa + labels: + {{- include "ssjdispatcher.labels" . | nindent 4 }} \ No newline at end of file diff --git a/helm/ssjdispatcher/values.yaml b/helm/ssjdispatcher/values.yaml index d9b1c165..5272d2e3 100644 --- a/helm/ssjdispatcher/values.yaml +++ b/helm/ssjdispatcher/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -42,20 +44,28 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any indexd secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false # -- (int) Number of replicas for the deployment. replicaCount: 1 @@ -63,11 +73,11 @@ replicaCount: 1 # -- (map) Docker image information. image: # -- (string) Docker repository. - repository: nginx + repository: quay.io/cdis/ssjdispatcher # -- (string) Docker pull policy. - pullPolicy: IfNotPresent + pullPolicy: Always # -- (string) Overrides the image tag whose default is the chart appVersion. - tag: "" + tag: "2022.08" # -- (list) Docker image pull secrets. imagePullSecrets: [] @@ -79,7 +89,8 @@ nameOverride: "" fullnameOverride: "" # -- (map) Security context for the containers in the pod -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -112,18 +123,6 @@ nodeSelector: {} # -- (list) Tolerations for the pods tolerations: [] -# -- (map) Labels to use for selecting the deployment. -selectorLabels: - app: ssjdispatcher - release: production - -# -- (map) Labels for the ssjdispatcher service. -labels: - # -- (string) Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs - netnolimit: "yes" - # -- (string) Grants ingress from the revproxy service for pods labeled with public=yes - public: "yes" - # -- (map) Security context to apply to the pod podSecurityContext: # -- (int) User that all the processes will run under in the container. @@ -136,20 +135,20 @@ affinity: podAntiAffinity: # -- (map) Option for scheduling to be required or preferred. preferredDuringSchedulingIgnoredDuringExecution: - # -- (int) Weight value for preferred scheduling. - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - # -- (list) Label key for match expression. - - key: app - # -- (string) Operation type for the match expression. - operator: In - # -- (list) Value for the match expression key. - values: - - ssjdispatcher - # -- (string) Value for topology key label. - topologyKey: "kubernetes.io/hostname" + # -- (int) Weight value for preferred scheduling. + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + # -- (list) Label key for match expression. + - key: app + # -- (string) Operation type for the match expression. + operator: In + # -- (list) Value for the match expression key. + values: + - ssjdispatcher + # -- (string) Value for topology key label. + topologyKey: "kubernetes.io/hostname" # -- (list) Volumes to attach to the container. volumes: @@ -235,3 +234,21 @@ serviceAccount: # -- (string) The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "ssjdispatcher-service-account" + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Workspace-Tab" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: + + +# -- (map) External secrets configuration +externalSecrets: + # -- (string) Will override the name of the aws secrets manager secret. Default is "credentials.json" + credsFile: diff --git a/helm/wts/Chart.yaml b/helm/wts/Chart.yaml index a728a368..16de2705 100644 --- a/helm/wts/Chart.yaml +++ b/helm/wts/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.6 +version: 0.1.18 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -24,10 +24,10 @@ version: 0.1.6 appVersion: "master" dependencies: -- name: common - version: 0.1.4 - repository: file://../common -- name: postgresql - version: 11.9.13 - repository: "https://charts.bitnami.com/bitnami" - condition: postgres.separate + - name: common + version: 0.1.16 + repository: file://../common + - name: postgresql + version: 11.9.13 + repository: "https://charts.bitnami.com/bitnami" + condition: postgres.separate diff --git a/helm/wts/README.md b/helm/wts/README.md index e1511ce5..0d66cd6a 100644 --- a/helm/wts/README.md +++ b/helm/wts/README.md @@ -1,6 +1,6 @@ # wts -![Version: 0.1.6](https://img.shields.io/badge/Version-0.1.6-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) +![Version: 0.1.18](https://img.shields.io/badge/Version-0.1.18-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: master](https://img.shields.io/badge/AppVersion-master-informational?style=flat-square) A Helm chart for gen3 workspace token service @@ -8,7 +8,7 @@ A Helm chart for gen3 workspace token service | Repository | Name | Version | |------------|------|---------| -| file://../common | common | 0.1.4 | +| file://../common | common | 0.1.16 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 | ## Values @@ -21,24 +21,31 @@ A Helm chart for gen3 workspace token service | autoscaling.maxReplicas | int | `100` | The maximum number of replicas to scale up to | | autoscaling.minReplicas | int | `1` | The minimum number of replicas to scale down to | | autoscaling.targetCPUUtilizationPercentage | int | `80` | The target CPU utilization percentage for autoscaling | +| commonLabels | map | `nil` | Will completely override the commonLabels defined in the common chart's _label_setup.tpl | +| criticalService | string | `"true"` | Valid options are "true" or "false". If invalid option is set- the value will default to "false". | +| externalSecrets | map | `{"dbcreds":null}` | External Secrets settings. | +| externalSecrets.dbcreds | string | `nil` | Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" | | fullnameOverride | string | `""` | Override the full name of the deployment. | -| global | map | `{"aws":{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false},"ddEnabled":false,"dev":true,"dictionaryUrl":"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json","dispatcherJobNum":10,"environment":"default","hostname":"localhost","kubeBucket":"kube-gen3","logsBucket":"logs-gen3","netPolicy":true,"portalApp":"gitops","postgres":{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}},"publicDataSets":true,"revproxyArn":"arn:aws:acm:us-east-1:123456:certificate","syncFromDbgap":false,"tierAccessLevel":"libre","userYamlS3Path":"s3://cdis-gen3-users/test/user.yaml"}` | Global configuration options. | | global.aws | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"enabled":false}` | AWS configuration | | global.aws.awsAccessKeyId | string | `nil` | Credentials for AWS stuff. | | global.aws.awsSecretAccessKey | string | `nil` | Credentials for AWS stuff. | | global.aws.enabled | bool | `false` | Set to true if deploying to AWS. Controls ingress annotations. | -| global.ddEnabled | bool | `false` | Whether Datadog is enabled. | | global.dev | bool | `true` | Whether the deployment is for development purposes. | | global.dictionaryUrl | string | `"https://s3.amazonaws.com/dictionary-artifacts/datadictionary/develop/schema.json"` | URL of the data dictionary. | -| global.dispatcherJobNum | int | `10` | Number of dispatcher jobs. | +| global.dispatcherJobNum | int | `"10"` | Number of dispatcher jobs. | | global.environment | string | `"default"` | Environment name. This should be the same as vpcname if you're doing an AWS deployment. Currently this is being used to share ALB's if you have multiple namespaces. Might be used other places too. | +| global.externalSecrets | map | `{"deploy":false,"separateSecretStore":false}` | External Secrets settings. | +| global.externalSecrets.deploy | bool | `false` | Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any wts secrets you have deployed. | +| global.externalSecrets.separateSecretStore | string | `false` | Will deploy a separate External Secret Store for this service. | | global.hostname | string | `"localhost"` | Hostname for the deployment. | | global.kubeBucket | string | `"kube-gen3"` | S3 bucket name for Kubernetes manifest files. | | global.logsBucket | string | `"logs-gen3"` | S3 bucket name for log files. | -| global.netPolicy | bool | `true` | Whether network policies are enabled. | +| global.minAvialable | int | `1` | The minimum amount of pods that are available at all times if the PDB is deployed. | +| global.netPolicy | map | `{"enabled":false}` | Controls network policy settings | +| global.pdb | bool | `false` | If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. | | global.portalApp | string | `"gitops"` | Portal application name. | -| global.postgres | map | `{"dbCreate":true,"master":{"host":null,"password":null,"port":"5432","username":"postgres"}}` | Postgres database configuration. | | global.postgres.dbCreate | bool | `true` | Whether the database should be created. | +| global.postgres.externalSecret | string | `""` | Name of external secret. Disabled if empty | | global.postgres.master | map | `{"host":null,"password":null,"port":"5432","username":"postgres"}` | Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres | | global.postgres.master.host | string | `nil` | hostname of postgres server | | global.postgres.master.password | string | `nil` | password for superuser in postgres. This is used to create or restore databases | @@ -46,25 +53,20 @@ A Helm chart for gen3 workspace token service | global.postgres.master.username | string | `"postgres"` | username of superuser in postgres. This is used to create or restore databases | | global.publicDataSets | bool | `true` | Whether public datasets are enabled. | | global.revproxyArn | string | `"arn:aws:acm:us-east-1:123456:certificate"` | ARN of the reverse proxy certificate. | -| global.syncFromDbgap | bool | `false` | Whether to sync data from dbGaP. | | global.tierAccessLevel | string | `"libre"` | Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` | -| global.userYamlS3Path | string | `"s3://cdis-gen3-users/test/user.yaml"` | Path to the user.yaml file in S3. | | hostname | string | `nil` | Hostname for the deployment. | | image | map | `{"pullPolicy":"Always","repository":"quay.io/cdis/workspace-token-service","tag":"feat_wts_internalfence"}` | Docker image information. | | image.pullPolicy | string | `"Always"` | Docker pull policy. | | image.repository | string | `"quay.io/cdis/workspace-token-service"` | Docker repository. | | image.tag | string | `"feat_wts_internalfence"` | Overrides the image tag whose default is the chart appVersion. | | imagePullSecrets | list | `[]` | Docker image pull secrets. | +| metricsEnabled | bool | `false` | Whether Metrics are enabled. | | nameOverride | string | `""` | Override the name of the chart. | | nodeSelector | map | `{}` | Node Selector for the pods | | oidc_client_id | string | `nil` | Id for the OIDC client. | | oidc_client_secret | string | `nil` | Secret for the OIDC client. | +| partOf | string | `"Authentication"` | Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. | | podAnnotations | map | `{}` | Annotations to add to the pod. | -| podLabels | map | `{"netnolimit":"yes","public":"yes","release":"production","tags.datadoghq.com/service":"token-service","userhelper":"yes"}` | Labels to add to the pod. | -| podLabels.netnolimit | string | `"yes"` | Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs | -| podLabels.public | string | `"yes"` | Grants ingress from the revproxy service for pods labeled with public=yes | -| podLabels.release | string | `"production"` | Release name. | -| podLabels.userhelper | string | `"yes"` | Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes | | podSecurityContext | map | `{}` | Security context for the pod | | postgres | map | `{"database":null,"dbCreate":null,"dbRestore":false,"host":null,"password":null,"port":"5432","separate":false,"username":null}` | Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you | | postgres.database | string | `nil` | Database name for postgres. This is a service override, defaults to - | @@ -75,18 +77,21 @@ A Helm chart for gen3 workspace token service | postgres.separate | string | `false` | Will create a Database for the individual service to help with developing it. | | postgres.username | string | `nil` | Username for postgres. This is a service override, defaults to - | | postgresql | map | `{"primary":{"persistence":{"enabled":false}}}` | Postgresql subchart settings if deployed separately option is set to "true". Disable persistence by default so we can spin up and down ephemeral environments | -| release | string | `"production"` | Release name. | +| release | string | `"production"` | Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". | | replicaCount | int | `1` | Number of replicas for the deployment. | -| resources | map | `{"limits":{"cpu":0.5,"memory":"512Mi"},"requests":{"cpu":0.1,"memory":"12Mi"}}` | Resource requests and limits for the containers in the pod | +| resources | map | `{"limits":{"cpu":0.5,"memory":"512Mi"},"requests":{"cpu":0.2,"memory":"120Mi"}}` | Resource requests and limits for the containers in the pod | | resources.limits | map | `{"cpu":0.5,"memory":"512Mi"}` | The maximum amount of resources that the container is allowed to use | | resources.limits.cpu | string | `0.5` | The maximum amount of CPU the container can use | | resources.limits.memory | string | `"512Mi"` | The maximum amount of memory the container can use | -| resources.requests | map | `{"cpu":0.1,"memory":"12Mi"}` | The amount of resources that the container requests | -| resources.requests.cpu | string | `0.1` | The amount of CPU requested | -| resources.requests.memory | string | `"12Mi"` | The amount of memory requested | +| resources.requests | map | `{"cpu":0.2,"memory":"120Mi"}` | The amount of resources that the container requests | +| resources.requests.cpu | string | `0.2` | The amount of CPU requested | +| resources.requests.memory | string | `"120Mi"` | The amount of memory requested | | roleName | string | `"workspace-token-service"` | Name of the role to be used for the role binding. | -| secrets | map | `{"external_oidc":null}` | Values for wts secret. | +| secrets | map | `{"awsAccessKeyId":null,"awsSecretAccessKey":null,"external_oidc":null}` | Values for wts secret and keys for External Secrets. | +| secrets.awsAccessKeyId | str | `nil` | AWS access key ID. Overrides global key. | +| secrets.awsSecretAccessKey | str | `nil` | AWS secret access key ID. Overrides global key. | | securityContext | map | `{}` | Security context for the containers in the pod | +| selectorLabels | map | `nil` | Will completely override the selectorLabels defined in the common chart's _label_setup.tpl | | service | map | `{"httpPort":80,"httpsPort":443,"type":"ClusterIP"}` | Configuration for the service | | service.httpPort | int | `80` | Port on which the service is exposed | | service.httpsPort | int | `443` | Secure port on which the service is exposed | @@ -96,6 +101,3 @@ A Helm chart for gen3 workspace token service | serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | | serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | | tolerations | list | `[]` | Tolerations for the pods | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/helm/wts/templates/_helpers.tpl b/helm/wts/templates/_helpers.tpl index d13a9fca..a8a094c0 100644 --- a/helm/wts/templates/_helpers.tpl +++ b/helm/wts/templates/_helpers.tpl @@ -34,19 +34,26 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "wts.labels" -}} -helm.sh/chart: {{ include "wts.chart" . }} -{{ include "wts.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- if .Values.commonLabels }} + {{- with .Values.commonLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.commonLabels" .)}} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "wts.selectorLabels" -}} -app: wts +{{- if .Values.selectorLabels }} + {{- with .Values.selectorLabels }} + {{- toYaml . }} + {{- end }} +{{- else }} + {{- (include "common.selectorLabels" .)}} +{{- end }} {{- end }} {{/* @@ -71,4 +78,4 @@ Create the name of the service account to use {{- else }} {{- default .Values.postgres.password }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/wts/templates/aws-config.yaml b/helm/wts/templates/aws-config.yaml new file mode 100644 index 00000000..398770d3 --- /dev/null +++ b/helm/wts/templates/aws-config.yaml @@ -0,0 +1,3 @@ +{{- if or (.Values.secrets.awsSecretAccessKey) (.Values.global.aws.awsSecretAccessKey ) }} +{{ include "common.awsconfig" . }} +{{- end -}} \ No newline at end of file diff --git a/helm/wts/templates/db-init.yaml b/helm/wts/templates/db-init.yaml index abbefb6e..d99ca1b2 100644 --- a/helm/wts/templates/db-init.yaml +++ b/helm/wts/templates/db-init.yaml @@ -1,6 +1,6 @@ -{{ include "common.db_setup_job" . }} ---- {{ include "common.db-secret" . }} --- +{{ include "common.db_setup_job" . }} +--- {{ include "common.db_setup_sa" . }} --- diff --git a/helm/wts/templates/deployment.yaml b/helm/wts/templates/deployment.yaml index 8d76f320..1e109964 100644 --- a/helm/wts/templates/deployment.yaml +++ b/helm/wts/templates/deployment.yaml @@ -22,15 +22,19 @@ spec: maxUnavailable: 0 template: metadata: - {{- with .Values.podAnnotations }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.podLabels }} + {{- if .Values.metricsEnabled }} + {{- include "common.grafanaAnnotations" . | nindent 8 }} + {{- end }} labels: + netnolimit: "yes" + public: "yes" + userhelper: "yes" {{- include "wts.selectorLabels" . | nindent 8 }} - {{- toYaml . | nindent 8 }} - {{- end }} + {{- include "common.extraLabels" . | nindent 8 }} spec: affinity: podAntiAffinity: @@ -173,17 +177,16 @@ spec: - name: SECRET_CONFIG value: "/var/www/wts/appcreds.json" resources: - limits: - cpu: 0.8 - memory: 512Mi + {{- toYaml .Values.resources | nindent 12 }} command: ["/bin/sh"] args: - "-c" - | - if hash alembic 2>/dev/null; then + if hash alembic 2>/dev/null || poetry run alembic --version >/dev/null 2>&1; then echo "Running DB migration" cd /wts - alembic upgrade head + # Managing virtual environments via poetry instead of python since the AL base image update, but retaining backwards compatibility + poetry run alembic upgrade head || alembic upgrade head else echo "Alembic not installed - not running DB migration" fi diff --git a/helm/wts/templates/external-secret.yaml b/helm/wts/templates/external-secret.yaml new file mode 100644 index 00000000..70c278fe --- /dev/null +++ b/helm/wts/templates/external-secret.yaml @@ -0,0 +1 @@ +{{ include "common.externalSecret.db" . }} \ No newline at end of file diff --git a/helm/wts/templates/pdb.yaml b/helm/wts/templates/pdb.yaml new file mode 100644 index 00000000..2ef2de13 --- /dev/null +++ b/helm/wts/templates/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.global.pdb (gt (int .Values.replicaCount) 1) }} +{{ include "common.pod_disruption_budget" . }} +{{- end }} \ No newline at end of file diff --git a/helm/wts/templates/secret-store.yaml b/helm/wts/templates/secret-store.yaml new file mode 100644 index 00000000..771c7760 --- /dev/null +++ b/helm/wts/templates/secret-store.yaml @@ -0,0 +1,3 @@ +{{ if .Values.global.externalSecrets.separateSecretStore }} +{{ include "common.secretstore" . }} +{{- end }} \ No newline at end of file diff --git a/helm/wts/templates/tests/test-connection.yaml b/helm/wts/templates/tests/test-connection.yaml index 7f3f42c3..250d8c47 100644 --- a/helm/wts/templates/tests/test-connection.yaml +++ b/helm/wts/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "wts.fullname" . }}-test-connection" + name: "wts-test-connection" labels: {{- include "wts.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "wts.fullname" . }}:{{ .Values.service.port }}'] + args: ['workspace-token-service:80/_status'] restartPolicy: Never diff --git a/helm/wts/templates/wts-oidc.yaml b/helm/wts/templates/wts-oidc.yaml index 082bd7bf..769a3c47 100644 --- a/helm/wts/templates/wts-oidc.yaml +++ b/helm/wts/templates/wts-oidc.yaml @@ -24,7 +24,7 @@ spec: containers: - name: fence-client # TODO: Make this configurable - image: "quay.io/cdis/fence:feat_dbenvvar" + image: "quay.io/cdis/fence:master" imagePullPolicy: {{ .Values.image.pullPolicy }} # TODO: ADD RESOURCES # resources: @@ -100,10 +100,11 @@ spec: args: - "-c" - | + echo "waiting for /shared/client_id" while [ ! -e /shared/client_id ] do - echo "waiting for /shared/client_id" - sleep 30 + echo "..." + sleep 5 done echo "Updating k8s secret wts-oidc-client" CLIENT_ID=$(cat /shared/client_id | base64) diff --git a/helm/wts/values.yaml b/helm/wts/values.yaml index 1caa019a..9386691f 100644 --- a/helm/wts/values.yaml +++ b/helm/wts/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -# -- (map) Global configuration options. +# Global configuration global: # -- (map) AWS configuration aws: @@ -14,10 +14,12 @@ global: awsSecretAccessKey: # -- (bool) Whether the deployment is for development purposes. dev: true - # -- (map) Postgres database configuration. + postgres: # -- (bool) Whether the database should be created. dbCreate: true + # -- (string) Name of external secret. Disabled if empty + externalSecret: "" # -- (map) Master credentials to postgres. This is going to be the default postgres server being used for each service, unless each service specifies their own postgres master: # -- (string) hostname of postgres server @@ -42,20 +44,33 @@ global: kubeBucket: kube-gen3 # -- (string) S3 bucket name for log files. logsBucket: logs-gen3 - # -- (bool) Whether to sync data from dbGaP. - syncFromDbgap: false - # -- (string) Path to the user.yaml file in S3. - userYamlS3Path: s3://cdis-gen3-users/test/user.yaml # -- (bool) Whether public datasets are enabled. publicDataSets: true # -- (string) Access level for tiers. acceptable values for `tier_access_level` are: `libre`, `regular` and `private`. If omitted, by default common will be treated as `private` tierAccessLevel: libre - # -- (bool) Whether network policies are enabled. - netPolicy: true + # -- (map) Controls network policy settings + netPolicy: + enabled: false # -- (int) Number of dispatcher jobs. - dispatcherJobNum: 10 - # -- (bool) Whether Datadog is enabled. - ddEnabled: false + dispatcherJobNum: "10" + # -- (bool) If the service will be deployed with a Pod Disruption Budget. Note- you need to have more than 2 replicas for the pdb to be deployed. + pdb: false + # -- (int) The minimum amount of pods that are available at all times if the PDB is deployed. + minAvialable: 1 + # -- (map) External Secrets settings. + externalSecrets: + # -- (bool) Will use ExternalSecret resources to pull secrets from Secrets Manager instead of creating them locally. Be cautious as this will override any wts secrets you have deployed. + deploy: false + # -- (string) Will deploy a separate External Secret Store for this service. + separateSecretStore: false + +# -- (bool) Whether Metrics are enabled. +metricsEnabled: false + +# -- (map) External Secrets settings. +externalSecrets: + # -- (string) Will override the name of the aws secrets manager secret. Default is "Values.global.environment-.Chart.Name-creds" + dbcreds: # -- (map) Postgres database configuration. If db does not exist in postgres cluster and dbCreate is set ot true then these databases will be created for you postgres: @@ -121,27 +136,17 @@ serviceAccount: # If not set and create is true, a name is generated using the fullname template name: "" -# -- (map) Labels to add to the pod. -podLabels: - # -- (string) Release name. - release: production - # -- (string) Grants ingress from the revproxy service for pods labeled with public=yes - public: "yes" - # -- (string) Grants egress from pods labeled with netnolimit=yes to any IP address. Use explicit proxy and AWS APIs - netnolimit: "yes" - # -- (string) Grants ingress from pods in usercode namespaces for gen3 pods labeled with userhelper=yes - userhelper: "yes" - tags.datadoghq.com/service: "token-service" - # -- (map) Annotations to add to the pod. podAnnotations: {} # -- (map) Security context for the pod -podSecurityContext: {} +podSecurityContext: + {} # fsGroup: 2000 # -- (map) Security context for the containers in the pod -securityContext: {} +securityContext: + {} # capabilities: # drop: # - ALL @@ -163,9 +168,9 @@ resources: # -- (map) The amount of resources that the container requests requests: # -- (string) The amount of CPU requested - cpu: 0.1 + cpu: 0.2 # -- (string) The amount of memory requested - memory: 12Mi + memory: 120Mi # -- (map) The maximum amount of resources that the container is allowed to use limits: # -- (string) The maximum amount of CPU the container can use @@ -196,11 +201,13 @@ affinity: {} # -- (string) Name of the role to be used for the role binding. roleName: workspace-token-service -# -- (string) Release name. -release: production -# -- (map) Values for wts secret. +# -- (map) Values for wts secret and keys for External Secrets. secrets: + # -- (str) AWS access key ID. Overrides global key. + awsAccessKeyId: + # -- (str) AWS secret access key ID. Overrides global key. + awsSecretAccessKey: external_oidc: # - base_url: # oidc_client_id: @@ -229,3 +236,15 @@ secrets: # "db_passwurd": "WTS_DB_PWD.REPLACE", # "db_database": "wts_default" # } + +# Values to determine the labels that are used for the deployment, pod, etc. +# -- (string) Valid options are "production" or "dev". If invalid option is set- the value will default to "dev". +release: "production" +# -- (string) Valid options are "true" or "false". If invalid option is set- the value will default to "false". +criticalService: "true" +# -- (string) Label to help organize pods and their use. Any value is valid, but use "_" or "-" to divide words. +partOf: "Authentication" +# -- (map) Will completely override the selectorLabels defined in the common chart's _label_setup.tpl +selectorLabels: +# -- (map) Will completely override the commonLabels defined in the common chart's _label_setup.tpl +commonLabels: diff --git a/sample-values/fence-config.yaml b/sample-values/fence-config.yaml deleted file mode 100644 index 11873115..00000000 --- a/sample-values/fence-config.yaml +++ /dev/null @@ -1,885 +0,0 @@ -#### FENCE CONFIG #### -fence: - FENCE_CONFIG: - APP_NAME: 'Gen3 Data Commons' - - # A URL-safe base64-encoded 32-byte key for encrypting keys in db - # in python you can use the following script to generate one: - # import base64 - # import os - # key = base64.urlsafe_b64encode(os.urandom(32)) - # print(key) - ENCRYPTION_KEY: REPLACEME - - # ////////////////////////////////////////////////////////////////////////////////////// - # DEBUG & SECURITY SETTINGS - # - Modify based on whether you're in a dev environment or in production - # ////////////////////////////////////////////////////////////////////////////////////// - # flask's debug setting - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - DEBUG: false - # if true, will automatically login a user with username "test" - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - MOCK_AUTH: false - # if true, will fake a successful login response from Google in /login/google - # NOTE: this will also modify the behavior of /link/google endpoints - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - # will login as the username set in cookie DEV_LOGIN_COOKIE_NAME - MOCK_GOOGLE_AUTH: false - DEV_LOGIN_COOKIE_NAME: "dev_login" - # if true, will ignore anything configured in STORAGE_CREDENTIALS - MOCK_STORAGE: false - # allow OIDC traffic on http for development. By default it requires https. - # - # WARNING: ONLY set to true when fence will be deployed in such a way that it will - # ONLY receive traffic from internal clients and can safely use HTTP. - AUTHLIB_INSECURE_TRANSPORT: true - # enable Prometheus Metrics for observability purposes - # - # WARNING: Any counters, gauges, histograms, etc. should be carefully - # reviewed to make sure its labels do not contain any PII / PHI - ENABLE_PROMETHEUS_METRICS: false - - # set if you want browsers to only send cookies with requests over HTTPS - SESSION_COOKIE_SECURE: true - - ENABLE_CSRF_PROTECTION: true - - # Signing key for WTForms to sign CSRF tokens with - WTF_CSRF_SECRET_KEY: '{{ENCRYPTION_KEY}}' - - # fence (at the moment) attempts a migration on startup. setting this to false will disable that - # WARNING: ONLY set to false if you do NOT want to automatically migrate your database. - # You should be careful about incompatible versions of your db schema with what - # fence expects. In other words, things could be broken if you update to a later - # fence that expects a schema your database isn't migrated to. - # NOTE: We are working to improve the migration process in the near future - ENABLE_DB_MIGRATION: true - - # ////////////////////////////////////////////////////////////////////////////////////// - # OPEN ID CONNECT (OIDC) - # - Fully configure at least one client so login works - # - WARNING: Be careful changing the *_ALLOWED_SCOPES as you can break basic - # and optional functionality - # ////////////////////////////////////////////////////////////////////////////////////// - OPENID_CONNECT: - # any OIDC IDP that does not differ from the generic implementation can be - # configured without code changes - generic_oidc_idp: # choose a unique ID and replace this key - name: 'some_idp' # optional; display name for this IDP - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/some_idp/login' # replace IDP name - # use `discovery` to configure IDPs that do not expose a discovery - # endpoint. One of `discovery_url` or `discovery` should be configured - discovery_url: 'https://server.com/.well-known/openid-configuration' - discovery: - authorization_endpoint: '' - token_endpoint: '' - jwks_uri: '' - user_id_field: '' # optional (default "sub"); claims field to get the user_id from - email_field: '' # optional (default "email"); claims field to get the user email from - scope: '' # optional (default "openid") - # These Google values must be obtained from Google's Cloud Console - # Follow: https://developers.google.com/identity/protocols/OpenIDConnect - # - # You'll need to obtain a Client ID and Client Secret. Set the redirect URIs - # in Google to be '{{BASE_URL}}/login/google/login', but expand BASE_URL to - # whatever you set it to above. - google: - discovery_url: 'https://accounts.google.com/.well-known/openid-configuration' - client_id: '' - client_secret: '' - # this is be the allowed redirect back to fence, should not need to change - redirect_url: '{{BASE_URL}}/login/google/login/' - scope: 'openid email' - # if mock is true, will fake a successful login response from Google in /login/google - # NOTE: this will also modify the behavior of /link/google endpoints - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - # will login as the username set in cookie DEV_LOGIN_COOKIE_NAME or default provided - # here - mock: '{{MOCK_GOOGLE_AUTH}}' # for backwards compatibility with older cfg files - mock_default_user: 'test@example.com' - # Support for multi-tenant fence (another fence is this fence's IDP) - # If this fence instance is a client of another fence, fill this cfg out. - # REMOVE if not needed - fence: - # this api_base_url should be the root url for the OTHER fence - # something like: https://example.com - api_base_url: '' - # this client_id and client_secret should be obtained by registering THIS fence as - # a new client of the OTHER fence - client_id: '' - client_secret: '' - client_kwargs: - # openid is required to use OIDC flow - scope: 'openid' - # callback after logging in through the other fence - redirect_uri: '{{BASE_URL}}/login/fence/login' - # The next 3 should not need to be changed if the provider is following - # Oauth2 endpoint naming conventions - authorize_url: '{{api_base_url}}/oauth2/authorize' - access_token_url: '{{api_base_url}}/oauth2/token' - refresh_token_url: '{{api_base_url}}/oauth2/token' - # Custom name to display for consent screens. If not provided, will use `fence`. - # If the other fence is using NIH Login, you should make name: `NIH Login` - name: '' - # if mock is true, will fake a successful login response for login - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - mock: false - mock_default_user: 'test@example.com' - # this is needed to enable InCommon login, if some LOGIN_OPTIONS are configured with idp=fence and a list of shib_idps: - shibboleth_discovery_url: 'https://login.bionimbus.org/Shibboleth.sso/DiscoFeed' - # you can setup up an orcid client here: https://orcid.org/developer-tools - orcid: - discovery_url: 'https://orcid.org/.well-known/openid-configuration' - client_id: '' - client_secret: '' - # make sure you put the FULL url for this deployment in the allowed redirects in - # ORCID.org. DO NOT include {{BASE_URL}} at ORCID.org, you need to actually put the - # full url - redirect_url: '{{BASE_URL}}/login/orcid/login/' - scope: 'openid' - # if mock is true, will fake a successful login response for login - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - mock: false - mock_default_user: '0000-0002-2601-8132' - ras: - discovery_url: 'https://sts.nih.gov/.well-known/openid-configuration' - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/ras/callback' - scope: 'openid email profile ga4gh_passport_v1' - # if mock is true, will fake a successful login response for login - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - mock: false - mock_default_user: 'test@example.com' - # Create a client in Azure here: - # https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview - # Currently supports organizational account only, so when registering a new App in - # Azure, make sure to select the `Accounts in any organizational directory` for - # supported account types. - microsoft: - discovery_url: 'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration' - # after registering a new appl, client_id can be found as - # "APPLICATION (CLIENT) ID" in Microsoft Azure - client_id: '' - # You have a generate a secret in Azure for this app, there should be a - # "Certificates & secrets" section where you can create a "New client secret" - client_secret: '' - # make sure you put the FULL url for this deployment in the allowed redirects in - # your app in Azure. DO NOT include {{BASE_URL}} in Azure, you need to actually put the - # full url - redirect_url: '{{BASE_URL}}/login/microsoft/login/' - scope: 'openid email' - # if mock is true, will fake a successful login response for login - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - mock: false - mock_default_user: 'test@example.com' - # For information on configuring an Okta tenant as an OIDC IdP refer to Okta documentation at: - # https://developer.okta.com/docs/reference/api/oidc/#2-okta-as-the-identity-platform-for-your-app-or-api - okta: - discovery_url: '' - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/okta/login/' - scope: 'openid email' - cognito: - # You must create a user pool in order to have a discovery url - discovery_url: 'https://cognito-idp.{REGION}.amazonaws.com/{USER-POOL-ID}/.well-known/openid-configuration' - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/cognito/login/' - scope: 'openid email' - # In the case where Cognito is being used solely as an intermediary to a single IdP, - # and that IdP is a SAML IdP with no 'email_verified' outgoing claim, but it is safe - # to assume all emails from this SAML IdP are in fact verified, we may set this to True - # assume_emails_verified: False - # CILogon subscribers can create and manage OIDC clients using COmanage Registry. - # Free tier users may request OIDC clients at https://cilogon.org/oauth2/register - cilogon: - discovery_url: 'https://cilogon.org/.well-known/openid-configuration' - client_id: '' - client_secret: '' - # When registering the Callback URLs for your CILogon OIDC client be - # sure to include the FULL url for this deployment, including the https:// scheme - # and server FQDN. - redirect_url: '{{BASE_URL}}/login/cilogon/login/' - scope: 'openid email profile' - # if mock is true, will fake a successful login response for login - # WARNING: DO NOT ENABLE IN PRODUCTION (for testing purposes only) - mock: false - mock_default_user: 'http://cilogon.org/serverT/users/64703' - synapse: - discovery_url: '' - client_id: '' - client_secret: '' - redirect_url: '' - scope: 'openid' - shibboleth: - client_id: '' - client_secret: '' - redirect_url: '{{BASE_URL}}/login/shib/login' - - # these are the *possible* scopes a client can be given, NOT scopes that are - # given to all clients. You can be more restrictive during client creation - CLIENT_ALLOWED_SCOPES: - - "openid" - - "user" - - "data" - - "google_credentials" - - "google_service_account" - - "google_link" - - "ga4gh_passport_v1" - - # these are the scopes that CAN be included in a user's own access_token - USER_ALLOWED_SCOPES: - - "fence" - - "openid" - - "user" - - "data" - - "admin" - - "google_credentials" - - "google_service_account" - - "google_link" - - "ga4gh_passport_v1" - - # these are the scopes that a browser session can create for a user (very - # similar to USER_ALLOWED_SCOPES, as the session will actually create access_tokens - # for an actively logged in user) - SESSION_ALLOWED_SCOPES: - - "openid" - - "user" - - "credentials" - - "data" - - "admin" - - "google_credentials" - - "google_service_account" - - "google_link" - - "ga4gh_passport_v1" - - # ////////////////////////////////////////////////////////////////////////////////////// - # LOGIN - # - Modify based on which OIDC provider(s) you configured above - # - NOTE: You can have multiple IDPs for users to login with, but one has to be set - # as the default - # ////////////////////////////////////////////////////////////////////////////////////// - - # List of enabled login options (used by data-portal to display login buttons). - # Each option must be configured with a "name" and an "idp". - # - "idp" must be a configured provider in OPENID_CONNECT section. - # Multiple options can be configured with the same idp. - # - if provider_id is "fence", "fence_idp" can be any of the providers - # supported by the other Fence. If not specified, will default to NIH login. - # - if provider_id is "fence" and fence_idp is "shibboleth", a list of - # "shib_idps" can be configured for InCommon login. If not specified, will - # default to NIH login. - # - Optional parameters: "desc" (description) and "secondary" (boolean - can - # be used by the frontend to display secondary buttons differently). - LOGIN_OPTIONS: - - name: 'Login from Google' - desc: 'description' - idp: google - # secondary: True - # - name: 'ORCID Login' - # idp: orcid - # - name: 'Microsoft Login' - # idp: microsoft - # - name: 'Okta Login'/sqz-krfi-ynw - # idp: okta - # # Cognito login: You may want to edit the name to reflect Cognito's IdP, - # # especially if Cognito is only using one IdP - # - name: 'Login from Cognito' - # desc: 'Amazon Cognito login' - # idp: cognito - # - name: 'Login from RAS' - # idp: ras - # - name: 'NIH Login' - # idp: fence - # fence_idp: shibboleth - # - name: 'ORCID Login through other Fence' - # idp: fence - # fence_idp: orcid - # - name: 'CILogon Login' - # idp: cilogon - # - name: 'InCommon Login' - # idp: fence - # fence_idp: shibboleth - # # "shib_idps" can be '*' or a list of one or more entity IDs - # shib_idps: - # - urn:mace:incommon:nih.gov - # - urn:mace:incommon:uchicago.edu - # The following can be used for shibboleth login, simply uncomment. - # NOTE: Don't enable shibboleth if the deployment is not protected by - # shibboleth module, the shib module takes care of preventing header - # spoofing. - # - name: 'Shibboleth Login' - # idp: shibboleth - - # Default login provider: - # - must be configured in LOGIN_OPTIONS and OPENID_CONNECT - # - if several options in LOGIN_OPTIONS are defined for this IDP, will default - # to the first one. - DEFAULT_LOGIN_IDP: google - - # Default login URL: DEPRECATED and replaced by LOGIN_OPTIONS + DEFAULT_LOGIN_IDP configs - # - Google? Use: '{{BASE_URL}}/login/google' - # - Multi-tenant fence (e.g. another fence instance)? Use: '{{BASE_URL}}/login/fence' - # - Sibboleth? Use: '{{BASE_URL}}/login/shib' - DEFAULT_LOGIN_URL: '{{BASE_URL}}/login/google' - - # `LOGIN_REDIRECT_WHITELIST` is a list of extra whitelisted URLs which can be redirected - # to by the `/login/*` endpoints. Fence automatically populates this with the redirect - # URLs for any registered OAuth clients, and its own URL. When validating the redirects, - # fence chesk whether the domain for the redirect matches a domain in the whitelist (so - # only the domains for the additional desired redirects are necessary here). - LOGIN_REDIRECT_WHITELIST: [] - - ### DEPRECATED and replaced by OPENID_CONNECT + LOGIN_OPTIONS configs - ENABLED_IDENTITY_PROVIDERS: {} - - - # ////////////////////////////////////////////////////////////////////////////////////// - # LIBRARY CONFIGURATION (authlib & flask) - # - Already contains reasonable defaults - # ////////////////////////////////////////////////////////////////////////////////////// - # authlib-specific configs for OIDC flow and JWTs - # NOTE: the OAUTH2_JWT_KEY cfg gets set automatically by fence if keys are setup - # correctly - OAUTH2_JWT_ALG: 'RS256' - OAUTH2_JWT_ENABLED: true - OAUTH2_JWT_ISS: '{{BASE_URL}}' - OAUTH2_PROVIDER_ERROR_URI: '/api/oauth2/errors' - - # used for flask, "path mounted under by the application / web server" - # since we deploy as microservices, fence is typically under {{base}}/user - # this is also why our BASE_URL default ends in /user - APPLICATION_ROOT: '/user' - - - # ////////////////////////////////////////////////////////////////////////////////////// - # Tokens, Lifetimes, & Expirations - # - Already contains reasonable defaults - # ////////////////////////////////////////////////////////////////////////////////////// - # The name of the browser cookie in which the access token will be stored. - ACCESS_TOKEN_COOKIE_NAME: "access_token" - - # The name of the browser cookie in which the session token will be stored. - # Note that the session token also stores information for the - # ``flask.session`` in the ``context`` field of the token. - SESSION_COOKIE_NAME: "fence" - - # The domain of the browser cookie in which the session token will be stored. - # Leave unset (not empty string!) for normal single-site deployment. - SESSION_COOKIE_DOMAIN: - - OAUTH2_TOKEN_EXPIRES_IN: - "authorization_code": 1200 - "implicit": 1200 - - # The number of seconds after an access token is issued until it expires. - ACCESS_TOKEN_EXPIRES_IN: 1200 - - # The number of seconds after a refresh token is issued until it expires. - REFRESH_TOKEN_EXPIRES_IN: 2592000 - - # The number of seconds after which a browser session is considered stale. - SESSION_TIMEOUT: 1800 - - # The maximum session lifetime in seconds. - SESSION_LIFETIME: 28800 - - # The number of seconds the user's Google service account key used for - # url signing will last before being expired/rotated - # 30 days: 2592000 seconds - GOOGLE_SERVICE_ACCOUNT_KEY_FOR_URL_SIGNING_EXPIRES_IN: 2592000 - - # The number of seconds after a User's Google Service account is added to bucket - # access until it expires. - # 7 days: 604800 seconds - GOOGLE_USER_SERVICE_ACCOUNT_ACCESS_EXPIRES_IN: 604800 - - # The number of seconds after a User's Google account is added to bucket - # access until it expires. - GOOGLE_ACCOUNT_ACCESS_EXPIRES_IN: 86400 - - # The number of seconds after a pre-signed url is issued until it expires. - MAX_PRESIGNED_URL_TTL: 3600 - - # The number of seconds after an API KEY is issued until it expires. - MAX_API_KEY_TTL: 2592000 - - # The number of seconds after an access token is issued until it expires. - MAX_ACCESS_TOKEN_TTL: 3600 - - # TEMPORARY: The maximum number of projects allowed in token claims. - # This config var should be removed after sheepdog and peregrine support - # auth checks against Arborist, and no longer check the token. - TOKEN_PROJECTS_CUTOFF: 10 - - # If set to true, will generate an new access token each time when a browser session update happens - RENEW_ACCESS_TOKEN_BEFORE_EXPIRATION: false - - # The maximum lifetime of a Gen3 passport in seconds - GEN3_PASSPORT_EXPIRES_IN: 43200 - - ######################################################################################## - # OPTIONAL CONFIGURATIONS # - ######################################################################################## - - # For displaying a privacy policy to users, we can either link to the URL specified by - # PRIVACY_POLICY_URL, or default to the `static/privacy_policy.md` file in fence. - PRIVACY_POLICY_URL: null - - # ////////////////////////////////////////////////////////////////////////////////////// - # RELIABILITY OPTS - # ////////////////////////////////////////////////////////////////////////////////////// - # Configurations related to resiliency, fault-tolerance and availability - # This is the number of requests per second that the Nginx proxy will accept before reaching fence - # The value defined in fence-config-public.yaml takes precedence over this one - # In the absence of this OVERRIDE prefixed config, the legacy NGINX_RATE_LIMIT from the k8s deployment yaml is applied - OVERRIDE_NGINX_RATE_LIMIT: 18 - - # ////////////////////////////////////////////////////////////////////////////////////// - # SUPPORT INFO - # ////////////////////////////////////////////////////////////////////////////////////// - # If you want an email address to show up when an unhandled error occurs, provide one - # here. Something like: support@example.com - SUPPORT_EMAIL_FOR_ERRORS: null - - # ////////////////////////////////////////////////////////////////////////////////////// - # SHIBBOLETH - # - Support using `shibboleth` in LOGIN_OPTIONS - # - Contains defaults for using NIH's Login. - # ////////////////////////////////////////////////////////////////////////////////////// - # assumes shibboleth is deployed under {{BASE_URL}}/shibboleth - SHIBBOLETH_HEADER: 'persistent_id' - SSO_URL: 'https://auth.nih.gov/affwebservices/public/saml2sso?SPID={{BASE_URL}}/shibboleth&RelayState=' - ITRUST_GLOBAL_LOGOUT: 'https://auth.nih.gov/siteminderagent/smlogout.asp?mode=nih&AppReturnUrl=' - - # ////////////////////////////////////////////////////////////////////////////////////// - # dbGaP USER SYNCING SUPPORT - # - Support syncing authorization information from dbGaP - # ////////////////////////////////////////////////////////////////////////////////////// - # "dbGaP project serves as an access gateway for researchers seeking to gain - # access to genotype and phenotype data" - # - # User syncing and access can also be done throught a User Access file. See - # fence's README for more information - dbGaP: - - info: - host: '' - username: '' - password: '' - port: 22 - proxy: '' - proxy_user: '' - protocol: 'sftp' - decrypt_key: '' - # parse out the consent from the dbgap accession number such that something - # like "phs000123.v1.p1.c2" becomes "phs000123.c2". - # - # NOTE: when this is "false" the above would become "phs000123" - parse_consent_code: true - # A consent of "c999" can indicate access to that study's "exchange area data" - # and when a user has access to one study's exchange area data, they - # have access to the parent study's "common exchange area data" that is not study - # specific. The following config is whether or not to parse/handle "c999" codes - # for access to the common exchange area data - # - # NOTE: When enabled you MUST also provide a mapping to the - # `study_common_exchange_areas` from study -> parent common exchange area resource - enable_common_exchange_area_access: false - # The below configuration is a mapping from studies to their "common exchange area data" - # Fence project name a user gets access to when parsing c999 exchange area codes (and - # subsequently gives access to an Arborist resource representing this common area - # as well) - study_common_exchange_areas: - 'example': 'test_common_exchange_area' - # 'studyX': 'test_common_exchange_area' - # 'studyY': 'test_common_exchange_area' - # 'studyZ': 'test_common_exchange_area' - # A mapping from the dbgap study / Fence project to which authorization namespaces the - # actual data lives in. For example, `studyX` data may exist in multiple organizations, so - # we need to know how to map authorization to all orgs resources - study_to_resource_namespaces: - '_default': ['/'] - 'test_common_exchange_area': ['/dbgap/'] - # above are for default support and exchange area support - # below are further examples - # - # 'studyX': ['/orgA/', '/orgB/'] - # 'studyX.c2': ['/orgB/', '/orgC/'] - # 'studyZ': ['/orgD/'] - # Regex to match an assession number that has consent information in forms like: - # phs00301123.c999 - # phs000123.v3.p1.c3 - # phs000123.c3 - # phs00301123.v3.p4.c999 - # Will NOT MATCH forms like: phs000123 - # - # WARNING: Do not change this without consulting the code that uses it - DBGAP_ACCESSION_WITH_CONSENT_REGEX: '(?Pphs[0-9]+)(.(?Pv[0-9]+)){0,1}(.(?Pp[0-9]+)){0,1}.(?Pc[0-9]+)' - - # ////////////////////////////////////////////////////////////////////////////////////// - # STORAGE BACKENDS AND CREDENTIALS - # - Optional: Used for `/admin` & `/credentials` endpoints for user management. - # Also used during User Syncing process to automate managing Storage - # access for users. - # ////////////////////////////////////////////////////////////////////////////////////// - # When true, this modifies usersync (not fence service itself) such that when syncing user - # access to a Google storage backend happens in "bulk" by doing a diff *per google group* - # between what's in Google and what's expected. Then it adds, removes only as necessary. - # This is in contrast to the default logic which does blind updates per user and ignores - # 409s from Google. - # NOTE: This reduces the number of API calls to Google in the general case, but increases - # memory usages by usersync (as it has to track all the Google groups and user access) - GOOGLE_BULK_UPDATES: false - - # Configuration for various storage systems for the backend - # NOTE: Remove the {} and supply backends if needed. Example in comments below - STORAGE_CREDENTIALS: {} - # Google Cloud Storage backend - # - # 'google': - # backend: 'google' - # # this should be the project id where the Google Groups for data access are managed - # google_project_id: 'some-project-id-12378923' - - # Cleversafe data storage backend - # - # 'cleversafe-server-a': - # backend: 'cleversafe' - # aws_access_key_id: '' - # aws_secret_access_key: '' - # host: 'somemanager.osdc.io' - # public_host: 'someobjstore.example.com' - # port: 443 - # is_secure: true - # username: 'someone' - # password: 'somepass' - # is_mocked: true - - # ////////////////////////////////////////////////////////////////////////////////////// - # AWS BUCKETS AND CREDENTIALS - # - Support `/data` endpoints - # ////////////////////////////////////////////////////////////////////////////////////// - AWS_CREDENTIALS: {} - # NOTE: Remove the {} and supply creds if needed. Example in comments below - # 'CRED1': - # aws_access_key_id: '' - # aws_secret_access_key: '' - # 'CRED2': - # aws_access_key_id: '' - # aws_secret_access_key: '' - - # NOTE: the region is optonal for s3_buckets, however it should be specified to avoid a - # call to GetBucketLocation which you make lack the AWS ACLs for. - # public buckets do not need the region field. - # the cred values should be keys in section `AWS_CREDENTIALS`. - S3_BUCKETS: {} - # NOTE: Remove the {} and supply buckets if needed. Example in comments below - # bucket1: - # cred: 'CRED1' - # region: 'us-east-1' - # # optionally you can manually specify an s3-compliant endpoint for this bucket - # endpoint_url: 'https://cleversafe.example.com/' - # bucket2: - # cred: 'CRED2' - # region: 'us-east-1' - # bucket3: - # cred: '*' # public bucket - # bucket4: - # cred: 'CRED1' - # region: 'us-east-1' - # role-arn: 'arn:aws:iam::role1' - - # `DATA_UPLOAD_BUCKET` specifies an S3 bucket to which data files are uploaded, - # using the `/data/upload` endpoint. This must be one of the first keys under - # `S3_BUCKETS` (since these are the buckets fence has credentials for). - DATA_UPLOAD_BUCKET: 'bucket1' - - # ////////////////////////////////////////////////////////////////////////////////////// - # PROXY - # - Optional: If the api is behind firewall that needs to set http proxy - # ////////////////////////////////////////////////////////////////////////////////////// - # NOTE: leave as-is to not use proxy - # this is only used by the Google Oauth2Client at the moment if provided - HTTP_PROXY: - host: null - port: 3128 - - # ////////////////////////////////////////////////////////////////////////////////////// - # MICROSERVICE PATHS - # - Support `/data` endpoints & authz functionality - # ////////////////////////////////////////////////////////////////////////////////////// - # url where indexd microservice is running (for signed urls primarily) - # NOTE: Leaving as null will force fence to default to {{BASE_URL}}/index - # example value: 'https://example.com/index' - INDEXD: null - - # this is the username which fence uses to make authenticated requests to indexd - INDEXD_USERNAME: 'fence' - # this is the password which fence uses to make authenticated requests to indexd - INDEXD_PASSWORD: '' - - # ////////////////////////////////////////////////////////////////////////////////////// - # AZURE STORAGE BLOB CONFIGURATION - # - Support Azure Blob Data Access Methods - # ////////////////////////////////////////////////////////////////////////////////////// - - # https://docs.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&tabs=azure-portal#view-account-access-keys - # AZ_BLOB_CREDENTIALS: 'fake connection string' - AZ_BLOB_CREDENTIALS: - - # AZ_BLOB_CONTAINER_URL: 'https://storageaccount.blob.core.windows.net/container/' - # this is the container used for uploading, and should match the storage account - # used in the connection string for AZ_BLOB_CREDENTIALS - AZ_BLOB_CONTAINER_URL: 'https://myfakeblob.blob.core.windows.net/my-fake-container/' - - # url where authz microservice is running - ARBORIST: null - - # url where the audit-service is running - AUDIT_SERVICE: 'http://audit-service' - ENABLE_AUDIT_LOGS: - presigned_url: false - login: false - # `PUSH_AUDIT_LOGS_CONFIG.type` is one of: [api, aws_sqs]. - # - if type == api: logs are created by hitting the log creation endpoint. - # - if type == aws_sqs: logs are pushed to an SQS and `aws_sqs_config` fields - # `sqs_url` and `region` are required. Field `aws_cred` is optional and it - # should be a key in section `AWS_CREDENTIALS`. - PUSH_AUDIT_LOGS_CONFIG: - type: aws_sqs - aws_sqs_config: - sqs_url: - region: - aws_cred: - - # ////////////////////////////////////////////////////////////////////////////////////// - # CLOUD API LIBRARY (CIRRUS) AND GOOGLE CONFIGURATION - # - Support Google Data Access Methods - # ////////////////////////////////////////////////////////////////////////////////////// - # Setting this up allows fence to create buckets, manage Google groups, etc. - # See directions here for setting up cirrus: https://github.com/uc-cdis/cirrus - CIRRUS_CFG: - GOOGLE_API_KEY: '' - GOOGLE_PROJECT_ID: '' - GOOGLE_APPLICATION_CREDENTIALS: '' - GOOGLE_STORAGE_CREDS: '' - GOOGLE_ADMIN_EMAIL: '' - GOOGLE_IDENTITY_DOMAIN: '' - GOOGLE_CLOUD_IDENTITY_ADMIN_EMAIL: '' - - # Prefix to namespace Google Groups on a single Cloud Identity (see cirrus - # setup for more info on Cloud Identity) - # - # NOTE: Make this short! Less than 8 characters if possible. Google has - # length restrictions on group names. - GOOGLE_GROUP_PREFIX: '' - - # Prefix to namespace Google Service Accounts in a single Google Cloud Platform Project. - # This is primarily to support multiple instances of fence references the same Google - # project. If that is not something you need to support, then you can leave this blank. - # - # NOTE: Make this short! Less than 8 characters if possible. Google has - # length restrictions on service account names. - GOOGLE_SERVICE_ACCOUNT_PREFIX: '' - - # A Google Project identitifier representing the default project to bill to for - # accessing Google Requester Pays buckets (for signed urls and/or temporary service account - # credentials). If this is provided and the API call for - # Google access does not include a `userProject`, this will be used instead. - # - # WARNING: Setting this WITHOUT setting "ENABLE_AUTOMATIC_BILLING_*" to `true` below, - # means that clients and end-users will be responsible for making sure that - # the service account used in either of these methods actually has billing - # permission in the specified project. - BILLING_PROJECT_FOR_SIGNED_URLS: - BILLING_PROJECT_FOR_SA_CREDS: - - # Setting this to `true` will make Fence automatically attempt to create a Custom Role - # in the billing project and give the necessary Google Service Account that role - # (which will allow it to bill to the project). - # - # NOTE: The Fence SA will need the necessary permissions in the specified project to - # both create a custom role and update the Project's IAM Policy to include the - # necessary SA. At the time of writing, there are pre-defined roles in Google's - # IAM that provide the necessary permissions. Those are "Project IAM Admin" and - # "Role Administrator" - # - # NOTE2: It may be possible to further restrict the permissions in the future to - # be more fine-grained. - # - ENABLE_AUTOMATIC_BILLING_PERMISSION_SIGNED_URLS: false - ENABLE_AUTOMATIC_BILLING_PERMISSION_SA_CREDS: false - - # ////////////////////////////////////////////////////////////////////////////////////// - # EMAIL - # - Support for sending emails from fence. Used for user certificates - # and `/google/service_accounts` endpoints - # ////////////////////////////////////////////////////////////////////////////////////// - # Gun Mail Service (for sending emails from fence) - # - # NOTE: Example in comments below - GUN_MAIL: - 'datacommons.io': - smtp_hostname: 'smtp.mailgun.org' - api_key: '' - default_login: 'postmaster@mailgun.example.com' - api_url: 'https://api.mailgun.net/v3/mailgun.example.com' - smtp_password: '' - - # For emails regarding users certificates - EMAIL_SERVER: 'localhost' - SEND_FROM: 'example@gmail.com' - SEND_TO: 'example@gmail.com' - - # ////////////////////////////////////////////////////////////////////////////////////// - # DATA ACCESS: GOOGLE LINKING & SERVICE ACCOUNT REGISTRATION - # - Support `/google/service_accounts` endpoints - # ////////////////////////////////////////////////////////////////////////////////////// - # whether or not to allow access to the /link/google endpoints - ALLOW_GOOGLE_LINKING: true - - # A Google Project with controlled data access will be determined INVALID if - # if it has a parent organization UNLESS that parent organization's ID is in this - # whitelist. - # - # NOTE: Remove the [] and Google Organization IDs if needed. Example in comments below - WHITE_LISTED_GOOGLE_PARENT_ORGS: [] - # - '12345678910' - - # A Google Project with Google Service Accounts determined INVALID will result in the - # the entire project being invalid UNLESS that service accounts's email is in this - # whitelist. - # - # NOTE: Remove the [] and service account emails if needed. Example in comments below - WHITE_LISTED_SERVICE_ACCOUNT_EMAILS: [] - # - 'example@developer.gserviceaccount.com' - # - 'example@test.iam.gserviceaccount.com' - - # when service accounts or google projects are determined invalid, an email is sent - # to the project owners. These settings are for that email - REMOVE_SERVICE_ACCOUNT_EMAIL_NOTIFICATION: - enable: false - # this domain MUST exist in GUN_MAIL config - domain: 'example.com' - from: 'do-not-reply@example.com' - subject: 'User service account removal notification' - # the {} gets replaced dynamically in the Python code to be the Project ID - content: > - Service accounts were removed from access control data because some users or - service accounts of GCP Project {} are not authorized to access the data sets - associated to the service accounts, or do not adhere to the security policies. - # this admin email will be included as a recipient to *any* email to anyone about - # service account removal. - # - # WARNING: This is NOT a bcc so the email is visible to the end-user - admin: - - 'admin@example.edu' - - PROBLEM_USER_EMAIL_NOTIFICATION: - # this domain MUST exist in GUN_MAIL config - domain: 'example.com' - from: 'do-not-reply@example.com' - subject: 'Account access error notification' - # the {} gets replaced dynamically in the Python code to be the Project ID - content: > - The Data Commons Framework utilizes dbGaP for data access authorization. - Another member of a Google project you belong to ({}) is attempting to - register a service account to the following additional datasets ({}). - Please contact dbGaP to request access. - # this admin email will be included as a recipient to *any* email to anyone about - # service account removal. - # - # WARNING: This is NOT a bcc so the email is visible to the end-user - admin: - - 'admin@example.edu' - - # Service account email domains that represent a service account that Google owns. - # These are usually created when a sepcific GCP service is enabled. - # This is used for Service Account Validation for Data Access. - GOOGLE_MANAGED_SERVICE_ACCOUNT_DOMAINS: - - 'dataflow-service-producer-prod.iam.gserviceaccount.com' - - 'cloudbuild.gserviceaccount.com' - - 'cloud-ml.google.com.iam.gserviceaccount.com' - - 'container-engine-robot.iam.gserviceaccount.com' - - 'dataflow-service-producer-prod.iam.gserviceaccount.com' - - 'sourcerepo-service-accounts.iam.gserviceaccount.com' - - 'dataproc-accounts.iam.gserviceaccount.com' - - 'gae-api-prod.google.com.iam.gserviceaccount.com' - - 'genomics-api.google.com.iam.gserviceaccount.com' - - 'containerregistry.iam.gserviceaccount.com' - - 'container-analysis.iam.gserviceaccount.com' - - 'cloudservices.gserviceaccount.com' - - 'stackdriver-service.iam.gserviceaccount.com' - - 'appspot.gserviceaccount.com' - - 'partnercontent.gserviceaccount.com' - - 'trifacta-gcloud-prod.iam.gserviceaccount.com' - - 'gcf-admin-robot.iam.gserviceaccount.com' - - 'compute-system.iam.gserviceaccount.com' - - 'gcp-sa-websecurityscanner.iam.gserviceaccount.com' - - 'storage-transfer-service.iam.gserviceaccount.com' - - 'firebase-sa-management.iam.gserviceaccount.com' - - 'firebase-rules.iam.gserviceaccount.com' - - 'gcp-sa-cloudbuild.iam.gserviceaccount.com' - - 'gcp-sa-automl.iam.gserviceaccount.com' - - 'gcp-sa-datalabeling.iam.gserviceaccount.com' - - 'gcp-sa-cloudscheduler.iam.gserviceaccount.com' - - # The types of service accounts that are allowed to be registered at - # /google/service_accounts endpoints - ALLOWED_USER_SERVICE_ACCOUNT_DOMAINS: - # compute engine default service account - - 'developer.gserviceaccount.com' - # app engine default service account - - 'appspot.gserviceaccount.com' - # user-managed service account - - 'iam.gserviceaccount.com' - - # Synapse integration and DREAM challenge mapping. Team is from Synapse, and group is - # providing the actual permission in Arborist. User will be added to the group for TTL - # seconds if the team matches. - DREAM_CHALLENGE_TEAM: 'DREAM' - DREAM_CHALLENGE_GROUP: 'DREAM' - SYNAPSE_URI: 'https://repo-prod.prod.sagebase.org/auth/v1' - SYNAPSE_JWKS_URI: - # deprecated, use the discovery_url in the OPENID_CONNECT block for the synapse client - SYNAPSE_DISCOVERY_URL: - SYNAPSE_AUTHZ_TTL: 86400 - - # Role caching for generating presigned urls if max role session increase is true - # then we can increase the amount of time that a session is valid for - MAX_ROLE_SESSION_INCREASE: false - ASSUME_ROLE_CACHE_SECONDS: 1800 - - # Optional user registration feature: Ask users to register (provide firstname/lastname/org/email) on login. - # If user registers, add them to configured Arborist group; idea is that the Arborist group - # will have access to download data. - REGISTER_USERS_ON: false - REGISTERED_USERS_GROUP: '' - # RAS refresh_tokens expire in 15 days - RAS_REFRESH_EXPIRATION: 1296000 - # List of JWT issuers from which Fence will accept GA4GH visas - GA4GH_VISA_ISSUER_ALLOWLIST: - - '{{BASE_URL}}' - - 'https://sts.nih.gov' - - 'https://stsstg.nih.gov' - # Number of projects that can be registered to a Google Service Accont - SERVICE_ACCOUNT_LIMIT: 6 - - # Global sync visas during login - # None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint - # True: Parse for all clients i.e. a fence client will always sync their visas during login - # False: Parse for no clients i.e. a fence client will not be able to sync visas during login even with parse_visas param - GLOBAL_PARSE_VISAS_ON_LOGIN: - # Settings for usersync with visas - USERSYNC: - sync_from_visas: false - # fallback to dbgap sftp when there are no valid visas for a user i.e. if they're expired or if they're malformed - fallback_to_dbgap_sftp: false - visa_types: - ras: ["https://ras.nih.gov/visas/v1", "https://ras.nih.gov/visas/v1.1"] - RAS_USERINFO_ENDPOINT: '/openid/connect/v1.1/userinfo' diff --git a/sample-values/user.yaml b/sample-values/user.yaml deleted file mode 100644 index 42a25552..00000000 --- a/sample-values/user.yaml +++ /dev/null @@ -1,92 +0,0 @@ -fence: - USER_YAML: | - authz: - # policies automatically given to anyone, even if they are not authenticated - anonymous_policies: - - open_data_reader - - # policies automatically given to authenticated users (in addition to their other policies) - all_users_policies: [] - - # each group can contain multiple policies and multiple users - groups: - - name: program1_readers - policies: - - program1_reader - users: - - username1@domain.com - - # resource tree - resources: - - name: open - - name: programs - subresources: - - name: program1 - - # each policy can contain multiple roles and multiple resources - policies: - - id: open_data_reader - role_ids: - - reader - - storage_reader - resource_paths: - - /open - - id: program1_reader - description: Read access to program1 - role_ids: - - reader - - storage_reader - resource_paths: - - /programs/program1 - - id: program1_indexd_admin - description: Admin access to program1 - role_ids: - - indexd_admin - resource_paths: - - /programs/program1 - - # currently existing methods are `read`, `create`, `update`, - # `delete`, `read-storage` and `write-storage` - roles: - - id: reader - permissions: - - id: reader - action: - method: read - service: '*' - - id: storage_reader - permissions: - - id: storage_reader - action: - method: read-storage - service: '*' - - id: creator - permissions: - - id: creator - action: - method: create - service: '*' - - id: indexd_admin - permissions: - - id: indexd_admin - action: - method: '*' - service: indexd - - # OIDC clients - clients: - client1: - policies: - - open_data_reader - - # all users must be defined here, even if they are not granted - # any individual permissions outside of the groups they are in. - # additional arbitrary information can be added in `tags`. - users: - username1@domain.com: {} - username2: - tags: - name: John Doe - email: johndoe@domain.com - policies: - - program1_reader diff --git a/sample-values/values_aws_dev.yaml b/sample-values/values_aws_dev.yaml deleted file mode 100644 index 9907dd06..00000000 --- a/sample-values/values_aws_dev.yaml +++ /dev/null @@ -1,69 +0,0 @@ -global: - environment: devplanetv1 - aws: - enabled: true - hostname: qureshi.planx-pla.net - revproxyArn: arn:aws:acm:us-east-1:707767160287:certificate/520ede2f-fc82-4bb9-af96-4b4af7deabbd -fence: - FENCE_CONFIG: - OPENID_CONNECT: - google: - client_id: "" - client_secret: "" - - -secrets: - awsAccessKeyId: - awsSecretAccessKey: - -hatchery: - image: - tag: feat_localdev - hatchery: - containers: - - target-port: 8888 - cpu-limit: '0.5' - memory-limit: 1Gi - name: "(Tutorials) Example Analysis Jupyter Lab Notebooks" - image: quay.io/cdis/heal-notebooks:combined_tutorials__latest - env: - FRAME_ANCESTORS: https://{{ .Values.global.hostname }} - args: - - "--NotebookApp.base_url=/lw-workspace/proxy/" - - "--NotebookApp.default_url=/lab" - - "--NotebookApp.password=''" - - "--NotebookApp.token=''" - - "--NotebookApp.shutdown_no_activity_timeout=5400" - - "--NotebookApp.quit_button=False" - command: - - start-notebook.sh - path-rewrite: "/lw-workspace/proxy/" - use-tls: 'false' - ready-probe: "/lw-workspace/proxy/" - lifecycle-post-start: - - "/bin/sh" - - "-c" - - export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; rm -rf /home/$IAM/pd/lost+found; - ln -s /data /home/$IAM/pd/; true - user-uid: 1000 - fs-gid: 100 - user-volume-location: "/home/jovyan/pd" - gen3-volume-location: "/home/jovyan/.gen3" - - -portal: - image: - tag: dev - -guppy: - enabled: true - dbRestore: true - -indexd: - dbRestore: true - -metadata: - dbRestore: true - -sheepdog: - dbRestore: true \ No newline at end of file diff --git a/sample-values/values_google_cloud_dev.yaml b/sample-values/values_google_cloud_dev.yaml deleted file mode 100644 index 4bb66aec..00000000 --- a/sample-values/values_google_cloud_dev.yaml +++ /dev/null @@ -1,9 +0,0 @@ -global: - dev: true - hostname: qureshi.planx-pla.net -fence: - FENCE_CONFIG: - OPENID_CONNECT: - google: - client_id: "" - client_secret: "" diff --git a/sample-values/values_local_dev.yaml b/sample-values/values_local_dev.yaml deleted file mode 100644 index d2c905d8..00000000 --- a/sample-values/values_local_dev.yaml +++ /dev/null @@ -1,10 +0,0 @@ -global: - dev: true - hostname: localhost -fence: - FENCE_CONFIG: - OPENID_CONNECT: - google: - client_id: "" - client_secret: "" - diff --git a/wip/acronymbot/templates/deployment.yaml b/wip/acronymbot/templates/deployment.yaml index 3fb80685..89092910 100644 --- a/wip/acronymbot/templates/deployment.yaml +++ b/wip/acronymbot/templates/deployment.yaml @@ -27,6 +27,7 @@ spec: labels: app: acronymbot {{- include "acronymbot.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: diff --git a/wip/cogwheel/templates/deployment.yaml b/wip/cogwheel/templates/deployment.yaml index 5a422dff..53a93deb 100644 --- a/wip/cogwheel/templates/deployment.yaml +++ b/wip/cogwheel/templates/deployment.yaml @@ -19,6 +19,7 @@ spec: {{- end }} labels: {{- include "cogwheel.selectorLabels" . | nindent 8 }} + {{- include "common.extraLabels" . | nindent 8 }} spec: volumes: {{- with .Values.volumes }}