diff --git a/.github/workflows/backfill-release-notes.yml b/.github/workflows/backfill-release-notes.yml new file mode 100644 index 0000000..a3c08e4 --- /dev/null +++ b/.github/workflows/backfill-release-notes.yml @@ -0,0 +1,78 @@ +name: Backfill Release Notes + +on: + workflow_dispatch: + +jobs: + backfill: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Backfill notes for helm-apps releases + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + + mapfile -t TAGS < <( + gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --paginate \ + --jq '.[] | select(.draft == false and .prerelease == false and (.tag_name | startswith("helm-apps-"))) | .tag_name' \ + | sort -V + ) + + if [ "${#TAGS[@]}" -eq 0 ]; then + echo "No helm-apps-* releases found." + exit 0 + fi + + for i in "${!TAGS[@]}"; do + tag="${TAGS[$i]}" + prev="" + if [ "$i" -gt 0 ]; then + prev="${TAGS[$((i-1))]}" + fi + + version="${tag#helm-apps-}" + echo "Updating notes for ${tag} (previous: ${prev:-none})" + + if [ -n "$prev" ]; then + generated_body="$(gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="${tag}" \ + -f previous_tag_name="${prev}" \ + --jq '.body')" + else + generated_body="$(gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="${tag}" \ + --jq '.body')" + fi + + changelog_body="$(awk -v ver="${version}" ' + $0 ~ "^## \\[" ver "\\]" {capture=1; next} + capture && $0 ~ "^## \\[" {exit} + capture {print} + ' CHANGELOG.md)" + + if [ -n "${changelog_body}" ]; then + body="${changelog_body}" + else + body="${generated_body}" + fi + + { + echo "## Helm Apps Library" + echo + echo "- Chart: \`helm-apps\`" + echo "- Version: \`${version}\`" + echo + echo "${body}" + } > /tmp/release_notes.md + + gh release edit "${tag}" \ + --title "helm-apps ${version}" \ + --notes-file /tmp/release_notes.md + done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f707537 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,322 @@ +name: CI + +on: + pull_request: + push: + branches-ignore: + - main + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set lib version + run: | + LIB_VERSION=$(sed -n '/version/{s/version: //;p;}' charts/helm-apps/Chart.yaml) + sed -i 's/_FLANT_APPS_LIBRARY_VERSION_/'${LIB_VERSION}'/' charts/helm-apps/templates/_apps-version.tpl + + - name: Install werf CLI + uses: werf/actions/install@v1.2 + with: + channel: ea + + - name: Install Dyff + run: | + curl --silent --location https://git.io/JYfAY | bash + + - name: Install kubeconform + run: | + set -euo pipefail + KUBECONFORM_VERSION=0.6.7 + curl -sSL -o /tmp/kubeconform.tar.gz "https://github.com/yannh/kubeconform/releases/download/v${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + tar -xzf /tmp/kubeconform.tar.gz -C /tmp kubeconform + sudo install -m 0755 /tmp/kubeconform /usr/local/bin/kubeconform + + - name: Update test chart dependencies + run: | + werf helm dependency update tests/.helm + + - name: Validate values schema + run: | + werf helm lint tests/.helm --values tests/.helm/values.yaml + + - name: Verify Kubernetes API compatibility + run: | + set -euo pipefail + + # New clusters: stable APIs must be rendered. + werf helm template tests tests/.helm \ + --set "global.env=prod" \ + --set "global._includes.apps-defaults.enabled=true" \ + --kube-version 1.29.0 > /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: policy/v1$' /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: batch/v1$' /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: autoscaling/v2$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: policy/v1beta1$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: batch/v1beta1$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: autoscaling/v2beta2$' /tmp/tests_k8s_129.yaml + kubeconform -strict -summary -ignore-missing-schemas -kubernetes-version 1.29.0 /tmp/tests_k8s_129.yaml + + # Legacy clusters: beta APIs remain supported. + werf helm template tests tests/.helm \ + --set "global.env=prod" \ + --set "global._includes.apps-defaults.enabled=true" \ + --kube-version 1.20.15 > /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: policy/v1beta1$' /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: batch/v1beta1$' /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: autoscaling/v2beta2$' /tmp/tests_k8s_120.yaml + ! grep -q '^apiVersion: autoscaling/v2$' /tmp/tests_k8s_120.yaml + kubeconform -strict -summary -ignore-missing-schemas -kubernetes-version 1.20.15 /tmp/tests_k8s_120.yaml + + - name: Render + run: | + set -e + source $(werf ci-env github --as-file) + cd tests && werf render --dev --set "global._includes.apps-defaults.enabled=true" --env=prod + + - name: Test render snapshot + run: | + set -e + source $(werf ci-env github --as-file) + cd tests && werf render --dev --set "global._includes.apps-defaults.enabled=true" --env=prod | sed '/werf.io\//d' > test_render_check.yaml + dyff between test_render.yaml test_render_check.yaml | tee /tmp/test_render_check + check_tests=$(sed 1,7d /tmp/test_render_check | wc -l) + if [ $check_tests -gt "7" ]; then exit 1; fi + + - name: Update contract chart dependencies + run: | + werf helm dependency update tests/contracts + + - name: Contract test for include merge behavior + run: | + set -euo pipefail + werf helm template contracts tests/contracts > /tmp/contracts_render.yaml + + # Include order override and local override precedence. + grep -q '"A": "2"' /tmp/contracts_render.yaml + grep -q '"LOCAL": "ok"' /tmp/contracts_render.yaml + grep -q '"key2": "local-value-2"' /tmp/contracts_render.yaml + + # Recursive merge and _include list concatenation from base-a/base-b. + grep -q '"key1": "value-1"' /tmp/contracts_render.yaml + grep -q '"fromBaseA": "A"' /tmp/contracts_render.yaml + grep -q '"fromBaseB": "B"' /tmp/contracts_render.yaml + + # Env-map behavior: + # production also resolves to override-default from higher-priority include. + grep -q '"ENV_SWITCH": "override-default"' /tmp/contracts_render.yaml + + # non-production resolves _default from higher-priority include. + werf helm template contracts tests/contracts --set global.env=dev > /tmp/contracts_render_dev.yaml + grep -q '"ENV_SWITCH": "override-default"' /tmp/contracts_render_dev.yaml + + # Generic passthrough fields support. + grep -q 'paused: true' /tmp/contracts_render.yaml + grep -q 'resizePolicy:' /tmp/contracts_render.yaml + grep -q 'podFailurePolicy:' /tmp/contracts_render.yaml + grep -q 'defaultBackend:' /tmp/contracts_render.yaml + grep -q 'volumeMode: Filesystem' /tmp/contracts_render.yaml + grep -q 'immutable: true' /tmp/contracts_render.yaml + grep -q 'stringData:' /tmp/contracts_render.yaml + grep -q '^apiVersion: networking.k8s.io/v1$' /tmp/contracts_render.yaml + grep -q '^kind: NetworkPolicy$' /tmp/contracts_render.yaml + grep -q '^apiVersion: cilium.io/v2$' /tmp/contracts_render.yaml + grep -q '^kind: CiliumNetworkPolicy$' /tmp/contracts_render.yaml + grep -q '^apiVersion: projectcalico.org/v3$' /tmp/contracts_render.yaml + grep -q 'selector: "app == '\''compat-service'\''"' /tmp/contracts_render.yaml + grep -q 'kubernetes.io/metadata.name: ingress-nginx' /tmp/contracts_render.yaml + grep -q 'port: 53' /tmp/contracts_render.yaml + grep -q 'name: "release-auto-app"' /tmp/contracts_render.yaml + grep -q 'image: alpine:3.19' /tmp/contracts_render.yaml + grep -q 'helm-apps/release: "production-v1"' /tmp/contracts_render.yaml + grep -q 'helm-apps/app-version: "3.19"' /tmp/contracts_render.yaml + grep -q 'name: "compat-route"' /tmp/contracts_render.yaml + grep -q 'host: "route.example.com"' /tmp/contracts_render.yaml + + # Strict mode (opt-in) for network policies: + # valid config should render, unknown keys should fail. + werf helm template contracts tests/contracts --set global.validation.strict=true > /tmp/contracts_render_strict.yaml + grep -Eq '"custom": ?"ok"|custom: ?"?ok"?' /tmp/contracts_render_strict.yaml + ! werf helm template contracts tests/contracts \ + --set global.validation.strict=true \ + --set apps-network-policies.compat-netpol.typoField=1 >/tmp/contracts_render_strict_fail.yaml + ! werf helm template contracts tests/contracts \ + --set global.validation.strict=true \ + --set apps-typo.bad.enabled=true >/tmp/contracts_render_strict_top_fail.yaml + + # Service spec compatibility by Kubernetes version. + werf helm template contracts tests/contracts --kube-version 1.29.0 > /tmp/contracts_render_129.yaml + grep -q 'loadBalancerClass: "internal-vip"' /tmp/contracts_render_129.yaml + grep -q 'internalTrafficPolicy: "Local"' /tmp/contracts_render_129.yaml + + werf helm template contracts tests/contracts --kube-version 1.20.15 > /tmp/contracts_render_120.yaml + ! grep -q 'loadBalancerClass:' /tmp/contracts_render_120.yaml + ! grep -q 'internalTrafficPolicy:' /tmp/contracts_render_120.yaml + grep -q 'ipFamilyPolicy: "SingleStack"' /tmp/contracts_render_120.yaml + grep -q 'allocateLoadBalancerNodePorts: true' /tmp/contracts_render_120.yaml + + werf helm template contracts tests/contracts --kube-version 1.19.16 > /tmp/contracts_render_119.yaml + ! grep -q 'loadBalancerClass:' /tmp/contracts_render_119.yaml + ! grep -q 'internalTrafficPolicy:' /tmp/contracts_render_119.yaml + ! grep -q 'ipFamilyPolicy:' /tmp/contracts_render_119.yaml + ! grep -q 'ipFamilies:' /tmp/contracts_render_119.yaml + ! grep -q 'allocateLoadBalancerNodePorts:' /tmp/contracts_render_119.yaml + + # Native YAML lists are forbidden in values (except _include/_include_files). + cat > /tmp/contracts_invalid_native_list.yaml <<'EOF' + apps-stateless: + compat-service: + service: + ports: + - name: http + port: 80 + targetPort: 8080 + EOF + ! werf helm template contracts tests/contracts \ + --values /tmp/contracts_invalid_native_list.yaml \ + >/tmp/contracts_invalid_native_list.out 2>/tmp/contracts_invalid_native_list.err + grep -q "list value is not allowed at Values.apps-stateless.compat-service.service.ports" /tmp/contracts_invalid_native_list.err + + - name: Contract test for internal-like release/deploy flow + run: | + set -euo pipefail + werf helm template contracts tests/contracts \ + --values tests/contracts/values.internal-compat.yaml > /tmp/contracts_internal_like.yaml + grep -q 'name: "compat-web"' /tmp/contracts_internal_like.yaml + grep -q 'image: alpine:1.2.3' /tmp/contracts_internal_like.yaml + grep -q 'helm-apps/release:' /tmp/contracts_internal_like.yaml + grep -q 'helm-apps/app-version: "1.2.3"' /tmp/contracts_internal_like.yaml + grep -q 'name: "compat-route"' /tmp/contracts_internal_like.yaml + grep -q 'host: "compat.example.com"' /tmp/contracts_internal_like.yaml + + kube-compatibility-matrix: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - kube_version: "1.19.16" + expect_policy: "policy/v1beta1" + expect_cronjob: "batch/v1beta1" + expect_hpa: "autoscaling/v2beta2" + - kube_version: "1.20.15" + expect_policy: "policy/v1beta1" + expect_cronjob: "batch/v1beta1" + expect_hpa: "autoscaling/v2beta2" + - kube_version: "1.23.17" + expect_policy: "policy/v1" + expect_cronjob: "batch/v1" + expect_hpa: "autoscaling/v2" + - kube_version: "1.29.0" + expect_policy: "policy/v1" + expect_cronjob: "batch/v1" + expect_hpa: "autoscaling/v2" + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set lib version + run: | + LIB_VERSION=$(sed -n '/version/{s/version: //;p;}' charts/helm-apps/Chart.yaml) + sed -i 's/_FLANT_APPS_LIBRARY_VERSION_/'${LIB_VERSION}'/' charts/helm-apps/templates/_apps-version.tpl + + - name: Install werf CLI + uses: werf/actions/install@v1.2 + with: + channel: ea + + - name: Install kubeconform + run: | + set -euo pipefail + KUBECONFORM_VERSION=0.6.7 + curl -sSL -o /tmp/kubeconform.tar.gz "https://github.com/yannh/kubeconform/releases/download/v${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + tar -xzf /tmp/kubeconform.tar.gz -C /tmp kubeconform + sudo install -m 0755 /tmp/kubeconform /usr/local/bin/kubeconform + + - name: Update chart dependencies + run: | + werf helm dependency update tests/.helm + werf helm dependency update tests/contracts + + - name: Render and verify APIs for Kubernetes ${{ matrix.kube_version }} + run: | + set -euo pipefail + + werf helm template tests tests/.helm \ + --set "global.env=prod" \ + --set "global._includes.apps-defaults.enabled=true" \ + --kube-version "${{ matrix.kube_version }}" > "/tmp/tests_k8s_${{ matrix.kube_version }}.yaml" + + grep -q "^apiVersion: ${{ matrix.expect_policy }}$" "/tmp/tests_k8s_${{ matrix.kube_version }}.yaml" + grep -q "^apiVersion: ${{ matrix.expect_cronjob }}$" "/tmp/tests_k8s_${{ matrix.kube_version }}.yaml" + grep -q "^apiVersion: ${{ matrix.expect_hpa }}$" "/tmp/tests_k8s_${{ matrix.kube_version }}.yaml" + + kubeconform -strict -summary -ignore-missing-schemas \ + -kubernetes-version "${{ matrix.kube_version }}" \ + "/tmp/tests_k8s_${{ matrix.kube_version }}.yaml" + + - name: Render contracts for Kubernetes ${{ matrix.kube_version }} + run: | + set -euo pipefail + werf helm template contracts tests/contracts \ + --kube-version "${{ matrix.kube_version }}" > "/tmp/contracts_k8s_${{ matrix.kube_version }}.yaml" + grep -q '^apiVersion:' "/tmp/contracts_k8s_${{ matrix.kube_version }}.yaml" + + kube-server-dry-run: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set lib version + run: | + LIB_VERSION=$(sed -n '/version/{s/version: //;p;}' charts/helm-apps/Chart.yaml) + sed -i 's/_FLANT_APPS_LIBRARY_VERSION_/'${LIB_VERSION}'/' charts/helm-apps/templates/_apps-version.tpl + + - name: Install werf CLI + uses: werf/actions/install@v1.2 + with: + channel: ea + + - name: Setup kubectl + uses: azure/setup-kubectl@v4 + with: + version: v1.29.0 + + - name: Create kind cluster + uses: helm/kind-action@v1.10.0 + with: + node_image: kindest/node:v1.29.2 + + - name: Install compatibility CRDs + run: | + set -euo pipefail + kubectl apply -f tests/crds/compat-crds.yaml + kubectl wait --for=condition=Established --timeout=120s crd --all + + - name: Update chart dependencies + run: | + werf helm dependency update tests/.helm + werf helm dependency update tests/contracts + + - name: Render contracts for server-side validation + run: | + set -euo pipefail + werf helm template contracts tests/contracts \ + --kube-version 1.29.0 \ + --set "apps-jobs.compat-job.restartPolicy=Never" \ + > /tmp/contracts_k8s_129_server.yaml + + - name: Server-side dry-run apply + run: | + set -euo pipefail + kubectl apply --dry-run=server -f /tmp/contracts_k8s_129_server.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59cc98c..9306be3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,64 +3,147 @@ name: Release Charts on: push: branches: - - "*" + - main + workflow_dispatch: jobs: release: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - - name: Set lib version - run: | - LIB_VERSION=$(sed -n '/version/{s/version: //;p;}' charts/helm-apps/Chart.yaml) - sed -i 's/_FLANT_APPS_LIBRARY_VERSION_/'${LIB_VERSION}'/' charts/helm-apps/templates/_apps-version.tpl - - - name: Install werf CLI - with: - channel: ea - uses: werf/actions/install@v1.2 - - - name: Install Helm3 - run: | - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash - - - name: Install Dyff - run: | - sudo snap install dyff - - - - name: Render - run: | - set -e - source $(werf ci-env github --as-file) - cd tests && werf render --dev --set "global._includes.apps-defaults.enabled=true" --env=prod - - - name: Test render - run: | - set -e - source $(werf ci-env github --as-file) - cd tests && werf render --dev --set "global._includes.apps-defaults.enabled=true" --env=prod | sed '/werf.io\//d' > test_render_check.yaml - dyff between test_render.yaml test_render_check.yaml | tee /tmp/test_render_check - check_tests=$(sed 1,7d /tmp/test_render_check | wc -l) - if [ $check_tests -gt "7" ]; then exit 1; fi - - - name: Run chart-releaser - if: ${{ github.ref == 'refs/heads/main' }} - uses: helm/chart-releaser-action@v1.4.0 - with: - charts_dir: charts - config: cr.yaml - env: - CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Set lib version + run: | + LIB_VERSION=$(sed -n '/version/{s/version: //;p;}' charts/helm-apps/Chart.yaml) + echo "LIB_VERSION=${LIB_VERSION}" >> "$GITHUB_ENV" + sed -i 's/_FLANT_APPS_LIBRARY_VERSION_/'${LIB_VERSION}'/' charts/helm-apps/templates/_apps-version.tpl + + - name: Install werf CLI + uses: werf/actions/install@v1.2 + with: + channel: ea + + - name: Install Dyff + run: | + curl --silent --location https://git.io/JYfAY | bash + + - name: Install kubeconform + run: | + set -euo pipefail + KUBECONFORM_VERSION=0.6.7 + curl -sSL -o /tmp/kubeconform.tar.gz "https://github.com/yannh/kubeconform/releases/download/v${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + tar -xzf /tmp/kubeconform.tar.gz -C /tmp kubeconform + sudo install -m 0755 /tmp/kubeconform /usr/local/bin/kubeconform + + - name: Update test chart dependencies + run: | + werf helm dependency update tests/.helm + + - name: Validate values schema + run: | + werf helm lint tests/.helm --values tests/.helm/values.yaml + + - name: Verify Kubernetes API compatibility + run: | + set -euo pipefail + + # New clusters: stable APIs must be rendered. + werf helm template tests tests/.helm \ + --set "global.env=prod" \ + --set "global._includes.apps-defaults.enabled=true" \ + --kube-version 1.29.0 > /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: policy/v1$' /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: batch/v1$' /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: autoscaling/v2$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: policy/v1beta1$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: batch/v1beta1$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: autoscaling/v2beta2$' /tmp/tests_k8s_129.yaml + kubeconform -strict -summary -ignore-missing-schemas -kubernetes-version 1.29.0 /tmp/tests_k8s_129.yaml + + # Legacy clusters: beta APIs remain supported. + werf helm template tests tests/.helm \ + --set "global.env=prod" \ + --set "global._includes.apps-defaults.enabled=true" \ + --kube-version 1.20.15 > /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: policy/v1beta1$' /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: batch/v1beta1$' /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: autoscaling/v2beta2$' /tmp/tests_k8s_120.yaml + ! grep -q '^apiVersion: autoscaling/v2$' /tmp/tests_k8s_120.yaml + kubeconform -strict -summary -ignore-missing-schemas -kubernetes-version 1.20.15 /tmp/tests_k8s_120.yaml + + - name: Render + run: | + set -e + source $(werf ci-env github --as-file) + cd tests && werf render --dev --set "global._includes.apps-defaults.enabled=true" --env=prod + + - name: Test render + run: | + set -e + source $(werf ci-env github --as-file) + cd tests && werf render --dev --set "global._includes.apps-defaults.enabled=true" --env=prod | sed '/werf.io\//d' > test_render_check.yaml + dyff between test_render.yaml test_render_check.yaml | tee /tmp/test_render_check + check_tests=$(sed 1,7d /tmp/test_render_check | wc -l) + if [ $check_tests -gt "7" ]; then exit 1; fi + + - name: Run chart-releaser + if: ${{ github.ref == 'refs/heads/main' }} + uses: helm/chart-releaser-action@v1.4.0 + with: + charts_dir: charts + config: cr.yaml + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: Generate and update GitHub release notes + if: ${{ github.ref == 'refs/heads/main' }} + env: + GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + run: | + set -euo pipefail + + TAG="helm-apps-${LIB_VERSION}" + + if ! gh release view "${TAG}" >/dev/null 2>&1; then + echo "Release ${TAG} not found (nothing new to publish), skipping notes update." + exit 0 + fi + + CHANGELOG_BODY="$(awk -v ver="${LIB_VERSION}" ' + $0 ~ "^## \\[" ver "\\]" {capture=1; next} + capture && $0 ~ "^## \\[" {exit} + capture {print} + ' CHANGELOG.md)" + + if [ -n "${CHANGELOG_BODY}" ]; then + GENERATED_BODY="${CHANGELOG_BODY}" + else + GENERATED_BODY=$(gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="${TAG}" \ + -f target_commitish="${GITHUB_SHA}" \ + --jq '.body') + fi + + { + echo "## Helm Apps Library" + echo + echo "- Chart: \`helm-apps\`" + echo "- Version: \`${LIB_VERSION}\`" + echo + echo "${GENERATED_BODY}" + } > /tmp/release_notes.md + + gh release edit "${TAG}" \ + --title "helm-apps ${LIB_VERSION}" \ + --notes-file /tmp/release_notes.md # - name: Publish to CR # env: @@ -69,4 +152,4 @@ jobs: # echo $CR_PAT | helm registry login -u alvnukov --password-stdin ghcr.io # find .cr-release-packages -mindepth 1 -maxdepth 1 -type f -name '*.tgz' -exec sh -c 'basename "$0"' '{}' \; | while read PACKAGE; do # helm push .cr-release-packages/$PACKAGE oci://ghcr.io/${GITHUB_REPOSITORY} -# done \ No newline at end of file +# done diff --git a/.gitignore b/.gitignore index 999b624..7f9f705 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ **.tgz /.packages/ tests/test_render_check.yaml +.idea/ + +# Internal planning +.internal/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..abdfeae --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,188 @@ +# AGENTS.md + +This file defines how AI agents should work with the `helm-apps` library in this repository. + +## 1. Goal + +When a user asks to deploy/update an app with this library, the agent should: + +1. Pick the correct `apps-*` entity. +2. Produce valid values in library style. +3. Keep behavior compatible across environments and Kubernetes versions. +4. Run required checks before finishing. + +## 2. Required Library Entry Point + +Any consumer chart must initialize the library with: + +```yaml +{{- include "apps-utils.init-library" $ }} +``` + +## 3. Supported Top-Level Sections + +Use these built-in groups when possible: + +- `apps-stateless` +- `apps-stateful` +- `apps-jobs` +- `apps-cronjobs` +- `apps-services` +- `apps-ingresses` +- `apps-network-policies` +- `apps-configmaps` +- `apps-secrets` +- `apps-pvcs` +- `apps-limit-range` +- `apps-certificates` +- `apps-dex-clients` +- `apps-dex-authenticators` +- `apps-custom-prometheus-rules` +- `apps-grafana-dashboards` +- `apps-kafka-strimzi` +- `apps-infra` + +Custom groups are allowed via: + +```yaml +my-group: + __GroupVars__: + type: apps-stateless +``` + +`__GroupVars__.type` may be a string or env-map. + +Custom renderers are also supported: + +1. Set `__GroupVars__.type` to your custom renderer name. +2. Define template `".render"` in the consumer chart templates. +3. Library will call `include (printf "%s.render" $type) $`. + +Context available inside custom renderer: + +- `$` (root context) +- `$.Values` +- `$.CurrentApp` +- `$.CurrentGroupVars` +- `$.CurrentGroup` +- `$.CurrentPath` +- `$.Release` +- `$.Capabilities` +- `$.Files` + +Any app fields from `custom-services..*` are passed as-is into `$.CurrentApp`. + +Minimal example: + +```yaml +custom-services: + __GroupVars__: + type: custom-services + minio: + enabled: true + host: + ip: minio.example.local + port: 9000 + extraLabels: + app.kubernetes.io/component: storage +``` + +```yaml +{{- define "custom-services.render" -}} +{{- $ := . -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $.CurrentApp.name | quote }} + labels: + app.kubernetes.io/name: {{ $.CurrentApp.name | quote }} + app.kubernetes.io/enabled: {{ printf "%v" $.CurrentApp.enabled | quote }} +{{- with $.CurrentApp.extraLabels }} +{{ toYaml . | indent 4 }} +{{- end }} +spec: + type: ExternalName + externalName: {{ printf "%v" $.CurrentApp.host.ip | quote }} + ports: + - port: {{ $.CurrentApp.host.port }} +{{- end -}} +``` + +## 4. Values Rules (Critical) + +1. Environment selection is done through `global.env`. +2. Prefer env-maps (`_default`, `prod`, regex keys) for env-specific values. +3. For most Kubernetes blocks, use YAML block strings (`|`) instead of native YAML lists/maps. +4. Native YAML lists are forbidden except allowed paths: + - `_include` + - `_include_files` + - `global._includes.*` + - `*.configFilesYAML.*.content.*` + - `*.envYAML.*` + - `apps-kafka-strimzi.*.kafka.brokers.hosts.*` + - `apps-kafka-strimzi.*.kafka.ui.dex.allowedGroups.*` + +If a forbidden list is used, render must fail with exact values path. + +## 5. Includes and Merge + +1. Reuse profiles via `global._includes` + `_include`. +2. Merge is recursive for maps. +3. `_include` chains are concatenated. +4. Local app values override included values. + +## 6. Release Mode + +Optional release matrix mode: + +- `global.release.enabled` (default `false`) +- `global.release.current` +- `global.release.versions` +- `global.release.autoEnableApps` (default `true`) +- app-level `releaseKey` (optional, fallback to app name) + +Behavior: + +- resolves `CurrentAppVersion`; +- uses it as image tag when `image.staticTag` is absent; +- adds annotations `helm-apps/release` and `helm-apps/app-version`. + +## 7. Network Policies + +For `apps-network-policies`, select implementation via `type`: + +- `kubernetes` -> `networking.k8s.io/v1` + `NetworkPolicy` +- `cilium` -> `cilium.io/v2` + `CiliumNetworkPolicy` +- `calico` -> `projectcalico.org/v3` + `NetworkPolicy` + +## 8. Mandatory Checks Before Final Answer + +Run (or equivalent): + +```bash +werf helm lint tests/.helm --values tests/.helm/values.yaml +werf helm template contracts tests/contracts +``` + +If you changed compatibility behavior, also check: + +```bash +werf helm template tests tests/.helm --set global.env=prod --set global._includes.apps-defaults.enabled=true --kube-version 1.29.0 +werf helm template tests tests/.helm --set global.env=prod --set global._includes.apps-defaults.enabled=true --kube-version 1.20.15 +``` + +## 9. If You Modify Library Behavior + +Update all relevant artifacts: + +1. Templates in `charts/helm-apps/templates/`. +2. Examples in `tests/.helm/values.yaml`. +3. Schema in `tests/.helm/values.schema.json`. +4. Contract tests in `tests/contracts/`. +5. CI checks in `.github/workflows/ci.yml`. +6. Docs (`README.md`, `docs/*`) and release notes/changelog when needed. + +## 10. Stability Priority + +For this repository, stability is higher priority than micro-optimizations. +Avoid risky shortcuts that reduce validation coverage or change merge semantics implicitly. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..088644c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,88 @@ +# Changelog + +All notable changes to the `helm-apps` library are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Automated GitHub Release notes generation in `release.yml`. + +## [1.6.0] - 2026-02-16 + +### Added +- New release matrix mode via `global.release`: + - `enabled`, `current`, `autoEnableApps`, `versions`. +- Added app-level `releaseKey` to map an app to release matrix keys. +- Automatic release annotations in rendered manifests: + - `helm-apps/release` + - `helm-apps/app-version` +- Added release-mode contract checks in CI. + +### Changed +- If `image.staticTag` is not set, image tag can be resolved from `CurrentAppVersion`. +- Extended `tests/.helm/values.schema.json` with `global.release` and `releaseKey`. +- Updated docs (`README.md`, `docs/reference-values.md`, `docs/parameter-index.md`) with release mode examples. + +## [1.5.0] - 2026-02-16 + +### Added +- Added `apps-network-policies` entity with `type`-based implementation selection: + - `kubernetes` -> `networking.k8s.io/v1`, `NetworkPolicy` + - `cilium` -> `cilium.io/v2`, `CiliumNetworkPolicy` + - `calico` -> `projectcalico.org/v3`, `NetworkPolicy` +- Added contract tests and CI checks for multiple NetworkPolicy implementations. +- Added opt-in strict validation: + - unknown keys in `apps-network-policies` fail; + - unknown top-level `apps-*` groups fail unless declared via `__GroupVars__.type`. + +### Changed +- Expanded user documentation and parameter navigation. +- Added values schema validation in CI. + +## [1.4.0] - 2026-02-16 + +### Added +- Added passthrough fields for Kubernetes entities: + - workload/container `extra*` fields for new or uncommon API fields without library changes. +- Added compatibility tests for multiple Kubernetes API versions. + +### Changed +- Improved compatibility across legacy and modern Kubernetes versions. + +## [1.3.2] - 2024-02-13 + +### Fixed +- Fixed handling of `null` variables in ConfigMap YAML. + +## [1.3.1] - 2024-02-12 + +### Fixed +- Additional fixes for `null` variable handling in ConfigMap YAML. + +## [1.3.0] - 2022-03-22 + +### Added +- Helm 3 compatibility. + +## [1.2.9] - 2021-12-07 + +### Fixed +- Error handling for include blocks loaded from files. + +## [1.2.8] - 2021-07-13 + +### Fixed +- Support for `tpl` in include file names. + +## [1.2.7] - 2021-06-03 + +### Fixed +- Correct merge behavior with `_default`. + +## [1.2.6] - 2021-05-21 + +### Fixed +- Added `_include_files` support. diff --git a/Makefile b/Makefile index 07f0838..a4e0a46 100644 --- a/Makefile +++ b/Makefile @@ -4,3 +4,5 @@ deps: werf helm dependency update tests/.helm save_tests: cd tests; werf render --set "global._includes.apps-defaults.enabled=true" --env=prod --dev | sed '/werf.io\//d' > test_render.yaml +ci_local: + bash scripts/ci-local.sh diff --git a/README.md b/README.md index bc01795..702fea4 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,101 @@ -## Репозиторий библиотеки для развертывания приложений в Kubernetes. -1. Позволяет: - * упростить структуру описания приложения. - * переиспользовать шаблоны одного приложения для множества других -2. Ускоряет: - * процесс ревью изменений приложения за счет стандартизирования подхода и уменьшению количества кода. - * развертывание новых приложений за счет лаконичного синтаксиса, сокращения повторяемого кода - * редактирование и добавление новых ресурсов к приложению -3. Упрощает: - * работу с сущностями Kubernetes (не нужно описывать все поля приложения, не нужно думать как правильно выглядит конструкции сущностей). - * связывание сущностей Kubernetes за счет использования хелперов - -> :warning: **На данный момент корректная работа чартов гарантируется только с утилитой** [**Werf**](https://werf.io) - -## Для подключения библиотеки необходимо: -### Инструкция по использованию -#### Использовать пример: -* Скопировать содержимое папки [docs/example](/docs/example) в корень нового проекта -* настроить файлы под свой проект -#### Вручную: -* Добавить в .gitlab-ci.yml строку подключения библиотеки общих чартов - ```bash - werf helm repo add --force-update helm-apps https://flant.github.io/helm-apps - ``` - + к примеру так: - ```yaml - before_script: - - type trdl && source $(trdl use werf ${WERF_VERSION:-1.2 ea}) - - type werf && source $(werf ci-env gitlab --as-file) - - werf helm repo add --force-update helm-apps https://flant.github.io/helm-apps - ``` - у себя на компьютере добавляем репозиторий helm-apps: - ```yaml - werf helm repo add --force-update helm-apps https://flant.github.io/helm-apps - ``` - и обновляем зависимости: - ```yaml - werf helm dependency update .helm - ``` -* Добавить в папку .helm/templates файл [init-helm-apps.yaml](tests/.helm/templates/init-helm-apps.yaml) для инициализаци библиотеки, содержимое файла: - ```yaml - {{- /* Подключаем библиотеку */}} - {{- include "apps-utils.init-library" $ }} - ``` -* В Chart.yaml в секцию **dependencies**: - ```yaml - apiVersion: v2 - name: test-app - version: 1.0.0 - dependencies: +# Helm Apps Library + +Библиотека Helm-шаблонов для стандартизированного деплоя приложений в Kubernetes. + +`helm-apps` позволяет описывать приложения через `values.yaml` без копирования шаблонов между сервисами. +Логика рендера централизована в библиотеке, а сервисные репозитории хранят только конфигурацию. + +> Библиотека полностью поддерживает Helm и совместима с werf. +> Практически, для командного daily workflow werf часто удобнее: он объединяет рендер и процесс поставки в единый поток, снижая количество ручных шагов в CI/CD. +> При этом весь функционал библиотеки доступен и через чистый Helm. + +История изменений: [`CHANGELOG.md`](CHANGELOG.md) + +## Зачем использовать библиотеку + +- Единый стандарт деплоя для всех сервисов команды. +- Меньше копипаста и ручных Kubernetes-манифестов. +- Быстрее ревью: одинаковая структура конфигов между проектами. +- Переиспользование через [`_include`](docs/parameter-index.md#core) и [`global._includes`](docs/parameter-index.md#core). +- Поддержка окружений через [`global.env`](docs/parameter-index.md#core) (`_default`, env overrides, regex env keys). +- Режим релиз-матрицы через [`global.release`](docs/reference-values.md#param-global-release) для автоподстановки тегов и централизованного переключения версий. +- Поддержка связанных ресурсов (Service, Ingress, ConfigMap, Secret, HPA, VPA, PDB и др.) в одной модели. + +## Какие ресурсы поддерживаются + +- `apps-stateless` (`Deployment`) +- `apps-stateful` (`StatefulSet`) +- `apps-jobs` (`Job`) +- `apps-cronjobs` (`CronJob`) +- `apps-services` (`Service`) +- `apps-ingresses` (`Ingress`) +- `apps-network-policies` (`NetworkPolicy`) +- `apps-configmaps` (`ConfigMap`) +- `apps-secrets` (`Secret`) +- `apps-pvcs` (`PersistentVolumeClaim`) +- `apps-limit-range` (`LimitRange`) +- `apps-certificates` (`Certificate`) +- `apps-dex-clients`, `apps-dex-authenticators` +- `apps-custom-prometheus-rules`, `apps-grafana-dashboards` +- `apps-kafka-strimzi` +- `apps-infra` + +Для `apps-network-policies` можно выбрать API через `type`: +- `kubernetes` (default) -> `networking.k8s.io/v1`, `NetworkPolicy` +- `cilium` -> `cilium.io/v2`, `CiliumNetworkPolicy` +- `calico` -> `projectcalico.org/v3`, `NetworkPolicy` +- для любого другого CNI можно явно задать `apiVersion`, `kind` и `spec`. + +## Быстрый старт + +### 1. Подключить dependency + +В `.helm/Chart.yaml`: + +```yaml +apiVersion: v2 +name: my-app +version: 1.0.0 +dependencies: - name: helm-apps version: ~1 repository: "@helm-apps" - ``` -* В [values.yaml](docs/example/.helm/values.yaml) отредактировать секцию global._includes с параметрами по умолчанию для хелперов. +``` -На данный момент актуальная документация находится в файле [tests/.helm/values.yaml](tests/.helm/values.yaml). Ведется дополнительная работа над созданием расширенной версии документации. +### 2. Добавить инициализацию библиотеки -[О хелперах]( docs/usage.md) +Создать `.helm/templates/init-helm-apps-library.yaml`: -## Пример простейшего деплоймента Nginx на библиотеке: -
-values.yaml секция приложений +```yaml +{{- include "apps-utils.init-library" $ }} +``` + +### 3. Обновить зависимости + +```bash +helm repo add --force-update helm-apps https://alvnukov.github.io/helm-apps +helm dependency update .helm +``` + +### 4. Описать приложение в values + +Минимальный пример: ```yaml global: - ci_url: example.com -# ... + env: prod + ci_url: example.org + apps-stateless: - # Приложение из примера в документации - nginx: + api: _include: ["apps-stateless-defaultApp"] - replicas: 1 containers: - nginx: + main: image: name: nginx ports: | - name: http containerPort: 80 - configFiles: - default.conf: - mountPath: /etc/nginx/templates/default.conf.template - content: | - server { - listen 80 default_server; - listen [::]:80 default_server; - server_name {{ $.Values.global.ci_url }} {{ $.Values.global.ci_url }}; - root /var/www/{{ $.Values.global.ci_url }}; - index index.html; - try_files $uri /index.html; - location / { - proxy_set_header Authorization "Bearer ${SECRET_TOKEN}"; - proxy_pass_header Authorization; - proxy_pass https://backend:3000; - } - } - secretEnvVars: - SECRET_TOKEN: "!!!secret-token-for-backend!!!" service: enabled: true ports: | @@ -104,240 +103,265 @@ apps-stateless: port: 80 apps-ingresses: - nginx: + api: _include: ["apps-ingresses-defaultIngress"] - host: '{{ $.Values.global.ci_url }}' + host: "{{ $.Values.global.ci_url }}" paths: | - path: / pathType: Prefix backend: service: - name: nginx + name: api port: number: 80 tls: enabled: true ``` -
-
-Сгенерирует следующее... + + +## Ключевая механика: `global._includes` и рекурсивный merge + +`global._includes` — это библиотека переиспользуемых конфигурационных блоков. +Приложение подключает их через `_include`, после чего библиотека делает рекурсивный merge. + +Базовый пример: ```yaml -# Helm Apps Library: apps-stateless.nginx.podDisruptionBudget -apiVersion: policy/v1beta1 -kind: PodDisruptionBudget -metadata: - name: "nginx" - labels: - app: "nginx" - chart: "tests" - repo: "" - annotations: - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 -spec: - selector: - matchLabels: - app: "nginx" - maxUnavailable: "15%" ---- -# Helm Apps Library: apps-stateless.nginx.containers.nginx.secretEnvVars -apiVersion: v1 -kind: Secret -metadata: - name: "envs-containers-nginx-nginx" - labels: - app: "nginx" - chart: "tests" - repo: "" - annotations: - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 -type: Opaque -data: - "SECRET_TOKEN": "ISEhc2VjcmV0LXRva2VuLWZvci1iYWNrZW5kISEh" ---- -# Helm Apps Library: apps-stateless.nginx.containers.nginx.configFiles.default.conf -apiVersion: v1 -kind: ConfigMap -metadata: - name: "config-containers-nginx-nginx-default-conf" - labels: - app: "nginx" - chart: "tests" - repo: "" - annotations: - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 -data: - "default.conf": | - server { - listen 80 default_server; - listen [::]:80 default_server; - server_name example.com example.com; - root /var/www/example.com; - index index.html; - try_files $uri /index.html; - location / { - proxy_set_header Authorization "Bearer ${SECRET_TOKEN}"; - proxy_pass_header Authorization; - proxy_pass https://backend:3000; - } - } ---- -# Helm Apps Library: apps-stateless.nginx.service -apiVersion: v1 -kind: Service -metadata: - name: "nginx" - labels: - app: "nginx" - chart: "tests" - repo: "" - annotations: - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 -spec: - selector: - app: "nginx" - ports: - - name: http - port: 80 ---- -# Source: tests/templates/init-flant-apps-library.yaml -# Helm Apps Library: apps-stateless.nginx -apiVersion: apps/v1 -kind: Deployment -metadata: - name: "nginx" - annotations: - checksum/config: "19812d5210967fd69097dc991263af171c4071ebb455357bd49be2a0ca05acdd" - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 - labels: - app: "nginx" - chart: "tests" - repo: "" -spec: - strategy: - rollingUpdate: - maxSurge: 20% - maxUnavailable: 50% - type: RollingUpdate - template: - metadata: - name: "nginx" - annotations: - checksum/config: "19812d5210967fd69097dc991263af171c4071ebb455357bd49be2a0ca05acdd" - labels: - app: "nginx" - chart: "tests" - repo: "" - spec: +global: + _includes: + profile-base: + replicas: 2 + service: + enabled: true + ports: | + - name: http + port: 80 containers: - - name: "nginx" - image: REPO:TAG - envFrom: - - secretRef: - name: "envs-containers-nginx-nginx" + main: resources: - volumeMounts: - - name: "config-containers-nginx-nginx-default-conf" - subPath: "default.conf" - mountPath: "/etc/nginx/templates/default.conf.template" - ports: - - name: http - containerPort: 80 - imagePullSecrets: - - name: registrysecret - volumes: - - name: "config-containers-nginx-nginx-default-conf" - configMap: - name: "config-containers-nginx-nginx-default-conf" - selector: - matchLabels: - app: "nginx" - revisionHistoryLimit: 3 - replicas: 1 ---- -# Source: tests/templates/init-flant-apps-library.yaml -# Helm Apps Library: apps-ingresses.nginx -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: "nginx" - annotations: - kubernetes.io/ingress.class: "nginx" - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 - labels: - app: "nginx" - chart: "tests" - repo: "" -spec: - tls: - - secretName: nginx - rules: - - host: "example.com" - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: nginx - port: - number: 80 ---- -# Helm Apps Library: apps-ingresses.nginx.tls -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: nginx - annotations: - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 -spec: - secretName: nginx - issuerRef: - kind: ClusterIssuer - name: letsencrypt - dnsNames: - - "example.com" ---- -# Helm Apps Library: apps-stateless.nginx.verticalPodAutoscaler -apiVersion: autoscaling.k8s.io/v1 -kind: VerticalPodAutoscaler -metadata: - name: "nginx" - labels: - app: "nginx" - chart: "tests" - repo: "" - annotations: - project.werf.io/env: "" - project.werf.io/name: test - werf.io/version: v1.2.162 -spec: - targetRef: - apiVersion: "apps/v1" - kind: Deployment - name: "nginx" - updatePolicy: - updateMode: "Off" - resourcePolicy: {} + requests: + mcpu: 100 + memoryMb: 128 + profile-prod: + replicas: 4 + containers: + main: + resources: + limits: + memoryMb: 512 + +apps-stateless: + api: + _include: ["profile-base", "profile-prod"] + containers: + main: + image: + name: nginx +``` + +Что важно: + +1. Merge рекурсивный: вложенные map-структуры не заменяются целиком, а объединяются по ключам. +2. Порядок `_include` важен: каждый следующий профиль может переопределять предыдущий. +3. Локальные поля приложения имеют приоритет над значениями из include-блоков. +4. Это главный механизм DRY в библиотеке: стандартные профили задаются один раз и переиспользуются во всех сервисах. +5. Native YAML list в values запрещены (кроме `_include` и `_include_files`): для Kubernetes list-полей используйте YAML block string (`|`). + +## Release mode (`global.release`) + +Опциональный режим для централизованного управления версиями приложений: +- `global.release.enabled` по умолчанию `false`; +- задаете текущий релиз в `global.release.current`; +- храните матрицу `release -> app -> version` в `global.release.versions`; +- ключ приложения берется из `releaseKey`, а если он не задан — из имени приложения (`app.name`); +- `autoEnableApps` по умолчанию `true`; +- app получает `CurrentAppVersion`, и если `image.staticTag` не задан, тег берется из релизной матрицы; +- в рендер добавляются аннотации `helm-apps/release` и `helm-apps/app-version`. + +Важно: +- если для app не найдена версия в `global.release.versions.`, приложение рендерится по обычной логике; +- если не задан ни `image.staticTag`, ни `CurrentAppVersion`, используется стандартный путь через `Values.werf.image`. + +Практический референс и пример: [`docs/reference-values.md#param-global-release`](docs/reference-values.md#param-global-release) + +### Примеры merge-поведения + +#### Пример 1: Рекурсивный merge map + +```yaml +global: + _includes: + base: + service: + enabled: true + headless: false + net: + service: + ports: | + - name: http + port: 80 + +apps-stateless: + api: + _include: ["base", "net"] +``` + +Итог для `api.service`: +- `enabled: true` +- `headless: false` +- `ports: ...` + +#### Пример 2: Порядок include (последний имеет приоритет) + +```yaml +global: + _includes: + base: + replicas: 2 + prod: + replicas: 5 + +apps-stateless: + api: + _include: ["base", "prod"] +``` + +Итог: `replicas: 5`. + +#### Пример 3: Локальный override сильнее include + +```yaml +global: + _includes: + base: + replicas: 2 + +apps-stateless: + api: + _include: ["base"] + replicas: 3 +``` + +Итог: `replicas: 3`. + +#### Пример 4: Env-map merge с `_default` и конкретным env + +Пример: + +```yaml +global: + _includes: + base: + replicas: + _default: 2 + production: 4 + canary: + replicas: + _default: 1 + production: 2 + +apps-stateless: + api: + _include: ["base", "canary"] ``` -
-Самостоятельно можно отрендерить следующей командой: +Итоговое поведение: +- для `production` будет использовано значение `4` (из `base.production`); +- для env без явного ключа будет использовано `_default: 1` (из `canary._default`). + +Практика: +- окружение передавайте через `global.env`; +- всегда проверяйте итоговый рендер в целевом env (`helm template ... --set global.env=`); +- для критичных env-map лучше держать все нужные env-ключи явно в финальном профиле. + + +#### Пример 5: `_include`-списки конкатенируются + +Если include-профиль сам содержит `_include`, итоговый список объединяется. + +```yaml +global: + _includes: + profile-a: + _include: ["base-a"] + replicas: 2 + profile-b: + _include: ["base-b"] + service: + enabled: true + +apps-stateless: + api: + _include: ["profile-a", "profile-b"] +``` + +Итоговый include-chain для `api` объединяет оба списка (`base-a` + `base-b`) и затем применяет локальные поля. + +#### Пример 6: Что со списками + +Важный нюанс библиотеки: +- специальные списки `_include` конкатенируются; +- обычные “списковые” параметры в большинстве случаев задаются как YAML-строки (`|`), а не как native list. + +Поэтому merge для обычных списков как list-поведение обычно не используется. +Практика: +- задавайте списковые Kubernetes-блоки строкой YAML; +- итог проверяйте через `helm template`. + +### 5. Проверить рендер ```bash -$ cd tests && werf render --dev --set "apps-ingresses.nginx.enabled=true" --set "apps-stateless.nginx.enabled=true" +helm lint .helm +helm template my-app .helm --set global.env=prod ``` + +### 6. Совместимость с версиями Kubernetes + +Библиотека автоматически учитывает версию Kubernetes через `.Capabilities`: +- выбирает подходящий `apiVersion` для `CronJob`, `PodDisruptionBudget`, `HorizontalPodAutoscaler`, `VerticalPodAutoscaler`; +- учитывает различия в полях `spec` между версиями (например, в `Service` и `StatefulSet`). +- поддерживает passthrough для редких/новых полей через: + - `extraSpec` (ресурсный `spec`); + - `podSpecExtra` (Pod template `spec`); + - `jobTemplateExtraSpec` (`Job.spec` / `CronJob.spec.jobTemplate.spec`); + - `extraFields` (top-level поля ресурса/контейнера). + +Практика для проверки: +- новый кластер: `helm template ... --kube-version 1.29.0` +- legacy-кластер: `helm template ... --kube-version 1.20.15` + +Текущий CI также проверяет рендер на нескольких версиях Kubernetes. + +## Маршрут по документации + +Стартовая точка: +- [docs/README.md](docs/README.md) + +Подробные документы: +- Концепция и архитектура: [docs/library-guide.md](docs/library-guide.md) +- Полный справочник полей: [docs/reference-values.md](docs/reference-values.md) +- Быстрый индекс параметров (описание + примеры): [docs/parameter-index.md](docs/parameter-index.md) +- Use-case карта (задача -> параметр -> пример -> проверка): [docs/use-case-map.md](docs/use-case-map.md) +- Готовые шаблоны для типовых сценариев: [docs/cookbook.md](docs/cookbook.md) +- Эксплуатация, triage, rollback: [docs/operations.md](docs/operations.md) +- Краткие правила helper-паттернов: [docs/usage.md](docs/usage.md) + +Практические артефакты: +- Полный рабочий пример values: [tests/.helm/values.yaml](tests/.helm/values.yaml) +- JSON Schema валидации values: [tests/.helm/values.schema.json](tests/.helm/values.schema.json) +- Готовый пример проекта: [docs/example](docs/example) + +Быстрые ссылки на параметры: +- Индекс параметров: [docs/parameter-index.md](docs/parameter-index.md) +- `global.env`: [описание + пример](docs/parameter-index.md#core) +- `_include` / `global._includes`: [описание + примеры merge](docs/parameter-index.md#core) +- `containers` / `envVars` / `secretEnvVars`: [описание + примеры](docs/parameter-index.md#containers-envconfig) + +## Для контрибьюторов библиотеки + +При изменении возможностей библиотеки обновляйте синхронно: + +1. шаблоны в `charts/helm-apps/templates`; +2. примеры в `tests/.helm/values.yaml`; +3. схему в `tests/.helm/values.schema.json`; +4. документацию в `docs/reference-values.md` и `docs/cookbook.md`. diff --git a/charts/helm-apps/Chart.yaml b/charts/helm-apps/Chart.yaml index 2cd2d1c..b0a6aea 100644 --- a/charts/helm-apps/Chart.yaml +++ b/charts/helm-apps/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: helm-apps description: A Helm applications library type: library -version: 1.1.2 +version: 1.6.2 +icon: https://raw.githubusercontent.com/alvnukov/helm-apps/main/docs/assets/icon.png maintainers: - name: alvnukov - email: alexandr.vnukov@flant.com url: https://github.com/alvnukov diff --git a/charts/helm-apps/templates/_apps-api-versions.tpl b/charts/helm-apps/templates/_apps-api-versions.tpl new file mode 100644 index 0000000..342fb87 --- /dev/null +++ b/charts/helm-apps/templates/_apps-api-versions.tpl @@ -0,0 +1,33 @@ +{{- define "apps-api-versions.cronJob" -}} +{{- if or (.Capabilities.APIVersions.Has "batch/v1/CronJob") (semverCompare ">=1.21-0" .Capabilities.KubeVersion.GitVersion) -}} +batch/v1 +{{- else -}} +batch/v1beta1 +{{- end -}} +{{- end -}} + +{{- define "apps-api-versions.podDisruptionBudget" -}} +{{- if or (.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget") (semverCompare ">=1.21-0" .Capabilities.KubeVersion.GitVersion) -}} +policy/v1 +{{- else -}} +policy/v1beta1 +{{- end -}} +{{- end -}} + +{{- define "apps-api-versions.horizontalPodAutoscaler" -}} +{{- if or (.Capabilities.APIVersions.Has "autoscaling/v2/HorizontalPodAutoscaler") (semverCompare ">=1.23-0" .Capabilities.KubeVersion.GitVersion) -}} +autoscaling/v2 +{{- else -}} +autoscaling/v2beta2 +{{- end -}} +{{- end -}} + +{{- define "apps-api-versions.verticalPodAutoscaler" -}} +{{- if .Capabilities.APIVersions.Has "autoscaling.k8s.io/v1/VerticalPodAutoscaler" -}} +autoscaling.k8s.io/v1 +{{- else if .Capabilities.APIVersions.Has "autoscaling.k8s.io/v1beta2/VerticalPodAutoscaler" -}} +autoscaling.k8s.io/v1beta2 +{{- else -}} +autoscaling.k8s.io/v1 +{{- end -}} +{{- end -}} diff --git a/charts/helm-apps/templates/_apps-compat.tpl b/charts/helm-apps/templates/_apps-compat.tpl new file mode 100644 index 0000000..e057ab9 --- /dev/null +++ b/charts/helm-apps/templates/_apps-compat.tpl @@ -0,0 +1,99 @@ +{{- define "apps-compat.normalizeServiceSpec" -}} +{{- $ := index . 0 -}} +{{- $service := index . 1 -}} +{{- if and $service (kindIs "map" $service) -}} + {{- if not (semverCompare ">=1.20-0" $.Capabilities.KubeVersion.GitVersion) -}} + {{- $_ := unset $service "allocateLoadBalancerNodePorts" -}} + {{- $_ := unset $service "clusterIPs" -}} + {{- $_ := unset $service "ipFamilies" -}} + {{- $_ := unset $service "ipFamilyPolicy" -}} + {{- end -}} + {{- if not (semverCompare ">=1.21-0" $.Capabilities.KubeVersion.GitVersion) -}} + {{- $_ := unset $service "loadBalancerClass" -}} + {{- end -}} + {{- if not (semverCompare ">=1.22-0" $.Capabilities.KubeVersion.GitVersion) -}} + {{- $_ := unset $service "internalTrafficPolicy" -}} + {{- end -}} +{{- end -}} +{{- end -}} + +{{- define "apps-compat.normalizeStatefulSetSpec" -}} +{{- $ := index . 0 -}} +{{- $app := index . 1 -}} +{{- if and $app (kindIs "map" $app) -}} + {{- $_ := unset $app "progressDeadlineSeconds" -}} + {{- if not (semverCompare ">=1.23-0" $.Capabilities.KubeVersion.GitVersion) -}} + {{- $_ := unset $app "persistentVolumeClaimRetentionPolicy" -}} + {{- end -}} + {{- if not (semverCompare ">=1.25-0" $.Capabilities.KubeVersion.GitVersion) -}} + {{- $_ := unset $app "minReadySeconds" -}} + {{- end -}} +{{- end -}} +{{- end -}} + +{{- define "apps-compat.renderRaw" -}} +{{- $ := index . 0 -}} +{{- $scope := index . 1 -}} +{{- $value := index . 2 -}} +{{- if kindIs "string" $value -}} +{{ include "fl.value" (list $ $scope $value) }} +{{- else if or (kindIs "map" $value) (kindIs "slice" $value) -}} +{{ toYaml $value }} +{{- else -}} +{{ include "fl.value" (list $ $scope $value) }} +{{- end -}} +{{- end -}} + +{{- define "apps-compat.enforceAllowedKeys" -}} +{{- $ := index . 0 -}} +{{- $scope := index . 1 -}} +{{- $allowed := index . 2 -}} +{{- $scopePath := index . 3 -}} +{{- if kindIs "map" $scope -}} +{{- range $key, $_ := $scope }} +{{- if and (not (has $key $allowed)) (not (hasPrefix "__" $key)) }} +{{- fail (printf "Strict mode: unknown key '%s' at %s" $key $scopePath) }} +{{- end }} +{{- end }} +{{- end }} +{{- end -}} + +{{- define "apps-compat.validateTopLevelStrict" -}} +{{- $ := index . 0 -}} +{{- $values := index . 1 -}} +{{- $knownTopLevel := index . 2 -}} +{{- if kindIs "map" $values -}} +{{- range $key, $val := $values }} +{{- if has $key $knownTopLevel }} +{{- else if and (kindIs "map" $val) (hasKey $val "__GroupVars__") }} +{{- else if hasPrefix "apps-" $key }} +{{- fail (printf "Strict mode: unknown top-level apps group '%s'. Use built-in apps-* group or define custom group with __GroupVars__.type" $key) }} +{{- end }} +{{- end }} +{{- end }} +{{- end -}} + +{{- define "apps-compat.assertNoUnexpectedLists" -}} +{{- $ := index . 0 -}} +{{- $value := index . 1 -}} +{{- $path := index . 2 -}} +{{- $pathString := join "." $path -}} +{{- if kindIs "slice" $value -}} + {{- $last := "" -}} + {{- if gt (len $path) 0 -}} + {{- $last = last $path -}} + {{- end -}} + {{- $isAllowedKafkaHosts := regexMatch "^Values\\.apps-kafka-strimzi\\..*\\.kafka\\.brokers\\.hosts\\.[^.]+$" $pathString -}} + {{- $isAllowedKafkaDexGroups := regexMatch "^Values\\.apps-kafka-strimzi\\..*\\.kafka\\.ui\\.dex\\.allowedGroups\\.[^.]+$" $pathString -}} + {{- $isAllowedGlobalInclude := regexMatch "^Values\\.global\\._includes\\..*" $pathString -}} + {{- $isAllowedConfigFilesYAMLContent := regexMatch "^Values\\..*\\.configFilesYAML\\..*\\.content\\..*" $pathString -}} + {{- $isAllowedEnvYAML := regexMatch "^Values\\..*\\.envYAML\\..*" $pathString -}} + {{- if not (or (eq $last "_include") (eq $last "_include_files") $isAllowedGlobalInclude $isAllowedKafkaHosts $isAllowedKafkaDexGroups $isAllowedConfigFilesYAMLContent $isAllowedEnvYAML) -}} + {{- fail (printf "Invalid values: list value is not allowed at %s. Use YAML block string ('|') for Kubernetes list fields. Allowed native lists: _include, _include_files, global._includes.*, *.configFilesYAML.*.content.*, *.envYAML.*, apps-kafka-strimzi.*.kafka.brokers.hosts.*, apps-kafka-strimzi.*.kafka.ui.dex.allowedGroups.*." (join "." $path)) -}} + {{- end -}} +{{- else if kindIs "map" $value -}} + {{- range $k, $v := $value -}} + {{- include "apps-compat.assertNoUnexpectedLists" (list $ $v (append $path $k)) -}} + {{- end -}} +{{- end -}} +{{- end -}} diff --git a/charts/helm-apps/templates/_apps-components.tpl b/charts/helm-apps/templates/_apps-components.tpl index 9ea0e7c..de9f2fd 100644 --- a/charts/helm-apps/templates/_apps-components.tpl +++ b/charts/helm-apps/templates/_apps-components.tpl @@ -8,7 +8,7 @@ {{- if include "fl.isTrue" (list $ . $verticalPodAutoscaler.enabled) }} --- {{- include "apps-utils.printPath" $ }} -apiVersion: autoscaling.k8s.io/v1 +apiVersion: {{ include "apps-api-versions.verticalPodAutoscaler" $ }} kind: VerticalPodAutoscaler {{- include "apps-helpers.metadataGenerator" (list $ $verticalPodAutoscaler ) }} spec: @@ -23,6 +23,9 @@ spec: {{- else }} resourcePolicy: {} {{- end }} +{{- with include "apps-compat.renderRaw" (list $ . $verticalPodAutoscaler.extraSpec) | trim }} + {{- . | nindent 2 }} +{{- end }} {{- end }} {{- end }} {{- include "apps-utils.leaveScope" $ }} @@ -61,23 +64,26 @@ spec: {{- if include "fl.isTrue" (list $ . .enabled) }} --- {{- include "apps-utils.printPath" $ }} -{{- if semverCompare ">=1.21.0-0" $.Capabilities.KubeVersion.GitVersion }} -apiVersion: policy/v1 -{{- else }} -apiVersion: policy/v1beta1 -{{- end }} +apiVersion: {{ include "apps-api-versions.podDisruptionBudget" $ }} kind: PodDisruptionBudget {{- include "apps-helpers.metadataGenerator" (list $ $podDisruptionBudget) }} spec: selector: matchLabels: -{{- include "fl.generateSelectorLabels" (list $ . $.CurrentApp.name) | trim | nindent 6 }} +{{- if empty (include "fl.value" (list $ . $.CurrentApp.selector)) }} +{{- include "fl.generateSelectorLabels" (list $ . $.CurrentApp.name) | trim | nindent 6 }} +{{- else }} +{{- $.CurrentApp.selector | trim | nindent 6 }} +{{- end }} {{- with include "fl.value" (list $ . .maxUnavailable) }} maxUnavailable: {{ . }} {{- end }} {{- with include "fl.value" (list $ . .minAvailable) }} minAvailable: {{ . }} {{- end }} + {{- with include "apps-compat.renderRaw" (list $ . .extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- end }} {{- end }} {{- include "apps-utils.leaveScope" $ }} @@ -93,7 +99,9 @@ spec: {{- include "apps-utils.enterScope" (list $ "service") }} --- {{- include "apps-utils.printPath" $ }} -{{- $_ := set $service "selector" (include "fl.generateSelectorLabels" (list $ . $.CurrentApp.name) | trim) }} +{{- if empty (include "fl.value" (list $ . $service.selector)) }} +{{- $_ := set $service "selector" (include "fl.generateSelectorLabels" (list $ . $.CurrentApp.name) | trim) }} +{{- end }} {{- if include "fl.isTrue" (list $ . $service.headless) }} {{- $_ := set $service "clusterIP" "None" }} {{- end }} @@ -107,6 +115,7 @@ spec: {{- define "apps-components._service" }} {{- $ := index . 0 }} {{- $RelatedScope := index . 1 }} +{{- include "apps-compat.normalizeServiceSpec" (list $ $RelatedScope) }} apiVersion: v1 kind: Service {{- include "apps-helpers.metadataGenerator" (list $ $RelatedScope) }} @@ -119,6 +128,9 @@ spec: {{- $_ = set $specs "Numbers" (list "healthCheckNodePort") }} {{- $_ = set $specs "Maps" (list "sessionAffinityConfig" "selector") }} {{- include "apps-utils.generateSpecs" (list $ $RelatedScope $specs) | nindent 2 }} + {{- with include "apps-compat.renderRaw" (list $ $RelatedScope $RelatedScope.extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- end }} @@ -132,7 +144,7 @@ spec: {{- if include "fl.isTrue" (list $ . $.CurrentApp.horizontalPodAutoscaler.enabled) }} --- {{- include "apps-utils.printPath" $ }} -apiVersion: autoscaling/v2beta2 +apiVersion: {{ include "apps-api-versions.horizontalPodAutoscaler" $ }} kind: HorizontalPodAutoscaler {{- include "apps-helpers.metadataGenerator" (list $ $.CurrentApp.horizontalPodAutoscaler ) }} spec: @@ -151,6 +163,9 @@ spec: {{- include "apps-helpers.generateHPAMetrics" (list $ $RelatedScope) | trim | nindent 2 }} {{- end }} {{- end }} + {{- with include "apps-compat.renderRaw" (list $ . $.CurrentApp.horizontalPodAutoscaler.extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- range $_customMetricResourceName, $_customMetricResource := $.CurrentApp.horizontalPodAutoscaler.customMetricResources }} {{- include "apps-utils.enterScope" (list $ $_customMetricResourceName) }} @@ -202,6 +217,29 @@ data: {{- end }} {{- end }} {{- include "apps-utils.leaveScope" $ }} +{{- /* ConfigMaps created by "configFilesYAML:" option */ -}} +{{- include "apps-utils.enterScope" (list $ "configFilesYAML") }} +{{- range $configFileName, $configFile := .configFilesYAML }} +{{- if kindIs "map" .content }} +{{- include "apps-utils.enterScope" (list $ $configFileName) }} +--- +{{- include "apps-utils.printPath" $ }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ print "config-yaml-" $containersType "-" $.CurrentApp.name "-" $.CurrentContainer.name "-" $configFileName | include "fl.formatStringAsDNSLabel" | quote }} + {{- with include "apps-helpers.generateAnnotations" (list $ .) | trim }} + {{- . | nindent 2 }} + {{- end }} + labels: {{ include "fl.generateLabels" (list $ . $.CurrentApp.name) | trim | nindent 4 }} +data: + +{{- include "apps-helpers.generateConfigYAML" (list $ .content .content "content") }} + {{ $configFileName | quote }}: | {{ toYaml .content | trim | nindent 4 }} +{{- include "apps-utils.leaveScope" $ }} +{{- end }} +{{- end }} +{{- include "apps-utils.leaveScope" $ }} {{- /* Secrets created by "secretConfigFiles:" option */ -}} {{- range $secretConfigFileName, $secretConfigFile := .secretConfigFiles }} {{- if include "fl.value" (list $ . .content) }} @@ -253,19 +291,33 @@ data: {{ include "fl.generateSecretEnvVars" (list $ . .secretEnvVars) | trim | n {{- $allConfigMaps := "" }} {{- range $_, $containersType := list "initContainers" "containers" }} {{- range $_containerName, $_container := index $.CurrentApp $containersType }} +{{- $_ := set $ "CurrentContainer" . }} {{- if hasKey . "enabled" }} {{- if include "fl.isTrue" (list $ . .enabled) }} -{{- range $configFileName, $configFile := .configFiles }} -{{- $allConfigMaps = print $allConfigMaps (include "fl.value" (list $ $RelatedScope $configFile.content)) }} -{{- end }} +{{- $allConfigMaps = print $allConfigMaps (include "apps-components._generate-config-checksum" $) }} {{- end }} {{- else }} -{{- range $configFileName, $configFile := .configFiles }} -{{- $allConfigMaps = print $allConfigMaps (include "fl.value" (list $ $RelatedScope $configFile.content)) }} -{{- end }} +{{- $allConfigMaps = print $allConfigMaps (include "apps-components._generate-config-checksum" $) }} {{- end }} {{- end }} {{- end }} {{- printf "checksum/config: '%s'" ($allConfigMaps | sha256sum) }} {{- end }} + +{{- define "apps-components._generate-config-checksum" }} +{{- $ := . }} +{{- with $.CurrentApp }} +{{- range $_, $configFile := $.CurrentContainer.configFiles }} +{{- print (include "fl.value" (list $ . $configFile.content)) }} +{{- end }} +{{- range $_, $configFile := $.CurrentContainer.secretFiles }} +{{- print (include "fl.value" (list $ . $configFile.content)) }} +{{- end }} +{{- range $_, $configFile := $.CurrentContainer.configFilesYAML }} + +{{- include "apps-helpers.generateConfigYAML" (list $ $configFile.content $configFile.content "content") }} +{{- $configFile.content | toYaml }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/helm-apps/templates/_apps-configmaps.tpl b/charts/helm-apps/templates/_apps-configmaps.tpl index 6ac6b36..2df7e1b 100644 --- a/charts/helm-apps/templates/_apps-configmaps.tpl +++ b/charts/helm-apps/templates/_apps-configmaps.tpl @@ -14,8 +14,29 @@ apiVersion: v1 kind: ConfigMap {{- include "apps-helpers.metadataGenerator" (list $ .) }} +{{- with include "apps-compat.renderRaw" (list $ . .extraFields) | trim }} +{{- . | nindent 0 }} +{{- end }} +{{- $data := "" }} +{{- with include "apps.generateConfigMapEnvVars" (list $ . .envVars) }} +{{- $data = printf "%s\n%s" $data . | trim }} +{{- end }} +{{- if kindIs "map" .data }} +{{- with include "apps.generateConfigMapData" (list $ . .data) }} +{{- $data = printf "%s\n%s" $data . | trim }} +{{- end }} +{{- else }} +{{- with include "fl.value" (list $ . .data) }} +{{- $data = printf "%s\n%s" $data . | trim }} +{{- end }} +{{- end }} +{{ with $data }} data: -{{- include "apps.generateConfigMapEnvVars" (list $ . .envVars "envVars") | nindent 2 }} -{{- include "fl.value" (list $ . .data) | nindent 2 }} +{{- . | nindent 2 }} +{{- end }} +{{ with include "fl.value" (list $ . .binaryData) }} +binaryData: +{{- . | nindent 2 }} +{{- end }} {{- end }} {{- end }} diff --git a/charts/helm-apps/templates/_apps-cronjobs.tpl b/charts/helm-apps/templates/_apps-cronjobs.tpl index 80e83bf..b86efc4 100644 --- a/charts/helm-apps/templates/_apps-cronjobs.tpl +++ b/charts/helm-apps/templates/_apps-cronjobs.tpl @@ -15,11 +15,7 @@ {{- if not .containers }} {{- fail (printf "Установлено значение enabled для не настроенной '%s' в %s джобы!" $.CurrentApp.name "apps-cronjobs") }} {{- end }} -{{- if semverCompare ">=1.21-0" $.Capabilities.KubeVersion.GitVersion }} -apiVersion: batch/v1 -{{- else }} -apiVersion: batch/v1beta1 -{{- end }} +apiVersion: {{ include "apps-api-versions.cronJob" $ }} kind: CronJob {{- include "apps-helpers.metadataGenerator" (list $ .) }} spec: @@ -28,6 +24,9 @@ spec: {{- $_ = set $specs "Numbers" (list "failedJobsHistoryLimit" "startingDeadlineSeconds" "successfulJobsHistoryLimit") }} {{- $_ = set $specs "Bools" (list "suspend") }} {{- include "apps-utils.generateSpecs" (list $ . $specs) | indent 2 }} + {{- with include "apps-compat.renderRaw" (list $ . .extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} jobTemplate: {{ include "apps-helpers.jobTemplate" (list $ .) | nindent 4 -}} {{- include "apps-components.generateConfigMapsAndSecrets" $ -}} diff --git a/charts/helm-apps/templates/_apps-default-values.yaml b/charts/helm-apps/templates/_apps-default-values.yaml index d13de04..2cf353f 100644 --- a/charts/helm-apps/templates/_apps-default-values.yaml +++ b/charts/helm-apps/templates/_apps-default-values.yaml @@ -163,4 +163,11 @@ global: enabled: true apps-configmaps-defaultConfigmap: _include: ["apps-default-library-app"] + apps-network-policies-defaultNetworkPolicy: + # CNI-agnostic baseline: use standard Kubernetes NetworkPolicy spec only. + type: kubernetes + policyTypes: | + - Ingress + ingress: | + - {} {{- end }} diff --git a/charts/helm-apps/templates/_apps-fl-wrappers.tpl b/charts/helm-apps/templates/_apps-fl-wrappers.tpl index a23b45a..17dc8ec 100644 --- a/charts/helm-apps/templates/_apps-fl-wrappers.tpl +++ b/charts/helm-apps/templates/_apps-fl-wrappers.tpl @@ -14,11 +14,18 @@ {{- define "apps.generateConfigMapEnvVars" }} {{- $ := index . 0 }} -{{- include "apps-utils.enterScope" (list $ "secretEnvVars") }} +{{- include "apps-utils.enterScope" (list $ "EnvVars") }} {{- include "fl.generateConfigMapEnvVars" . }} {{- include "apps-utils.leaveScope" $ }} {{- end }} +{{- define "apps.generateConfigMapData" }} +{{- $ := index . 0 }} +{{- include "apps-utils.enterScope" (list $ "data") }} +{{- include "fl.generateConfigMapData" . }} +{{- include "apps-utils.leaveScope" $ }} +{{- end }} + {{- define "apps.value" }} {{- $ := index . 0 }} {{- include "apps-utils.enterScope" (list $ (last .)) }} diff --git a/charts/helm-apps/templates/_apps-helpers.tpl b/charts/helm-apps/templates/_apps-helpers.tpl index a327268..41b15f3 100644 --- a/charts/helm-apps/templates/_apps-helpers.tpl +++ b/charts/helm-apps/templates/_apps-helpers.tpl @@ -19,9 +19,24 @@ - name: {{ print "config-" $containersType "-" $.CurrentApp.name "-" $.CurrentContainer.name "-" $configFileName | include "fl.formatStringAsDNSLabel" | quote }} configMap: name: {{ .name | quote }} - {{- with include "fl.value" (list $ . .defaultMode) }} + {{- with include "fl.value" (list $ . .defaultMode) }} defaultMode: {{ . }} - {{- end }} + {{- end }} + {{- end }} + {{- range $configFileName, $_ := .configFilesYAML }} + {{- if kindIs "map" .content }} + {{- $_ := set . "name" (print "config-yaml-" $containersType "-" $.CurrentApp.name "-" $.CurrentContainer.name "-" $configFileName | include "fl.formatStringAsDNSLabel") }} + {{- else }} + {{- if not ( include "fl.value" (list $ . .name)) }} + {{- fail (printf "Для app '%s' %s '%s' в configFiles '%s' нет content и забыли указать .name" $.CurrentApp.name $containersType $.CurrentContainer.name $configFileName) }} + {{- end }} + {{- end }} +- name: {{ print "config-yaml-" $containersType "-" $.CurrentApp.name "-" $.CurrentContainer.name "-" $configFileName | include "fl.formatStringAsDNSLabel" | quote }} + configMap: + name: {{ .name | quote }} + {{- with include "fl.value" (list $ . .defaultMode) }} + defaultMode: {{ . }} + {{- end }} {{- end }} {{- /* Mount Secrets created by "secretConfigFiles:" option as volumes */ -}} {{- range $secretConfigFileName, $_ := .secretConfigFiles }} @@ -52,8 +67,14 @@ subPath: {{ $configFileName | quote }} mountPath: {{ include "fl.valueQuoted" (list $ . .mountPath) }} {{- end }} - {{- end -}} - + {{- end }} + {{- range $configFileName, $configFile := $.CurrentContainer.configFilesYAML }} + {{- if or (kindIs "map" .content) (include "fl.value" (list $ . .name)) }} +- name: {{ print "config-yaml-" $.CurrentApp._currentContainersType "-" $.CurrentApp.name "-" $.CurrentContainer.name "-" $configFileName | include "fl.formatStringAsDNSLabel" | quote }} + subPath: {{ $configFileName | quote }} + mountPath: {{ include "fl.valueQuoted" (list $ . .mountPath) }} + {{- end }} + {{- end }} {{- /* Mount secret files from ConfigMaps created by "secretConfigFiles:" option */ -}} {{- range $secretConfigFileName, $secretConfigFile := $.CurrentContainer.secretConfigFiles }} - name: {{ print "config-" $.CurrentApp._currentContainersType "-" $.CurrentApp.name "-" $.CurrentContainer.name "-" $secretConfigFileName | include "fl.formatStringAsDNSLabel" | quote }} @@ -109,6 +130,9 @@ {{- $_ = set $specsContainers "Strings" (list "imagePullPolicy" "terminationMessagePath" "terminationMessagePolicy" "workingDir") }} {{- $_ = set $specsContainers "Bools" (list "stdin" "stdinOnce" "tty" "workingDir" ) }} {{- include "apps-utils.generateSpecs" (list $ . $specsContainers) | trim | nindent 2 }} + {{- with include "apps-compat.renderRaw" (list $ . .extraFields) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- end }} {{- include "apps-utils.leaveScope" $ }} {{- end }} @@ -137,6 +161,9 @@ spec: {{- $_ = set $specsTemplate "Numbers" (list "activeDeadlineSeconds" "priority" "terminationGracePeriodSeconds") }} {{- $_ = set $specsTemplate "Bools" (list "automountServiceAccountToken" "enableServiceLinks" "hostIPC" "hostNetwork" "hostPID" "setHostnameAsFQDN" "shareProcessNamespace") }} {{- include "apps-utils.generateSpecs" (list $ . $specsTemplate) | trim | nindent 2 }} + {{- with include "apps-compat.renderRaw" (list $ . .podSpecExtra) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- $_ := set . "__specName__" "template"}} {{- end }} {{- end -}} @@ -188,34 +215,6 @@ spec: {{- end }} {{- end }} -{{- define "apps-helpers.generateEnvYAML" }} - {{- $ := . }} - {{- $envs := $.CurrentEnvYAML.envs }} - {{- range $CurrentEnvKey, $CurrentEnvDict := $envs }} - {{- include "apps-utils.enterScope" (list $ $CurrentEnvKey) }} - {{- if kindIs "map" $CurrentEnvDict }} - {{- if hasKey $CurrentEnvDict "_default" }} - {{- if not (kindIs "map" $.CurrentContainer.envVars) }} - {{- $_ := set $.CurrentContainer "envVars" dict }} - {{- end }} - {{- $envName := slice $.CurrentPath $.CurrentEnvYAML.startPathLength | join "_" | upper }} - {{- if hasKey $CurrentEnvDict "name" }} - {{- $envName = $CurrentEnvDict.name }} - {{- end }} - {{- if hasKey $.CurrentContainer.envVars $envName }} - {{- $_ := set $.CurrentContainer.envVars $envName (mergeOverwrite $CurrentEnvDict (index $.CurrentContainer.envVars $envName)) }} - {{- else }} - {{- $_ := set $.CurrentContainer.envVars $envName $CurrentEnvDict }} - {{- end }} - {{- end }} - {{- $_ := set $.CurrentEnvYAML "envs" $CurrentEnvDict }} - {{- include "apps-helpers.generateEnvYAML" $ }} - {{- else }} - {{- end }} - {{- include "apps-utils.leaveScope" $ }} - {{- end }} -{{- end }} - {{- define "apps-helpers.jobTemplate" }} {{- $ := index . 0 }} {{- $RelatedScope := index . 1 }} @@ -227,6 +226,9 @@ spec: {{- $_ = set $specs "Numbers" (list "activeDeadlineSeconds" "backoffLimit" "completions" "parallelism" "ttlSecondsAfterFinished") }} {{- $_ = set $specs "Bools" (list "manualSelector" "suspend") }} {{ include "apps-utils.generateSpecs" (list $ . $specs) | trim | indent 2 }} + {{- with include "apps-compat.renderRaw" (list $ . .jobTemplateExtraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} template: {{ include "apps-helpers.podTemplate" (list $ .) | trim | nindent 4 }} {{- end }} {{- end -}} @@ -243,6 +245,12 @@ spec: {{- if hasKey $.CurrentApp "werfWeight" }} {{- $_ := set $libAnnotations "werf.io/weight" (include "fl.value" (list $ . $.CurrentApp.werfWeight)) }} {{- end }} +{{- if hasKey $ "CurrentReleaseVersion" }} +{{- $_ := set $libAnnotations "helm-apps/release" (include "fl.value" (list $ . $.CurrentReleaseVersion)) }} +{{- end }} +{{- if hasKey $.CurrentApp "CurrentAppVersion" }} +{{- $_ := set $libAnnotations "helm-apps/app-version" (include "fl.value" (list $ . $.CurrentApp.CurrentAppVersion)) }} +{{- end }} {{- $libVersion := include "apps-version.getLibraryVersion" $ | trim }} {{- with $libVersion }} {{- if not (eq . "_FLANT_APPS_LIBRARY_VERSION_") }} @@ -320,3 +328,91 @@ metadata: {{- include "apps-utils.leaveScope" $ }} {{- end -}} +{{- define "apps-helpers.generateEnvYAML" }} + {{- $ := . }} + {{- $envs := $.CurrentEnvYAML.envs }} + {{- range $CurrentEnvKey, $CurrentEnvDict := $envs }} + {{- include "apps-utils.enterScope" (list $ $CurrentEnvKey) }} + {{- if kindIs "map" $CurrentEnvDict }} + {{- if hasKey $CurrentEnvDict "_default" }} + {{- if not (kindIs "map" $.CurrentContainer.envVars) }} + {{- $_ := set $.CurrentContainer "envVars" dict }} + {{- end }} + {{- $envName := slice $.CurrentPath $.CurrentEnvYAML.startPathLength | join "_" | upper }} + {{- if hasKey $CurrentEnvDict "name" }} + {{- $envName = $CurrentEnvDict.name }} + {{- end }} + {{- if hasKey $.CurrentContainer.envVars $envName }} + {{- $_ := set $.CurrentContainer.envVars $envName (mergeOverwrite $CurrentEnvDict (index $.CurrentContainer.envVars $envName)) }} + {{- else }} + {{- $_ := set $.CurrentContainer.envVars $envName $CurrentEnvDict }} + {{- end }} + {{- end }} + {{- $_ := set $.CurrentEnvYAML "envs" $CurrentEnvDict }} + {{- include "apps-helpers.generateEnvYAML" $ }} + {{- else }} + {{- end }} + {{- include "apps-utils.leaveScope" $ }} + {{- end }} +{{- end }} + +{{- define "apps-helpers.generateConfigYAML" }} + + {{- include "apps-helpers._generateConfigYAML" . }} + {{- $content := index . 2 }} + {{- $indicatorMap := dict "indicator" false }} + {{- include "apps-helpers._generateConfigYAML.clean" (list $content $indicatorMap) }} +{{- end }} +{{- define "apps-helpers._generateConfigYAML" }} + {{- $ := index . 0 }} + {{- $owner := index . 1 }} + {{- $content := index . 2 }} + {{- $contentName := index . 3 }} + {{- range $CurrentKey, $CurrentDict := $content }} + {{- include "apps-utils.enterScope" (list $ $CurrentKey) }} + {{- if kindIs "map" $CurrentDict }} + {{- if hasKey $CurrentDict "_default" }} + {{- $val := index $CurrentDict "_default" }} + {{- if hasKey $CurrentDict $.Values.global.env }} + {{- $val = index $CurrentDict $.Values.global.env }} + {{- end }} + {{- if kindIs "string" $val }} + {{- $_ := set $content $CurrentKey (include "fl.value" (list $ . $CurrentDict))}} + {{- else if kindIs "invalid" $val }} + {{- $_ := unset $content $CurrentKey }} + {{- if eq (len $owner) 0 }} + {{- $_ := unset $owner $contentName }} + {{- end }} + {{- else }} + {{- $_ := set $content $CurrentKey $val }} + {{- end }} + {{- else }} + {{- include "apps-helpers.generateConfigYAML" (list $ $content $CurrentDict $CurrentKey) }} + {{- end }} + {{- end }} + {{- include "apps-utils.leaveScope" $ }} + {{- end }} +{{- end }} +{{- define "apps-helpers._generateConfigYAML.clean" }} +{{- $content := index . 0 }} +{{- $indicatorMap := index . 1 }} +{{- range $CurrentKey, $CurrentDict := $content }} +{{- if kindIs "map" $CurrentDict }} +{{- if eq (len $CurrentDict) 0 }} +{{- $_ := set $indicatorMap "indicator" true }} +{{- $_ = unset $content $CurrentKey }} +{{- else }} +{{- $i := dict "indicator" true }} +{{- range $_,$_ := until 10 }} +{{- if $i.indicator }} +{{- $_ := set $i "indicator" false }} +{{- include "apps-helpers._generateConfigYAML.clean" (list $CurrentDict $i) }} +{{- if $i.indicator }} +{{- $_ := set $indicatorMap.indicator true }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/helm-apps/templates/_apps-ingresses.tpl b/charts/helm-apps/templates/_apps-ingresses.tpl index 415b522..9cfcea8 100644 --- a/charts/helm-apps/templates/_apps-ingresses.tpl +++ b/charts/helm-apps/templates/_apps-ingresses.tpl @@ -22,9 +22,11 @@ metadata: {{- with .dexAuth }} {{- if (include "fl.isTrue" (list $ $.CurrentApp .enabled)) }} {{- include "apps-utils.enterScope" (list $ "dexAuth") }} + {{- if $.Values.werf }} nginx.ingress.kubernetes.io/auth-signin: https://$host/dex-authenticator/sign_in nginx.ingress.kubernetes.io/auth-url: https://{{ $.CurrentApp.name }}-dex-authenticator.{{ $.Values.werf.namespace }}.svc.{{ include "apps-utils.requiredValue" (list $ . "clusterDomain") }}/dex-authenticator/auth nginx.ingress.kubernetes.io/auth-response-headers: X-Auth-Request-User,X-Auth-Request-Email,Authorization + {{- end }} {{- include "apps-utils.leaveScope" $ }} {{- end }} {{- end }} @@ -46,6 +48,9 @@ spec: - host: {{ include "fl.valueQuoted" (list $ . .host) }} http: paths: {{- include "fl.value" (list $ . .paths) | nindent 6 }} + {{- with include "apps-compat.renderRaw" (list $ . .extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- if .tls }} {{- if include "fl.isTrue" (list $ . .tls.enabled) }} {{- if not (include "fl.value" (list $ . .tls.secret_name)) }} diff --git a/charts/helm-apps/templates/_apps-jobs.tpl b/charts/helm-apps/templates/_apps-jobs.tpl index 45843bf..d66929c 100644 --- a/charts/helm-apps/templates/_apps-jobs.tpl +++ b/charts/helm-apps/templates/_apps-jobs.tpl @@ -16,6 +16,9 @@ {{- if not .containers }} {{- fail (printf "Установлено значение enabled для не настроенной '%s' в %s джобы!" $.CurrentApp.name "apps-jobs") }} {{- end }} +{{- if and (kindIs "invalid" .jobTemplateExtraSpec) (not (kindIs "invalid" .extraSpec)) }} +{{- $_ := set . "jobTemplateExtraSpec" .extraSpec }} +{{- end }} apiVersion: batch/v1 kind: Job {{- include "apps-helpers.metadataGenerator" (list $ .) -}} @@ -27,4 +30,4 @@ kind: Job {{- include "apps-components.verticalPodAutoscaler" (list $ . .verticalPodAutoscaler "Job") -}} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/helm-apps/templates/_apps-kafka-strimzi.tpl b/charts/helm-apps/templates/_apps-kafka-strimzi.tpl index f7badfe..9748689 100644 --- a/charts/helm-apps/templates/_apps-kafka-strimzi.tpl +++ b/charts/helm-apps/templates/_apps-kafka-strimzi.tpl @@ -146,7 +146,7 @@ spec: {{- include "kafka-topics" (list $ . .topics) }} --- -apiVersion: autoscaling.k8s.io/v1beta2 +apiVersion: {{ include "apps-api-versions.verticalPodAutoscaler" $ }} kind: VerticalPodAutoscaler metadata: name: {{ $.CurrentKafka.name }}-{{ $.Values.global.env }}-kafka @@ -159,7 +159,7 @@ spec: updateMode: "Off" --- -apiVersion: autoscaling.k8s.io/v1beta2 +apiVersion: {{ include "apps-api-versions.verticalPodAutoscaler" $ }} kind: VerticalPodAutoscaler metadata: name: {{ $.CurrentKafka.name }}-{{ $.Values.global.env }}-zookeeper @@ -172,7 +172,7 @@ spec: updateMode: "Off" --- -apiVersion: autoscaling.k8s.io/v1beta2 +apiVersion: {{ include "apps-api-versions.verticalPodAutoscaler" $ }} kind: VerticalPodAutoscaler metadata: name: {{ $.CurrentKafka.name }}-{{ $.Values.global.env }}-entity-operator @@ -185,7 +185,7 @@ spec: updateMode: "Off" --- -apiVersion: autoscaling.k8s.io/v1beta2 +apiVersion: {{ include "apps-api-versions.verticalPodAutoscaler" $ }} kind: VerticalPodAutoscaler metadata: name: {{ $.CurrentKafka.name }}-{{ $.Values.global.env }}-kafka-exporter @@ -223,4 +223,3 @@ spec: min.insync.replicas: {{ include "fl.value" (list $ . .min_insync_replicas) }} {{- end }} {{- end }} - diff --git a/charts/helm-apps/templates/_apps-network-policies.tpl b/charts/helm-apps/templates/_apps-network-policies.tpl new file mode 100644 index 0000000..f7d4a9e --- /dev/null +++ b/charts/helm-apps/templates/_apps-network-policies.tpl @@ -0,0 +1,159 @@ +{{- define "apps-network-policies" }} + {{- $ := index . 0 }} + {{- $RelatedScope := index . 1 }} + {{- if not (kindIs "invalid" $RelatedScope) }} + {{- $_ := set $RelatedScope "__GroupVars__" (dict "type" "apps-network-policies" "name" "apps-network-policies") }} + {{- include "apps-utils.renderApps" (list $ $RelatedScope) }} + {{- end -}} +{{- end -}} + +{{- define "apps-network-policies.render" }} +{{- $ := . }} +{{- with $.CurrentApp }} +{{- $strict := false }} +{{- with $.Values.global.validation }} +{{- if include "fl.isTrue" (list $ . .strict) }} +{{- $strict = true }} +{{- end }} +{{- end }} +{{- if $strict }} +{{- $allowedKeys := list +"_include" +"__AppType__" +"enabled" +"name" +"randomName" +"werfWeight" +"annotations" +"labels" +"_preRenderHook" +"type" +"apiVersion" +"kind" +"spec" +"podSelector" +"policyTypes" +"ingress" +"egress" +"endpointSelector" +"ingressDeny" +"egressDeny" +"selector" +"types" +"extraSpec" +}} +{{- include "apps-compat.enforceAllowedKeys" (list $ . $allowedKeys (printf "apps-network-policies.%s" $.CurrentApp.name)) }} +{{- end }} +{{- $type := include "fl.value" (list $ . .type) | default "kubernetes" }} +{{- $apiVersion := include "fl.value" (list $ . .apiVersion) }} +{{- $kind := include "fl.value" (list $ . .kind) }} +{{- if not $apiVersion }} + {{- if eq $type "kubernetes" }} + {{- $apiVersion = "networking.k8s.io/v1" }} + {{- else if eq $type "cilium" }} + {{- $apiVersion = "cilium.io/v2" }} + {{- else if eq $type "calico" }} + {{- $apiVersion = "projectcalico.org/v3" }} + {{- else }} + {{- fail (printf "apps-network-policies.%s: set apiVersion/kind for unsupported type=%s" $.CurrentApp.name $type) }} + {{- end }} +{{- end }} +{{- if not $kind }} + {{- if eq $type "kubernetes" }} + {{- $kind = "NetworkPolicy" }} + {{- else if eq $type "cilium" }} + {{- $kind = "CiliumNetworkPolicy" }} + {{- else if eq $type "calico" }} + {{- $kind = "NetworkPolicy" }} + {{- else }} + {{- fail (printf "apps-network-policies.%s: set apiVersion/kind for unsupported type=%s" $.CurrentApp.name $type) }} + {{- end }} +{{- end }} +apiVersion: {{ $apiVersion }} +kind: {{ $kind }} +{{- include "apps-helpers.metadataGenerator" (list $ .) }} +spec: + {{- if include "fl.value" (list $ . .spec) }} + {{- include "apps-compat.renderRaw" (list $ . .spec) | trim | nindent 2 }} + {{- else if eq $type "kubernetes" }} + {{- with include "fl.value" (list $ . .podSelector) | trim }} + podSelector: + {{- . | nindent 4 }} + {{- else }} + podSelector: + matchLabels: +{{- include "fl.generateSelectorLabels" (list $ . $.CurrentApp.name) | trim | nindent 6 }} + {{- end }} + {{- with include "fl.value" (list $ . .policyTypes) }} + policyTypes: + {{- . | nindent 2 }} + {{- end }} + {{- with include "fl.value" (list $ . .ingress) }} + ingress: + {{- . | nindent 2 }} + {{- end }} + {{- with include "fl.value" (list $ . .egress) }} + egress: + {{- . | nindent 2 }} + {{- end }} + {{- else if eq $type "cilium" }} + {{- with include "fl.value" (list $ . .endpointSelector) | trim }} + endpointSelector: + {{- . | nindent 4 }} + {{- else }} + endpointSelector: + matchLabels: +{{- include "fl.generateSelectorLabels" (list $ . $.CurrentApp.name) | trim | nindent 6 }} + {{- end }} + {{- with include "fl.value" (list $ . .ingress) }} + ingress: + {{- . | nindent 2 }} + {{- end }} + {{- with include "fl.value" (list $ . .egress) }} + egress: + {{- . | nindent 2 }} + {{- end }} + {{- with include "fl.value" (list $ . .ingressDeny) }} + ingressDeny: + {{- . | nindent 2 }} + {{- end }} + {{- with include "fl.value" (list $ . .egressDeny) }} + egressDeny: + {{- . | nindent 2 }} + {{- end }} + {{- else if eq $type "calico" }} + {{- with include "fl.value" (list $ . .selector) }} + selector: {{ . | quote }} + {{- else }} + selector: {{ print "app == '" $.CurrentApp.name "'" | quote }} + {{- end }} + {{- with include "fl.value" (list $ . .types) }} + types: + {{- . | nindent 2 }} + {{- else }} + {{- if or (include "fl.value" (list $ . .ingress)) (include "fl.value" (list $ . .egress)) }} + types: + {{- if include "fl.value" (list $ . .ingress) }} + - Ingress + {{- end }} + {{- if include "fl.value" (list $ . .egress) }} + - Egress + {{- end }} + {{- end }} + {{- end }} + {{- with include "fl.value" (list $ . .ingress) }} + ingress: + {{- . | nindent 2 }} + {{- end }} + {{- with include "fl.value" (list $ . .egress) }} + egress: + {{- . | nindent 2 }} + {{- end }} + {{- else }} + {{- fail (printf "apps-network-policies.%s: unsupported type=%s (use kubernetes|cilium|calico or set apiVersion/kind+spec)" $.CurrentApp.name $type) }} + {{- end }} + {{- with include "apps-compat.renderRaw" (list $ . .extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/helm-apps/templates/_apps-pvcs.tpl b/charts/helm-apps/templates/_apps-pvcs.tpl index 6933251..bce9ad4 100644 --- a/charts/helm-apps/templates/_apps-pvcs.tpl +++ b/charts/helm-apps/templates/_apps-pvcs.tpl @@ -19,5 +19,8 @@ spec: {{- $_ := set $specsPVCs "Maps" (list "resources" ) }} {{- $_ := set $specsPVCs "Strings" (list "storageClassName") }} {{- include "apps-utils.generateSpecs" (list $ . $specsPVCs) | indent 2 }} + {{- with include "apps-compat.renderRaw" (list $ . .extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/helm-apps/templates/_apps-release.tpl b/charts/helm-apps/templates/_apps-release.tpl new file mode 100644 index 0000000..1fc94fd --- /dev/null +++ b/charts/helm-apps/templates/_apps-release.tpl @@ -0,0 +1,43 @@ +{{- define "apps-release.prepareApp" -}} +{{- $ := . -}} +{{- $releaseCfg := dict -}} +{{- if and (hasKey $.Values "global") (kindIs "map" $.Values.global) (hasKey $.Values.global "release") (kindIs "map" $.Values.global.release) -}} + {{- $releaseCfg = $.Values.global.release -}} +{{- end -}} +{{- if kindIs "map" $releaseCfg -}} + {{- if and (hasKey $releaseCfg "enabled") (include "fl.isTrue" (list $ $.CurrentApp $releaseCfg.enabled)) -}} + {{- $currentRelease := include "fl.value" (list $ $.CurrentApp $releaseCfg.current) -}} + {{- if empty $currentRelease -}} + {{- fail "global.release.enabled=true requires global.release.current" -}} + {{- end -}} + {{- $_ := set $ "CurrentReleaseVersion" $currentRelease -}} + + {{- $versions := $releaseCfg.versions -}} + {{- if not (kindIs "map" $versions) -}} + {{- fail "global.release.enabled=true requires global.release.versions map" -}} + {{- end -}} + + {{- $releaseVersions := index $versions $currentRelease -}} + {{- if not $releaseVersions -}} + {{- fail (printf "Release not found in global.release.versions: %s" $currentRelease) -}} + {{- end -}} + + {{- $releaseKey := $.CurrentApp.name -}} + {{- if hasKey $.CurrentApp "releaseKey" -}} + {{- $releaseKey = include "fl.value" (list $ $.CurrentApp $.CurrentApp.releaseKey) -}} + {{- end -}} + + {{- $appVersion := index $releaseVersions $releaseKey -}} + {{- if $appVersion -}} + {{- $_ := set $.CurrentApp "CurrentAppVersion" (include "fl.value" (list $ $.CurrentApp $appVersion)) -}} + {{- $autoEnable := true -}} + {{- if hasKey $releaseCfg "autoEnableApps" -}} + {{- $autoEnable = include "fl.isTrue" (list $ $.CurrentApp $releaseCfg.autoEnableApps) -}} + {{- end -}} + {{- if $autoEnable -}} + {{- $_ := set $.CurrentApp "enabled" true -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- end -}} diff --git a/charts/helm-apps/templates/_apps-secrets.tpl b/charts/helm-apps/templates/_apps-secrets.tpl index e668c39..f80675f 100644 --- a/charts/helm-apps/templates/_apps-secrets.tpl +++ b/charts/helm-apps/templates/_apps-secrets.tpl @@ -14,6 +14,9 @@ apiVersion: v1 kind: Secret {{- include "apps-helpers.metadataGenerator" (list $ .) }} +{{- with include "apps-compat.renderRaw" (list $ . .extraFields) | trim }} +{{- . | nindent 0 }} +{{- end }} type: {{- include "fl.value" (list $ . .type) | default "Opaque" | nindent 2 }} data: {{- if (include "fl.value" (list $ . .data)) }} diff --git a/charts/helm-apps/templates/_apps-specs.tpl b/charts/helm-apps/templates/_apps-specs.tpl index a44ed95..23c0eef 100644 --- a/charts/helm-apps/templates/_apps-specs.tpl +++ b/charts/helm-apps/templates/_apps-specs.tpl @@ -12,8 +12,13 @@ {{- $ := index . 0 }} {{- $relativeScope := index . 1 }} {{- with $relativeScope }} -matchLabels: {{- include "fl.generateSelectorLabels" (list $ . .name) | nindent 2 }} -{{- $_ := set . "__specName__" "selector"}} +matchLabels: +{{- if empty (include "fl.value" (list $ . .selector)) }} +{{- include "fl.generateSelectorLabels" (list $ . .name) | nindent 2 }} +{{- else }} +{{- .selector | nindent 2}} +{{- end }} +{{- $_ := set . "__specName__" "selector" }} {{- end }} {{- end }} diff --git a/charts/helm-apps/templates/_apps-stateful.tpl b/charts/helm-apps/templates/_apps-stateful.tpl index 3635833..585fe5a 100644 --- a/charts/helm-apps/templates/_apps-stateful.tpl +++ b/charts/helm-apps/templates/_apps-stateful.tpl @@ -34,6 +34,7 @@ kind: StatefulSet {{- end }} {{- include "apps-helpers.metadataGenerator" (list $ .) }} spec: + {{- include "apps-compat.normalizeStatefulSetSpec" (list $ .) }} {{- /* https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#statefulset-v1-apps */ -}} {{- $specs := dict }} {{- $_ = set $specs "Maps" (list "apps-helpers.podTemplate" "apps-specs.selector" "persistentVolumeClaimRetentionPolicy" "updateStrategy") }} @@ -41,6 +42,9 @@ spec: {{- $_ = set $specs "Strings" (list "apps-specs.serviceName" "podManagementPolicy") }} {{- $_ = set $specs "Lists" (list "apps-specs.volumeClaimTemplates") }} {{- include "apps-utils.generateSpecs" (list $ . $specs) | nindent 2 }} + {{- with include "apps-compat.renderRaw" (list $ . .extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- $_ = unset . "__annotations__" -}} {{- include "apps-components.generateConfigMapsAndSecrets" $ -}} @@ -56,4 +60,4 @@ spec: {{ $serviceAccount -}} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/helm-apps/templates/_apps-stateless.tpl b/charts/helm-apps/templates/_apps-stateless.tpl index cf6ba34..e0d39b2 100644 --- a/charts/helm-apps/templates/_apps-stateless.tpl +++ b/charts/helm-apps/templates/_apps-stateless.tpl @@ -37,6 +37,9 @@ spec: {{- $_ = set $specs "Numbers" (list "minReadySeconds" "progressDeadlineSeconds" "revisionHistoryLimit" "replicas") }} {{- $_ = set $specs "Maps" (list "strategy" "apps-helpers.podTemplate" "apps-specs.selector") }} {{- include "apps-utils.generateSpecs" (list $ . $specs) | indent 2 }} + {{- with include "apps-compat.renderRaw" (list $ . .extraSpec) | trim }} + {{- . | nindent 2 }} + {{- end }} {{- $_ = unset . "__annotations__" }} {{- include "apps-components.generateConfigMapsAndSecrets" $ -}} diff --git a/charts/helm-apps/templates/_apps-system.tpl b/charts/helm-apps/templates/_apps-system.tpl index 181bb55..ef02132 100644 --- a/charts/helm-apps/templates/_apps-system.tpl +++ b/charts/helm-apps/templates/_apps-system.tpl @@ -32,7 +32,7 @@ roleRef: subjects: - kind: ServiceAccount name: {{ $serviceAccountName }} - namespace: {{ $.Values.werf.namespace }} + namespace: {{ $.Release.Namespace }} {{- include "apps-utils.leaveScope" $ }} {{- end }} {{- include "apps-utils.leaveScope" $ }} diff --git a/charts/helm-apps/templates/_apps-utils.tpl b/charts/helm-apps/templates/_apps-utils.tpl index 0fdb9e4..179f576 100644 --- a/charts/helm-apps/templates/_apps-utils.tpl +++ b/charts/helm-apps/templates/_apps-utils.tpl @@ -18,8 +18,12 @@ {{- $imageName := include "fl.value" (list $ . $imageConfig.name) }} {{- if include "fl.value" (list $ . $imageConfig.staticTag) }} {{- $imageName }}:{{ include "fl.value" (list $ . $imageConfig.staticTag) }} -{{- else -}} -{{- index $.Values.werf.image $imageName }} +{{- else if hasKey $.CurrentApp "CurrentAppVersion" }} +{{- $imageName }}:{{ include "fl.value" (list $ . $.CurrentApp.CurrentAppVersion) }} +{{- else -}} +{{- with $.Values.werf }} +{{- index .image $imageName }} +{{- end }} {{- end }} {{- end -}} @@ -39,9 +43,11 @@ {{ $relativeScope.__specName__ }}: {{ print . | nindent 0 }} {{- end }} {{- else }} +{{- if hasKey $relativeScope $specName }} {{- with include "fl.value" (list $ $relativeScope (index $relativeScope .)) | trim }} {{ $specName }}: {{ print . | trim | nindent 0 }} {{- end }} +{{- end }} {{- end }} {{- end }} {{- end }} @@ -52,10 +58,12 @@ {{ $relativeScope.__specName__ }}: {{ print . | nindent 2 }} {{- end }} {{- else }} +{{- if hasKey $relativeScope $specName }} {{- with include "fl.value" (list $ $relativeScope (index $relativeScope .)) | trim }} {{ $specName }}: {{ print . | nindent 2 }} {{- end }} {{- end }} +{{- end }} {{- end }} {{- end }} {{- with $specs.Strings }} @@ -65,10 +73,12 @@ {{ $relativeScope.__specName__ }}: {{ print . | quote }} {{- end }} {{- else }} +{{- if hasKey $relativeScope $specName }} {{- with include "fl.valueQuoted" (list $ $relativeScope (index $relativeScope .)) }} {{ $specName }}: {{ . }} {{- end }} {{- end }} +{{- end }} {{- end }} {{- end }} {{- with $specs.Numbers }} @@ -78,10 +88,12 @@ {{ $relativeScope.__specName__ }}: {{ print . }} {{- end }} {{- else }} +{{- if hasKey $relativeScope $specName }} {{- with include "fl.value" (list $ $relativeScope (index $relativeScope .)) }} {{ $specName }}: {{ . }} {{- end }} {{- end }} +{{- end }} {{- end }} {{- end }} {{- with $specs.Bools }} @@ -92,7 +104,7 @@ {{ $relativeScope.__specName__ }}: {{ print $specValue }} {{- end }} {{- else }} -{{- if ne (include "fl.value" (list $ $relativeScope (index $relativeScope .))) "" }} +{{- if and (hasKey $relativeScope $specName) (ne (include "fl.value" (list $ $relativeScope (index $relativeScope .))) "") }} {{ $specName }}: {{ include "fl.isTrue" (list $ $relativeScope (index $relativeScope .)) }} {{- end }} {{- end }} @@ -120,6 +132,7 @@ {{- end }} {{- define "apps-utils.preRenderHooks" }} {{- $ := . }} +{{- include "apps-release.prepareApp" $ }} {{- if hasKey $ "CurrentGroupVars" }} {{- if hasKey $.CurrentGroupVars "_preRenderAppHook" }} {{- $_ := include "fl.value" (list $ $.CurrentApp $.CurrentGroupVars._preRenderAppHook) }} @@ -187,6 +200,8 @@ {{- if not (kindIs "invalid" $.CurrentGroupVars) }} {{- $_ = set $groupScope.__GroupVars__ "type" (include "apps-utils.requiredValue" (list $ $.CurrentGroupVars "type")) }} {{- end }} +{{- else }} +{{- $_ = set $groupScope.__GroupVars__ "type" (include "fl.value" (list $ $groupScope $groupScope.__GroupVars__.type)) }} {{- end }} {{- $_ = set $ "CurrentGroupVars" $groupScope.__GroupVars__ }} {{- $_ = set $ "CurrentGroup" $groupScope }} @@ -200,7 +215,9 @@ {{- define "apps-utils.init-library" }} {{- $ := . }} +{{- include "apps-utils.includesFromFiles" $ }} {{- $_ := include "fl.expandIncludesInValues" (list $ $.Values) }} +{{- include "apps-compat.assertNoUnexpectedLists" (list $ $.Values (list "Values")) }} {{- include "apps-utils.findApps" $ }} --- # Source: apps.utils: fl.expandIncludesInValues @@ -222,7 +239,23 @@ "pvcs" "certificates" "services" +"network-policies" }} +{{- $knownTopLevel := list "global" "enabled" "_include" "werf" "helm-apps" }} +{{- range $app := $Library }} +{{- $knownTopLevel = append $knownTopLevel (printf "apps-%s" $app) }} +{{- end }} +{{- $strict := false }} +{{- with $.Values.global }} +{{- with .validation }} +{{- if include "fl.isTrue" (list $ . .strict) }} +{{- $strict = true }} +{{- end }} +{{- end }} +{{- end }} +{{- if $strict }} +{{- include "apps-compat.validateTopLevelStrict" (list $ $.Values $knownTopLevel) }} +{{- end }} {{- range $app := $Library }} {{- include (printf "apps-%s" $app) (list $ (index $.Values (printf "apps-%s" $app))) }} {{- end }} @@ -252,3 +285,53 @@ {{- define "apps-utils.printPath" }} {{- printf "\n---\n# Helm Apps Library: %s" (.CurrentPath | join ".") }} {{- end }} + +{{- define "apps-utils.includesFromFiles" }} +{{- $_ := set $ "HelmAppsArgs" (dict "owner" . "current" .Values "currentName" "Values")}} +{{- include "apps-utils._includesFromFiles" (list . . .Values "Values") }} +{{- end }} + +{{- define "apps-utils._includesFromFiles" }} +{{- $ := index . 0 }} +{{- $owner := index . 1 }} +{{- $current := index . 2 }} +{{- $currentName := index . 3 }} +{{- if kindIs "map" $current }} +{{- if hasKey $current "_include_from_file" }} +{{- $fn := include "apps-utils.tpl" (list $ $current._include_from_file) }} +{{- $includeContent := $.Files.Get $fn | fromYaml }} +{{- $_ := required (printf "Including file %s in _include_from_file emtty or has errors!" $fn) $includeContent }} +{{- $currentDict := deepCopy $current}} +{{- $_ = mergeOverwrite $includeContent $currentDict }} +{{- $_ = mergeOverwrite $current $includeContent }} +{{- $_ = unset $current "_include_from_file"}} +{{- end }} +{{- if hasKey $current "_include_files" }} +{{- $newInclude := list }} +{{- range $_, $fileName := $current._include_files }} +{{- $fn := include "apps-utils.tpl" (list $ $fileName) }} +{{- $includeContent := $.Files.Get $fn | fromYaml }} +{{- $_ := required (printf "Including file %s in _include_files emtty or has errors!" $fn) $includeContent }} +{{- $includeName := sha256sum $fileName }} +{{- $_ = set $.Values.global._includes $includeName $includeContent }} +{{- $newInclude = append $newInclude $includeName }} +{{- end }} +{{- if hasKey $current "_include" }} +{{- $newInclude = concat $newInclude $current._include }} +{{- end }} +{{- $_ := set $current "_include" $newInclude }} +{{- $_ = unset $current "_include_files"}} +{{- end }} +{{- range $k, $v := $current }} +{{- if kindIs "map" $v }} +{{- include "apps-utils._includesFromFiles" (list $ $current $v $k) }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "apps-utils.tpl" }} +{{- $ := index . 0 }} +{{- $value := index . 1 }} +{{- tpl $value $ }} +{{- end }} diff --git a/charts/helm-apps/templates/fl-functions/_expandIncludesInValues.tpl b/charts/helm-apps/templates/fl-functions/_expandIncludesInValues.tpl index e2e6cd5..5679a51 100644 --- a/charts/helm-apps/templates/fl-functions/_expandIncludesInValues.tpl +++ b/charts/helm-apps/templates/fl-functions/_expandIncludesInValues.tpl @@ -12,63 +12,7 @@ {{- end }} {{- end }} -{{- define "_fl.make_includes_from" }} -{{- $ := index . 0 }} -{{- $prevContext := index . 1 }} -{{- $curContext := index . 2 }} -{{- $varName := index . 3 }} -{{- if kindIs "map" $curContext }} -{{- if hasKey $curContext "_include_from" }} -{{- $includeVar := "" }} -{{- $excludeParam := list }} -{{- $includeParams := $curContext._include_from }} -{{- if kindIs "map" $includeParams }} -{{- $excludeParam = $curContext._include_from.exclude }} -{{- $includeVar = $curContext._include_from.path }} -{{- else }} -{{- $includeVar = $curContext._include_from }} -{{- end }} -{{- $tmpMap := include "_getMapKeyValue" (list $.Values $includeVar) | fromJson }} -{{- if gt (len $excludeParam) 0 }} -{{- range $e := $excludeParam }} -{{- $_ := unset $tmpMap $e }} -{{- end }} -{{- end }} -{{- $curContext := mergeOverwrite $curContext $tmpMap }} -{{- $_ := set $prevContext $varName $curContext }} -{{- $_ = unset $curContext "_include_from" }} -{{- end }} -{{- range $key,$varsDict := $curContext -}} -{{- if kindIs "map" $varsDict -}} -{{- if gt (len $varsDict) 0 -}} -{{- include "_fl.make_includes_from" (list $ $curContext $varsDict $key) -}} -{{- end -}} -{{- end -}} -{{- end }} -{{- end }} -{{- end }} -{{- define "_getMapKeyValue" }} -{{- $map := index . 0 }} -{{- $path := index . 1 }} -{{- $tmpMap := $map }} -{{- if contains "." $path }} -{{- $keys := regexSplit "\\." $path -1 }} -{{- range $k := $keys }} -{{ if kindIs "map" $tmpMap }} -{{- $tmpValue := get $tmpMap $k }} -{{- if kindIs "map" $tmpValue }} -{{ $tmpMap = $tmpValue }} -{{- else }} -{{ fail $k }} -{{- end }} -{{- end }} -{{- end }} -{{- else }} -{{- $tmpMap = get $tmpMap $path }} -{{- end }} -{{- toJson $tmpMap }} -{{- end }} {{- define "fl._recursiveMergeAndExpandIncludes" }} {{- $ := index . 0 }} @@ -109,6 +53,9 @@ {{- define "fl._recursiveMapsMerge" }} {{- $ := index . 0 }} {{- $mapToMergeFrom := index . 1 }} + {{- if kindIs "map" $mapToMergeFrom }} + {{- $mapToMergeFrom = deepCopy $mapToMergeFrom }} + {{- end }} {{- $mapToMergeInto := index . 2 }} {{- range $keyToMergeFrom, $valToMergeFrom := $mapToMergeFrom }} @@ -116,7 +63,9 @@ {{- if kindIs "map" $valToMergeFrom }} {{- if kindIs "map" $valToMergeInto }} + {{- if not (hasKey $mapToMergeFrom "_default") }} {{- include "fl._recursiveMapsMerge" (list $ $valToMergeFrom $valToMergeInto) }} + {{- end }} {{- else if not (hasKey $mapToMergeInto $keyToMergeFrom) }} {{- $_ := set $mapToMergeInto $keyToMergeFrom $valToMergeFrom }} {{- end }} @@ -152,3 +101,62 @@ {{- end }} {{- dict "wrapper" $result | toJson }} {{- end }} + + +{{- define "_fl.make_includes_from" }} +{{- $ := index . 0 }} +{{- $prevContext := index . 1 }} +{{- $curContext := index . 2 }} +{{- $varName := index . 3 }} +{{- if kindIs "map" $curContext }} +{{- if hasKey $curContext "_include_from" }} +{{- $includeVar := "" }} +{{- $excludeParam := list }} +{{- $includeParams := $curContext._include_from }} +{{- if kindIs "map" $includeParams }} +{{- $excludeParam = $curContext._include_from.exclude }} +{{- $includeVar = $curContext._include_from.path }} +{{- else }} +{{- $includeVar = $curContext._include_from }} +{{- end }} +{{- $tmpMap := include "_getMapKeyValue" (list $.Values $includeVar) | fromJson }} +{{- if gt (len $excludeParam) 0 }} +{{- range $e := $excludeParam }} +{{- $_ := unset $tmpMap $e }} +{{- end }} +{{- end }} +{{- $curContext := mergeOverwrite $curContext $tmpMap }} +{{- $_ := set $prevContext $varName $curContext }} +{{- $_ = unset $curContext "_include_from" }} +{{- end }} +{{- range $key,$varsDict := $curContext -}} +{{- if kindIs "map" $varsDict -}} +{{- if gt (len $varsDict) 0 -}} +{{- include "_fl.make_includes_from" (list $ $curContext $varsDict $key) -}} +{{- end -}} +{{- end -}} +{{- end }} +{{- end }} +{{- end }} + +{{- define "_getMapKeyValue" }} +{{- $map := index . 0 }} +{{- $path := index . 1 }} +{{- $tmpMap := $map }} +{{- if contains "." $path }} +{{- $keys := regexSplit "\\." $path -1 }} +{{- range $k := $keys }} +{{ if kindIs "map" $tmpMap }} +{{- $tmpValue := get $tmpMap $k }} +{{- if kindIs "map" $tmpValue }} +{{ $tmpMap = $tmpValue }} +{{- else }} +{{ fail $k }} +{{- end }} +{{- end }} +{{- end }} +{{- else }} +{{- $tmpMap = get $tmpMap $path }} +{{- end }} +{{- toJson $tmpMap }} +{{- end }} \ No newline at end of file diff --git a/charts/helm-apps/templates/fl-snippets/_generateConfigMapData.tpl b/charts/helm-apps/templates/fl-snippets/_generateConfigMapData.tpl new file mode 100644 index 0000000..e433e58 --- /dev/null +++ b/charts/helm-apps/templates/fl-snippets/_generateConfigMapData.tpl @@ -0,0 +1,21 @@ +{{- define "fl.generateConfigMapData" }} + {{- $ := index . 0 }} + {{- $relativeScope := index . 1 }} + {{- $data := index . 2 }} + {{- $upper := false }} + {{- if gt (len .) 3 }} + {{- $upper = true }} + {{- end -}} + + {{- range $key, $value := $data }} + {{- if $upper }} + {{- $key = upper $key }} + {{- end }} + {{- $value = include "apps.value" (list $ $relativeScope $value $key) }} + {{- if eq $value "___FL_THIS_ENV_VAR_WILL_BE_DEFINED_BUT_EMPTY___" }} +{{ $key | quote }}: "" + {{- else if ne $value "" }} +{{ $key | quote }}: {{ $value | quote }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/helm-apps/templates/fl-snippets/_generateConfigMapEnvVars.tpl b/charts/helm-apps/templates/fl-snippets/_generateConfigMapEnvVars.tpl index 290f262..19bdfb3 100644 --- a/charts/helm-apps/templates/fl-snippets/_generateConfigMapEnvVars.tpl +++ b/charts/helm-apps/templates/fl-snippets/_generateConfigMapEnvVars.tpl @@ -1,17 +1,8 @@ {{- define "fl.generateConfigMapEnvVars" }} - {{- $ := index . 0 }} - {{- $relativeScope := index . 1 }} - {{- $envs := index . 2 }} - - {{- range $envVarName, $envVarVal := $envs }} - {{- if $.Values.global.configFlantLibVariableUppercaseEnvs }} - {{- $envVarName = upper $envVarName }} - {{- end }} - {{- $envVarVal = include "apps.value" (list $ $relativeScope $envVarVal $envVarName) }} - {{- if eq $envVarVal "___FL_THIS_ENV_VAR_WILL_BE_DEFINED_BUT_EMPTY___" }} -{{ $envVarName | quote }}: "" - {{- else if ne $envVarVal "" }} -{{ $envVarName | quote }}: {{ $envVarVal | quote }} - {{- end }} - {{- end }} +{{- $ := index . 0 }} +{{- if $.Values.global.configFlantLibVariableUppercaseEnvs }} +{{- include "fl.generateConfigMapData" (append . true) }} +{{- else }} +{{- include "fl.generateConfigMapData" . }} +{{- end }} {{- end }} diff --git a/charts/helm-apps/templates/fl-snippets/_generateContainerImage.tpl b/charts/helm-apps/templates/fl-snippets/_generateContainerImage.tpl index 9960f7d..265f724 100644 --- a/charts/helm-apps/templates/fl-snippets/_generateContainerImage.tpl +++ b/charts/helm-apps/templates/fl-snippets/_generateContainerImage.tpl @@ -7,6 +7,8 @@ {{- if include "fl.value" (list $ . $imageConfig.staticTag) }} {{- $imageName }}:{{ include "fl.value" (list $ . $imageConfig.staticTag) }} {{- else -}} - {{- index $.Values.werf.image $imageName }} + {{- with $.Values.werf }} + {{- index .image $imageName }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/helm-apps/templates/fl-snippets/_generateLabels.tpl b/charts/helm-apps/templates/fl-snippets/_generateLabels.tpl index 4519395..50737e1 100644 --- a/charts/helm-apps/templates/fl-snippets/_generateLabels.tpl +++ b/charts/helm-apps/templates/fl-snippets/_generateLabels.tpl @@ -4,5 +4,7 @@ {{- $appName := index . 2 }} app: {{ $appName | quote }} chart: {{ $.Chart.Name | trunc 63 | quote }} -repo: {{ regexSplit "/" $.Values.werf.repo -1 | rest | join "-" | trunc 63 | quote }} +{{- with $.Values.werf }} +repo: {{ regexSplit "/" .repo -1 | rest | join "-" | trunc 63 | quote }} +{{- end }} {{- end }} diff --git a/charts/helm-apps/templates/fl-snippets/_generateSecretData.tpl b/charts/helm-apps/templates/fl-snippets/_generateSecretData.tpl index b1c3fbf..f561b90 100644 --- a/charts/helm-apps/templates/fl-snippets/_generateSecretData.tpl +++ b/charts/helm-apps/templates/fl-snippets/_generateSecretData.tpl @@ -2,9 +2,13 @@ {{- $ := index . 0 }} {{- $relativeScope := index . 1 }} {{- $data := index . 2 }} + {{- $upper := false }} + {{- if gt (len .) 3 }} + {{- $upper = true }} + {{- end -}} {{- range $key, $value := $data }} - {{- if $.Values.global.configFlantLibVariableUppercaseEnvs }} + {{- if $upper }} {{- $key = upper $key }} {{- end }} {{- $value = include "apps.value" (list $ $relativeScope $value $key) }} @@ -14,4 +18,4 @@ {{ $key | quote }}: {{ $value | b64enc | quote }} {{- end }} {{- end }} -{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/helm-apps/templates/fl-snippets/_generateSecretEnvVars.tpl b/charts/helm-apps/templates/fl-snippets/_generateSecretEnvVars.tpl index 9b4399f..09bc409 100644 --- a/charts/helm-apps/templates/fl-snippets/_generateSecretEnvVars.tpl +++ b/charts/helm-apps/templates/fl-snippets/_generateSecretEnvVars.tpl @@ -1,3 +1,8 @@ {{- define "fl.generateSecretEnvVars" }} +{{- $ := index . 0 }} +{{- if $.Values.global.configFlantLibVariableUppercaseEnvs }} +{{- include "fl.generateSecretData" (append . true) }} +{{- else }} {{- include "fl.generateSecretData" . }} {{- end }} +{{- end }} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d90facf --- /dev/null +++ b/docs/README.md @@ -0,0 +1,64 @@ +# Документация Helm Apps Library + +Этот файл — точка входа в документацию. +Если открываете docs впервые, начните отсюда. + +Примечание: библиотека полностью поддерживает `Helm` и совместима с `werf`. +На практике `werf` часто удобнее для продуктовых команд, потому что он объединяет рендер и delivery-процесс в один workflow. +При этом все сценарии библиотеки доступны через чистый `Helm`. + +## Быстрый маршрут (15 минут) + +1. Прочитать концепцию и зачем библиотека нужна: [docs/library-guide.md](library-guide.md) +2. Взять готовый шаблон под свой сценарий: [docs/cookbook.md](cookbook.md) +3. Сверить поля и типы перед merge: [docs/reference-values.md](reference-values.md) +4. Быстро найти нужный параметр и пример: [docs/parameter-index.md](parameter-index.md) +5. Найти решение по задаче: [docs/use-case-map.md](use-case-map.md) +6. Проверить values по schema: [tests/.helm/values.schema.json](../tests/.helm/values.schema.json) +7. Сравнить с рабочими примерами: [tests/.helm/values.yaml](../tests/.helm/values.yaml) + +## Как читать документацию по роли + +### Разработчик сервиса + +1. `docs/cookbook.md` +2. `docs/reference-values.md` +3. `docs/parameter-index.md` (быстрый переход по параметрам) +4. `docs/use-case-map.md` (карта решений по задачам) +5. `docs/operations.md` (разделы triage и частые ошибки) + +### DevOps / Platform Engineer + +1. `docs/library-guide.md` +2. `docs/reference-values.md` +3. `docs/parameter-index.md` +4. `docs/use-case-map.md` +5. `docs/operations.md` + +### Ревьюер MR с изменениями `.helm/values.yaml` + +1. `docs/reference-values.md` +2. `docs/parameter-index.md` +3. `docs/use-case-map.md` +4. `docs/operations.md` (чеклисты merge/release) +5. `tests/.helm/values.schema.json` + +## Карта документов + +- Архитектура и принципы: [docs/library-guide.md](library-guide.md) +- Полный справочник полей: [docs/reference-values.md](reference-values.md) +- Индекс параметров с примерами: [docs/parameter-index.md](parameter-index.md) +- Карта use-cases: [docs/use-case-map.md](use-case-map.md) +- Готовые практические рецепты: [docs/cookbook.md](cookbook.md) +- Эксплуатация, triage, rollback: [docs/operations.md](operations.md) +- Краткие правила по helper-паттернам: [docs/usage.md](usage.md) +- Полный рабочий пример values: [tests/.helm/values.yaml](../tests/.helm/values.yaml) +- Schema валидации values: [tests/.helm/values.schema.json](../tests/.helm/values.schema.json) + +## Минимальный командный чеклист + +```bash +helm dependency update .helm +helm lint .helm +helm template my-app .helm --set global.env=prod +``` diff --git a/docs/assets/icon.png b/docs/assets/icon.png new file mode 100644 index 0000000..7d0d73b Binary files /dev/null and b/docs/assets/icon.png differ diff --git a/docs/assets/icon.svg b/docs/assets/icon.svg new file mode 100644 index 0000000..36bdee6 --- /dev/null +++ b/docs/assets/icon.svg @@ -0,0 +1,51 @@ + + helm-apps icon + Layered deployment library icon with Helm wheel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/cookbook.md b/docs/cookbook.md new file mode 100644 index 0000000..57d79ff --- /dev/null +++ b/docs/cookbook.md @@ -0,0 +1,471 @@ +# Helm Apps Library Cookbook + + +Готовые рецепты для типовых сценариев. +Все примеры можно адаптировать под ваш `global._includes`. + +Быстрая навигация: +- [Старт docs](README.md) +- [Parameter Index](parameter-index.md) +- [Reference](reference-values.md) + +Оглавление (часто используемое): +- [1. Базовый HTTP API](#1-базовый-http-api-stateless) +- [2. API + Ingress + TLS](#2-api--ingress--tls) +- [4. CronJob](#4-cronjob) +- [6. Секреты через secretEnvVars](#6-секреты-через-secretenvvars) +- [9. configFilesYAML](#9-yaml-конфиг-с-env-override-configfilesyaml) +- [10. HPA](#10-hpa-для-api) +- [11. ServiceAccount + ClusterRole](#11-serviceaccount--clusterrole) +- [20. Как использовать cookbook](#20-как-использовать-cookbook) + +## 1. Базовый HTTP API (stateless) + + +```yaml +apps-stateless: + api: + _include: ["apps-stateless-defaultApp"] + replicas: + _default: 2 + production: 4 + containers: + main: + image: + name: api + staticTag: "1.0.0" + ports: | + - name: http + containerPort: 8080 + envVars: + APP_ENV: + _default: dev + production: production + resources: + requests: + mcpu: 200 + memoryMb: 256 + limits: + mcpu: 1000 + memoryMb: 1024 + service: + enabled: true + ports: | + - name: http + port: 80 + targetPort: 8080 +``` + +Параметры: [containers](reference-values.md#param-containers), [service](reference-values.md#param-service), [envVars](reference-values.md#param-envvars) +Навигация: [Parameter Index](parameter-index.md#workload) | [Наверх](#top) + +## 2. API + Ingress + TLS + + +```yaml +apps-ingresses: + api: + _include: ["apps-ingresses-defaultIngress"] + ingressClassName: nginx + host: api.example.org + paths: | + - path: / + pathType: Prefix + backend: + service: + name: api + port: + number: 80 + tls: + enabled: true +``` + +Параметры: [ingress](reference-values.md#param-ingress), [global.env](reference-values.md#param-global-env) +Навигация: [Parameter Index](parameter-index.md#networking-and-scaling) | [Наверх](#top) + +## 3. Worker без Service + +```yaml +apps-stateless: + worker: + _include: ["apps-stateless-defaultApp"] + service: + enabled: false + containers: + main: + image: + name: worker + staticTag: "1.0.0" + command: | + - /app/worker + envVars: + QUEUE: default +``` + +## 4. CronJob + + +```yaml +apps-cronjobs: + sync-every-5m: + _include: ["apps-cronjobs-defaultCronJob"] + schedule: "*/5 * * * *" + containers: + main: + image: + name: sync + staticTag: "2.1.0" + command: | + - /app/sync + envVars: + LOG_LEVEL: info +``` + +Параметры: [containers](reference-values.md#param-containers), [global._includes/_include](reference-values.md#param-global-includes) +Навигация: [Parameter Index](parameter-index.md#core) | [Наверх](#top) + +## 5. One-shot Job (migration) + +```yaml +apps-jobs: + db-migrate: + _include: ["apps-jobs-defaultJob"] + backoffLimit: 1 + containers: + main: + image: + name: migrate + staticTag: "3.0.0" + command: | + - /app/migrate +``` + +## 6. Секреты через `secretEnvVars` + + +```yaml +apps-stateless: + api: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: api + staticTag: "1.0.0" + secretEnvVars: + DB_PASSWORD: very-secret + JWT_SECRET: + _default: dev-secret + production: prod-secret +``` + +Параметры: [secretEnvVars](reference-values.md#param-secretenvvars) +Навигация: [Parameter Index](parameter-index.md#containers-envconfig) | [Наверх](#top) + +## 7. Из внешнего Secret через `fromSecretsEnvVars` + + +```yaml +apps-stateless: + api: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: api + staticTag: "1.0.0" + fromSecretsEnvVars: + external-secret: + APP_DB_PASSWORD: db_password + APP_API_TOKEN: api_token +``` + +Параметры: [fromSecretsEnvVars](reference-values.md#param-fromsecretsenvvars) +Навигация: [Parameter Index](parameter-index.md#containers-envconfig) | [Наверх](#top) + +## 8. Файлы конфигурации (ConfigMap mount) + + +```yaml +apps-stateless: + nginx: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: nginx + staticTag: "1.27" + configFiles: + nginx.conf: + mountPath: /etc/nginx/nginx.conf + content: | + worker_processes auto; + events { worker_connections 1024; } +``` + +Параметры: [configFiles](reference-values.md#param-configfiles) +Навигация: [Parameter Index](parameter-index.md#containers-envconfig) | [Наверх](#top) + +## 9. YAML-конфиг с env override (`configFilesYAML`) + + +```yaml +apps-stateless: + app: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: app + staticTag: "1.0.0" + configFilesYAML: + app.yaml: + mountPath: /etc/app/app.yaml + content: + db: + host: + _default: db.dev + production: db.prod + cache: + ttlSeconds: + _default: 30 + production: 300 +``` + +Параметры: [configFilesYAML](reference-values.md#param-configfilesyaml), [global.env](reference-values.md#param-global-env) +Навигация: [Parameter Index](parameter-index.md#containers-envconfig) | [Наверх](#top) + +## 10. HPA для API + + +```yaml +apps-stateless: + api: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: api + staticTag: "1.0.0" + horizontalPodAutoscaler: + enabled: true + minReplicas: 2 + maxReplicas: 10 + behavior: | + scaleDown: + policies: + - type: Percent + value: 10 + periodSeconds: 60 + metrics: + cpu: + enabled: true + averageUtilization: 70 + memory: + enabled: true + averageUtilization: 80 +``` + +Параметры: [horizontalPodAutoscaler](reference-values.md#param-hpa), [hpa.metrics](reference-values.md#param-hpa-metrics) +Навигация: [Parameter Index](parameter-index.md#networking-and-scaling) | [Наверх](#top) + +## 11. ServiceAccount + ClusterRole + + +```yaml +apps-stateless: + metrics-client: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: client + staticTag: "1.0.0" + serviceAccount: + enabled: true + name: metrics-client + clusterRole: + name: metrics-client:reader + rules: | + - apiGroups: ["monitoring.coreos.com"] + resources: ["prometheuses/http"] + resourceNames: ["main", "longterm"] + verbs: ["get"] +``` + +Параметры: [serviceAccount](reference-values.md#param-serviceaccount) +Навигация: [Parameter Index](parameter-index.md#workload) | [Наверх](#top) + +## 12. Stateful сервис с PVC + + +```yaml +apps-stateful: + redis: + _include: ["apps-stateful-defaultApp"] + replicas: 1 + containers: + main: + image: + name: redis + staticTag: "7.2" + ports: | + - name: redis + containerPort: 6379 + persistantVolumes: + data: + mountPath: /data + size: + _default: 1Gi + production: 20Gi + storageClass: fast-ssd +``` + +## 13. Dedicated ConfigMap/Secret resources + +```yaml +apps-configmaps: + shared-env: + _include: ["apps-configmaps-defaultConfigmap"] + envVars: + FEATURE_FLAG_X: "true" + REQUEST_TIMEOUT_MS: + _default: "1000" + production: "5000" + +apps-secrets: + shared-secret: + _include: ["apps-secrets-defaultSecret"] + envVars: + API_KEY: secret +``` + +## 14. Внешний Service через `apps-services` + +```yaml +apps-services: + api-internal: + _include: ["apps-defaults"] + ports: | + - name: http + port: 80 + targetPort: 8080 + selector: | + app: api +``` + +## 15. Пользовательская группа и mix app types + +```yaml +payment: + __GroupVars__: + type: apps-stateless + api: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: payment-api + staticTag: "1.0.0" + ingress: + __AppType__: apps-ingresses + _include: ["apps-ingresses-defaultIngress"] + host: pay.example.org + paths: | + - path: / + pathType: Prefix + backend: + service: + name: api + port: + number: 80 +``` + +## 16. Рецепт с `_default` + regex env + + +```yaml +apps-stateless: + env-aware: + _include: ["apps-stateless-defaultApp"] + containers: + main: + image: + name: app + staticTag: "1.0.0" + envVars: + LOG_LEVEL: + _default: info + production: warning + "^prod-.*$": error + FEATURE_ALPHA: + _default: "false" + "^dev-.*$": "true" +``` + +Параметры: [global.env](reference-values.md#param-global-env), [envVars](reference-values.md#param-envvars) +Навигация: [Parameter Index](parameter-index.md#core) | [Наверх](#top) + +## 17. apps-infra: NodeUser + +```yaml +apps-infra: + node-users: + platform-admin: + enabled: true + uid: 2001 + isSudoer: true + sshPublicKeys: | + - ssh-rsa AAAAB3Nza... + extraGroups: | + - wheel + nodeGroups: | + - worker +``` + +## 18. apps-dex-authenticators + +```yaml +apps-dex-authenticators: + auth-api: + enabled: true + applicationDomain: api.example.org + applicationIngressClassName: nginx + applicationIngressCertificateSecretName: api-example-org-tls + allowedGroups: | + - platform-admins + - backend-team +``` + +## 19. apps-custom-prometheus-rules + +```yaml +apps-custom-prometheus-rules: + api-rules: + groups: + api-group: + alerts: + high-error-rate: + isTemplate: false + content: | + expr: sum(rate(http_requests_total{status=~"5.."}[5m])) > 10 + for: 10m + labels: + severity_level: "3" +``` + +## 20. Как использовать cookbook + +1. Выберите сценарий, близкий вашему сервису. +2. Скопируйте блок в `values.yaml`. +3. Подключите ваш include-профиль. +4. Добавьте env-overrides. +5. Прогоните `helm template` с нужным окружением через `global.env`. + +Связанные документы: +- [docs/library-guide.md](library-guide.md) +- [docs/reference-values.md](reference-values.md) +- [docs/parameter-index.md](parameter-index.md) +- [tests/.helm/values.yaml](../tests/.helm/values.yaml) + +Навигация: [Наверх](#top) diff --git a/docs/example/.gitlab-ci.yml b/docs/example/.gitlab-ci.yml index e7d056d..f13c8ad 100644 --- a/docs/example/.gitlab-ci.yml +++ b/docs/example/.gitlab-ci.yml @@ -11,4 +11,4 @@ before_script: - set -eo pipefail - type trdl && source $(trdl use werf ${WERF_VERSION:-1.2 ea}) - type werf && source $(werf ci-env gitlab --as-file) -- werf helm repo add --force-update helm-apps https://flant.github.io/helm-apps +- werf helm repo add --force-update helm-apps https://alvnukov.github.io/helm-apps diff --git a/docs/example/.helm/helm-apps-defaults.yaml b/docs/example/.helm/helm-apps-defaults.yaml new file mode 100644 index 0000000..95649ed --- /dev/null +++ b/docs/example/.helm/helm-apps-defaults.yaml @@ -0,0 +1,102 @@ +apps-defaults: + enabled: false +apps-default-library-app: + _include: ["apps-defaults"] + # CLIENT: ask if this is ok for a defaul + imagePullSecrets: | + - name: registrysecret +## Конфигурация по умолчанию для CronJob в целом. +apps-cronjobs-defaultCronJob: + _include: ["apps-default-library-app"] + concurrencyPolicy: "Forbid" + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 1 + backoffLimit: 0 + priorityClassName: + prod: "production-high" + restartPolicy: "Never" + startingDeadlineSeconds: 60 + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + resourcePolicy: | + {} + +apps-secrets-defaultSecret: + _include: ["apps-defaults"] + +apps-ingresses-defaultIngress: + _include: ["apps-defaults"] + class: "nginx" + +apps-jobs-defaultJob: + _include: ["apps-default-library-app"] + backoffLimit: 0 + priorityClassName: + prod: "production-high" + restartPolicy: "Never" + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + resourcePolicy: | + {} + +apps-stateful-defaultApp: + _include: ["apps-default-library-app"] + revisionHistoryLimit: 3 + terminationGracePeriodSeconds: + _default: 30 + prod: 60 + affinity: | + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: {{ include "fl.generateSelectorLabels" (list $ . .name) | nindent 22 }} + topologyKey: kubernetes.io/hostname + weight: 10 + priorityClassName: + prod: "production-medium" + podDisruptionBudget: + enabled: true + maxUnavailable: "15%" + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + service: + enabled: false + name: "{{ $.CurrentApp.name }}" + headless: true + +apps-stateless-defaultApp: + _include: ["apps-default-library-app"] + revisionHistoryLimit: 3 + strategy: + _default: | + rollingUpdate: + maxSurge: 20% + maxUnavailable: 50% + type: RollingUpdate + prod: | + rollingUpdate: + maxSurge: 20% + maxUnavailable: 25% + type: RollingUpdate + priorityClassName: + prod: "production-medium" + podDisruptionBudget: + enabled: true + maxUnavailable: "15%" + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + resourcePolicy: | + {} + horizontalPodAutoscaler: + enabled: false + service: + enabled: false + name: "{{ $.CurrentApp.name }}" + +apps-configmaps-defaultConfigmap: + _include: ["apps-defaults"] diff --git a/docs/example/.helm/values.yaml b/docs/example/.helm/values.yaml index 2d1c6d2..e39ea98 100644 --- a/docs/example/.helm/values.yaml +++ b/docs/example/.helm/values.yaml @@ -18,106 +18,7 @@ global: # # Подробнее: https://github.com/flant/helm-charts/tree/master/.helm/charts/flant-lib#flexpandincludesinvalues-function _includes: - apps-defaults: - enabled: false - apps-default-library-app: - _include: ["apps-defaults"] - # CLIENT: ask if this is ok for a defaul - imagePullSecrets: | - - name: registrysecret - ## Конфигурация по умолчанию для CronJob в целом. - apps-cronjobs-defaultCronJob: - _include: ["apps-default-library-app"] - concurrencyPolicy: "Forbid" - successfulJobsHistoryLimit: 1 - failedJobsHistoryLimit: 1 - backoffLimit: 0 - priorityClassName: - prod: "production-high" - restartPolicy: "Never" - startingDeadlineSeconds: 60 - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - resourcePolicy: | - {} + # _include_from_file добавляет(инклудит) YAML из файла, работает в любом месте values.yaml + _include_from_file: helm-apps-defaults.yaml - apps-secrets-defaultSecret: - _include: ["apps-defaults"] - apps-ingresses-defaultIngress: - _include: ["apps-defaults"] - class: "nginx" - - apps-jobs-defaultJob: - _include: ["apps-default-library-app"] - backoffLimit: 0 - priorityClassName: - prod: "production-high" - restartPolicy: "Never" - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - resourcePolicy: | - {} - - apps-stateful-defaultApp: - _include: ["apps-default-library-app"] - revisionHistoryLimit: 3 - terminationGracePeriodSeconds: - _default: 30 - prod: 60 - affinity: | - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: {{ include "fl.generateSelectorLabels" (list $ . .name) | nindent 22 }} - topologyKey: kubernetes.io/hostname - weight: 10 - priorityClassName: - prod: "production-medium" - podDisruptionBudget: - enabled: true - maxUnavailable: "15%" - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - service: - enabled: false - name: "{{ $.CurrentApp.name }}" - headless: true - - apps-stateless-defaultApp: - _include: ["apps-default-library-app"] - revisionHistoryLimit: 3 - strategy: - _default: | - rollingUpdate: - maxSurge: 20% - maxUnavailable: 50% - type: RollingUpdate - prod: | - rollingUpdate: - maxSurge: 20% - maxUnavailable: 25% - type: RollingUpdate - priorityClassName: - prod: "production-medium" - podDisruptionBudget: - enabled: true - maxUnavailable: "15%" - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - resourcePolicy: | - {} - horizontalPodAutoscaler: - enabled: false - service: - enabled: false - name: "{{ $.CurrentApp.name }}" - headless: true - - apps-configmaps-defaultConfigmap: - _include: ["apps-defaults"] diff --git a/docs/flant-pr-draft.md b/docs/flant-pr-draft.md new file mode 100644 index 0000000..d6bc927 --- /dev/null +++ b/docs/flant-pr-draft.md @@ -0,0 +1,99 @@ +# Flant Upstream PR Draft + +This file prepares an upstream PR package for merging key `helm-apps` improvements into Flant library flow. + +## 1. Recommended PR Scope + +To keep review safe and predictable, split into 3 PRs: + +1. **Compatibility + validation safety** +2. **NetworkPolicy entity (CNI-aware)** +3. **Release matrix mode + docs/contracts** + +This avoids one huge diff and keeps rollback simple. + +## 2. Commit Candidates (from current repo) + +### PR-1: Compatibility + validation safety + +- `d0c2d21` feat(compat): kubernetes version-aware api/spec compatibility checks +- `9c28e30` fix(validation): fail on unexpected native lists and show exact values path +- `1128e61` fix(validation): allow native lists in approved template-driven paths +- `b4b50de` fix(stability): keep full list validation traversal and safe hasKey checks +- `18e1a6f` feat(strict): opt-in unknown-key validation for network policies +- `be5bf5e` feat(strict): unknown top-level apps groups validation with custom-group allowance + +### PR-2: NetworkPolicy entity + +- `48e8b08` feat(network-policy): add cni-aware network policy entity with type-based rendering + +### PR-3: Release matrix mode + +- `f4db8a2` Add release matrix mode with image tag fallback, schema/docs/contracts, and CI coverage +- `2915860` fix: improve schema compatibility and add internal-like test coverage +- `c3cae63` fix(docs): clarify optional releaseKey and app name fallback +- `614e0a0` fix(docs): clarify release defaults, fallbacks, and custom group type behavior +- `01b5164` fix(ci): add internal-like contract scenario for release/deploy flow + +## 3. PR Text Template (English) + +Use this in GitHub PR description: + +```md +## What + +This PR introduces compatibility and validation improvements for the helm-apps library: + +- Kubernetes-version-aware API rendering for resources with changed API versions. +- Safer values validation: + - fail on unexpected native YAML lists with exact values path in error; + - preserve allowed list-based subtrees used by template-driven fields; + - opt-in strict validation for selected entities/groups. +- Extended contract checks for backward/forward compatibility. + +## Why + +The main goal is to improve deployment reliability across mixed Kubernetes versions and reduce silent misconfiguration risks in large values trees. + +## Backward compatibility + +- Behavior remains backward-compatible by default. +- Strict unknown-key checks are opt-in via `global.validation.strict`. +- Existing templates using string-based Kubernetes blocks continue to work unchanged. + +## Validation + +Locally validated with: + +- `werf helm lint tests/.helm --values tests/.helm/values.yaml` +- contract templates checks (`tests/contracts`) +- Kubernetes compatibility matrix (render + kubeconform): + - 1.19.16 + - 1.20.15 + - 1.23.17 + - 1.29.0 + +## Notes for reviewers + +- Validation failure messages now include exact values path for faster troubleshooting. +- Custom top-level groups remain supported through `__GroupVars__.type`. +``` + +## 4. Upstream Checklist + +- [ ] Confirm target Flant repo and base branch. +- [ ] Create feature branch in upstream fork. +- [ ] Cherry-pick commits for selected PR scope. +- [ ] Resolve path conflicts in templates/schema/tests. +- [ ] Run local checks: + - [ ] `bash scripts/ci-local.sh --skip-snapshot` + - [ ] matrix render checks for 1.19/1.20/1.23/1.29 +- [ ] Push branch and open PR with template text. +- [ ] Attach rendered diff snippets for risky API-switch parts. + +## 5. Suggested Branch Names + +- `flant/compat-validation-safety` +- `flant/network-policy-cni-aware` +- `flant/release-matrix-mode` + diff --git a/docs/library-guide.md b/docs/library-guide.md new file mode 100644 index 0000000..996f3dd --- /dev/null +++ b/docs/library-guide.md @@ -0,0 +1,463 @@ +# Helm Apps Library Handbook + + +Быстрая навигация: +- [Старт docs](README.md) +- [Parameter Index](parameter-index.md) +- [Reference](reference-values.md) +- [Cookbook](cookbook.md) +- [Operations](operations.md) + +## 1. Для кого этот документ + +Документ предназначен для: +- разработчиков, которые деплоят приложения в Kubernetes через Helm; +- DevOps/SRE, которые поддерживают единый стандарт деплоя сервисов; +- ревьюеров конфигураций `.helm/values.yaml`. + +Примечание: библиотека полностью поддерживает `Helm` и совместима с `werf`. +Практически `werf` нередко удобнее в командной эксплуатации: меньше ручной склейки шагов между рендером и поставкой. +Но все возможности библиотеки остаются полностью доступными через `Helm`. + +Если нужен быстрый старт, сначала прочитайте раздел `3`. +Если нужен маршрут по документам, откройте `docs/README.md`. +Если нужен полный справочник полей, смотрите `docs/reference-values.md`. +Если нужны готовые шаблоны под типовые сценарии, смотрите `docs/cookbook.md`. + +## 2. Что такое helm-apps и зачем его использовать + +`helm-apps` это библиотечный Helm chart (`type: library`), который рендерит Kubernetes-ресурсы на основе унифицированной структуры `values.yaml`. + +Ключевая идея: +- логика рендера ресурсов живет в одной библиотеке; +- сервисные репозитории описывают только конфигурацию; +- дефолты и переиспользование реализуются через `_include` и `global._includes`. + +Почему это выгодно команде: +- меньше ручных манифестов и копипаста; +- единый формат деплоя во всех сервисах; +- проще ревью и онбординг; +- меньше расхождений между сервисами в runtime-поведении; +- быстрее массовые изменения платформенных практик. + +Когда библиотека особенно полезна: +- десятки+ сервисов с похожими паттернами деплоя; +- необходимость стандартизировать логику HPA/VPA/PDB/Ingress/ServiceAccount; +- мультиокружения с разными параметрами в одном `values.yaml`. + +## 3. Quick Start + +1. Подключить библиотеку в `.helm/Chart.yaml` как dependency. +2. Создать шаблон инициализации: + +```yaml +{{- include "apps-utils.init-library" $ }} +``` + +3. Задать в `global._includes` дефолтные профили. +4. Описать приложения в секциях `apps-*` или custom-группах. +5. Проверить рендер: + +```bash +helm dependency update .helm +helm template my-app .helm --set global.env=prod +``` + +## 4. Базовая модель конфигурации + +Верхний уровень `values.yaml`: +- `global` — общие переменные и include-блоки; +- `apps-*` — встроенные группы ресурсов; +- произвольные группы через `__GroupVars__`. + +### 4.1 Встроенные группы `apps-*` + +Библиотека поддерживает: +- `apps-stateless` (`Deployment`); +- `apps-stateful` (`StatefulSet`); +- `apps-jobs` (`Job`); +- `apps-cronjobs` (`CronJob`); +- `apps-ingresses` (`Ingress`, optional `Certificate`, optional `DexAuthenticator`); +- `apps-services` (`Service`); +- `apps-network-policies` (`NetworkPolicy`); +- `apps-configmaps` (`ConfigMap`); +- `apps-secrets` (`Secret`); +- `apps-pvcs` (`PersistentVolumeClaim`); +- `apps-certificates` (`Certificate`); +- `apps-limit-range` (`LimitRange`); +- `apps-dex-clients` (`DexClient`); +- `apps-dex-authenticators` (`DexAuthenticator`); +- `apps-custom-prometheus-rules` (`CustomPrometheusRules`); +- `apps-grafana-dashboards` (`GrafanaDashboardDefinition`); +- `apps-kafka-strimzi` (Kafka + KafkaTopic + VPA под Strimzi); +- `apps-infra` (`NodeUser` и `NodeGroup` Deckhouse). + +### 4.2 Произвольные группы через `__GroupVars__` + +Позволяют описывать “логические” группы приложений: + +```yaml +payment-group: + __GroupVars__: + type: + _default: apps-stateless + prod: apps-stateful + api: + _include: ["apps-stateless-defaultApp"] + worker: + _include: ["apps-stateless-defaultApp"] +``` + +`__GroupVars__.type` поддерживает: +- строку (`apps-stateless`, `apps-ingresses`, ...); +- env-map с выбором через `global.env`. + +Для отдельного приложения можно переопределить тип: + +```yaml +payment-group: + __GroupVars__: + type: apps-stateless + edge: + __AppType__: apps-ingresses +``` + +### 4.3 Пользовательские рендер-шаблоны через `__GroupVars__.type` + +Можно рендерить собственные сущности через библиотечный цикл `renderApps`. + +Идея: +1. В группе задается `__GroupVars__.type: `. +2. В chart приложения объявляется шаблон `define ".render"`. +3. Библиотека вызывает его автоматически как `include (printf "%s.render" $type) $`. + +В custom renderer доступны переменные контекста: +- `$` (root context), +- `$.Values`, +- `$.CurrentApp`, +- `$.CurrentGroupVars`, +- `$.CurrentGroup`, +- `$.CurrentPath`, +- `$.Release`, +- `$.Capabilities`, +- `$.Files`. + +Любые поля приложения из `custom-services..*` автоматически пробрасываются в `$.CurrentApp.*`. + +Пример values: + +```yaml +custom-services: + __GroupVars__: + type: custom-services + minio: + enabled: true + host: + ip: minio.example.local + port: 9000 + extraLabels: + app.kubernetes.io/component: storage +``` + +Пример шаблона в chart приложения: + +```yaml +{{- define "custom-services.render" -}} +{{- $ := . -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $.CurrentApp.name | quote }} + labels: + app.kubernetes.io/name: {{ $.CurrentApp.name | quote }} + app.kubernetes.io/enabled: {{ printf "%v" $.CurrentApp.enabled | quote }} +{{- with $.CurrentApp.extraLabels }} +{{ toYaml . | indent 4 }} +{{- end }} +spec: + type: ExternalName + externalName: {{ printf "%v" $.CurrentApp.host.ip | quote }} + ports: + - port: {{ $.CurrentApp.host.port }} +{{- end -}} +``` + +В этом примере `name`, `enabled`, `host.ip`, `host.port` и `extraLabels` приходят из `values` текущего app через `$.CurrentApp`. + +## 5. Переиспользование конфигурации + +### 5.1 `global._includes` + `_include` + +`global._includes` хранит шаблонные блоки: + +```yaml +global: + _includes: + apps-stateless-defaultApp: + replicas: 2 + service: + enabled: false +``` + +Подключение в приложении: + +```yaml +apps-stateless: + billing-api: + _include: ["apps-stateless-defaultApp"] +``` + +Несколько include: + +```yaml +_include: ["profile-base", "profile-prod", "profile-api"] +``` + +Практика: +- делите include на небольшие “профили”; +- используйте явные имена (`apps-stateless-defaultApp`, `profile-worker`); +- локальные overrides держите прямо в приложении. + +### 5.2 `_include_from_file` + +Поддерживается загрузка include-блоков из файлов через `global._includes`. +Используйте это для больших наборов дефолтов, чтобы не раздувать основной `values.yaml`. + +## 6. Окружения: `_default`, env-override, regex + +Любое значение может задаваться: +- как обычный скаляр; +- как map по окружениям. + +Пример: + +```yaml +replicas: + _default: 2 + production: 5 + "^prod-.*$": 4 +``` + +Алгоритм выбора значения: +1. точное совпадение `global.env`; +2. regex-совпадение по ключам; +3. `_default`. + +Важно: +- несколько regex-совпадений для одного поля вызывают ошибку; +- для вложенных env-структур (`envYAML`, `configFilesYAML`) ожидается `_default`, иначе узел может быть проигнорирован логикой рендера. + +## 7. Типы полей: “строка YAML” vs map/list + +В библиотеке есть поля, которые вставляются в манифест как raw YAML. +Часто это делается через block string: + +```yaml +annotations: | + key: value +ports: | + - name: http + port: 80 +``` + +Преимущество: +- 1:1 перенос kubernetes-структуры без дополнительной обвязки. + +Риск: +- если передать не тот тип, можно получить неочевидный runtime-результат. + +Рекомендация: +- держите schema-валидацию включенной; +- используйте рабочие шаблоны из `docs/cookbook.md`. + +## 8. Контейнерный слой + +`containers` и `initContainers` поддерживают: +- image: `name`, `staticTag`, `generateSignatureBasedTag`; +- process: `command`, `args`, `workingDir`; +- env: `envVars`, `secretEnvVars`, `envFrom`, `envYAML`, `fromSecretsEnvVars`; +- resources: `requests/limits` (`mcpu`, `memoryMb`, `ephemeralStorageMb`); +- configs: `configFiles`, `configFilesYAML`, `secretConfigFiles`; +- probes/lifecycle/security: `livenessProbe`, `readinessProbe`, `startupProbe`, `lifecycle`, `securityContext`; +- volumes: `volumeMounts`, `persistantVolumes`. + +Особенности: +- `secretEnvVars` автоматически создают Secret и подключают его в `envFrom`; +- `configFiles*` автоматически создают ConfigMap/Secret и монтируются в контейнер; +- `alwaysRestart` добавляет псевдослучайный env `FL_APP_ALWAYS_RESTART`. + +## 9. Слой Pod/Workload + +Для `apps-stateless` и `apps-stateful` доступны: +- `replicas`; +- `affinity`, `tolerations`, `nodeSelector`, `topologySpreadConstraints`; +- `imagePullSecrets`, `volumes`; +- `serviceAccount` с optional `clusterRole`; +- `podDisruptionBudget`; +- `verticalPodAutoscaler`; +- `horizontalPodAutoscaler` (для stateless); +- `service`. + +Для `apps-jobs`/`apps-cronjobs`: +- `backoffLimit`, `activeDeadlineSeconds`, `restartPolicy`; +- для cron: `schedule`, `concurrencyPolicy`, `startingDeadlineSeconds`, `successfulJobsHistoryLimit`, `failedJobsHistoryLimit`. + +## 10. Сетевой слой + +### 10.1 Service + +`apps-services` или вложенный `service` у workload: +- `ports`; +- `selector`; +- `type`, `clusterIP`, `sessionAffinity`, и другие параметры Service API. + +### 10.2 Ingress + +`apps-ingresses`: +- `host`, `paths`; +- `class` и/или `ingressClassName`; +- `tls.enabled`; +- optional `tls.secret_name`. + +Если `tls.enabled=true` и `secret_name` не задан: +- библиотека генерирует `Certificate` автоматически. + +`dexAuth` в ingress: +- включает генерацию связанного `DexAuthenticator` для защиты приложения. + +## 11. Безопасность и доступы + +### 11.1 Secrets + +Сценарии: +- `apps-secrets` для отдельного Secret ресурса; +- `secretEnvVars` в контейнере для привязки секретов к pod; +- `secretConfigFiles` для файловых секретов. + +### 11.2 ServiceAccount и RBAC + +В приложении: + +```yaml +serviceAccount: + enabled: true + name: app-sa + clusterRole: + name: app-sa:read + rules: | + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] +``` + +Библиотека создаст: +- `ServiceAccount`; +- `ClusterRole`; +- `ClusterRoleBinding`. + +## 12. Масштабирование и SLO + +### 12.1 VerticalPodAutoscaler + +Поддерживается для workload-ресурсов и части специализированных групп. +Используйте: +- `updateMode: Off` для сбора метрик; +- `updateMode: Initial/Auto` по стратегии команды. + +### 12.2 HorizontalPodAutoscaler + +Поддержка: +- `cpu`, `memory`; +- object/custom metrics; +- optional генерация Deckhouse metric ресурсов (`customMetricResources`). + +## 13. Observability + +Поддерживаются: +- `apps-custom-prometheus-rules`; +- `apps-grafana-dashboards`; +- `deckhouseMetrics` в приложении. + +Это позволяет держать метрики/правила алертинга рядом с конфигурацией деплоя сервиса. + +## 14. Специализированные группы + +### 14.1 `apps-kafka-strimzi` + +Шаблоны для Strimzi: +- `Kafka` cluster; +- `KafkaTopic`; +- сопутствующие VPA. + +Типичный сценарий: +- единый блок конфигурации Kafka в values; +- env-override для `prod/non-prod`. + +### 14.2 `apps-infra` + +Deckhouse-инфраструктурные сущности: +- `node-users`; +- `node-groups`. + +Используйте для инфраструктурных репозиториев или platform-слоя. + +## 15. Хуки и расширяемость + +Поддерживаются pre-render hooks: +- group-level: `__GroupVars__._preRenderGroupHook`; +- app-level default: `__GroupVars__._preRenderAppHook`; +- app-level explicit: `_preRenderHook`. + +Практические применения: +- массовая модификация группы перед рендером; +- автоматическое включение/клонирование приложений; +- вычисление derived-конфигурации. + +## 16. Рекомендуемые практики команды + +1. Выделяйте платформенные include-профили в `global._includes`. +2. Делайте значения окружений через `_default` + target-env overrides. +3. Не храните business-логику в Helm-шаблонах сервисов. +4. Для сложных структур используйте raw YAML блоки и шаблоны из cookbook. +5. Прогоняйте `helm template` в CI на каждом merge request. +6. Проверяйте значения schema-валидатором. + +## 17. Антипаттерны + +1. Копировать готовые Deployment/Service шаблоны между сервисами. +2. Размазывать дефолты по множеству несвязанных include-блоков. +3. Использовать “неявные” regex для env, создающие неоднозначности. +4. Смешивать в одном приложении слишком много unrelated-ролей (api+worker+cron). +5. Отключать schema-валидацию в CI. + +## 18. Валидация + +Основные артефакты: +- примеры: `tests/.helm/values.yaml`; +- схема: `tests/.helm/values.schema.json`. + +Рекомендуемые проверки: + +```bash +helm lint .helm +helm template my-app .helm --set global.env=prod +``` + +## 19. Миграция на библиотеку (пошагово) + +1. Подключите библиотеку и инициализатор. +2. Перенесите один сервис в `apps-stateless`. +3. Вынесите общие настройки в include-профили. +4. Добавьте service/ingress/hpa/vpa/pdb. +5. Перенесите jobs/cronjobs. +6. Включите CI-проверки schema + render. +7. Только после стабилизации удаляйте legacy шаблоны. + +## 20. Навигация по документации + +- Концепция и архитектура: `docs/library-guide.md` +- Полный справочник полей: `docs/reference-values.md` +- Готовые рецепты: `docs/cookbook.md` +- Эксплуатация и troubleshooting: `docs/operations.md` +- Полные рабочие примеры: `tests/.helm/values.yaml` +- Схема валидации: `tests/.helm/values.schema.json` diff --git a/docs/operations.md b/docs/operations.md new file mode 100644 index 0000000..14f3dac --- /dev/null +++ b/docs/operations.md @@ -0,0 +1,304 @@ +# Helm Apps Library Operations Playbook + + +Документ для эксплуатации и поддержки деплоев на `helm-apps`: +- как быстро диагностировать проблемы; +- как локализовать источник ошибки; +- какие команды и чеклисты использовать в CI/CD и при релизах; +- как откатываться безопасно. + +Быстрая навигация: +- [Старт docs](README.md) +- [Parameter Index](parameter-index.md) +- [Reference](reference-values.md) + +Оглавление: +- [2. Быстрый triage](#2-быстрый-triage-по-слоям) +- [3. Команды диагностики](#3-стандартные-команды-диагностики) +- [4. Частые ошибки](#4-частые-ошибки-и-что-делать) +- [5. Чеклист перед merge](#5-чеклист-изменения-values-перед-merge) +- [6. Чеклист релиза](#6-чеклист-релиза) +- [7. Rollback стратегия](#7-rollback-стратегия) + +## 1. Operational Mindset + +При инцидентах действуйте в порядке: +1. Подтвердить симптом (что именно сломано). +2. Локализовать слой (schema -> render -> apply -> runtime). +3. Найти минимальный diff, который вызвал проблему. +4. Восстановить сервис (rollback/hotfix). +5. Зафиксировать постоянное исправление (include/profile/schema/tests). + +## 2. Быстрый triage по слоям + +### 2.1 Layer 1: Values/Schema + +Признаки: +- ошибки валидации `values`; +- не тот тип поля; +- пропущены обязательные ключи. + +Проверки: + +```bash +helm lint .helm +``` + +Для репозитория библиотеки: + +```bash +helm lint tests/.helm --values tests/.helm/values.yaml +``` + +### 2.2 Layer 2: Render + +Признаки: +- шаблоны не рендерятся; +- ошибки `include`/`tpl`/`required`/`fail`; +- неоднозначный env regex. + +Проверки: + +```bash +helm template my-app .helm --set global.env=prod +``` + +Если рендер падает: +- ищите в тексте ошибки полный `CurrentPath` (путь до проблемного блока); +- сверяйте тип/структуру поля с `docs/reference-values.md`; +- проверяйте merge include-блоков. + +### 2.3 Layer 3: Apply/Release + +Признаки: +- рендер успешен, но релиз не применился; +- ошибки Kubernetes API validation; +- forbidden/unauthorized по RBAC. + +Проверки: +- события namespace; +- статус rollout; +- актуальность CRD (для cert-manager/Deckhouse/Strimzi). + +### 2.4 Layer 4: Runtime + +Признаки: +- pod crashloop; +- readiness/liveness failures; +- нет трафика через ingress/service; +- HPA/VPA не работают как ожидается. + +Проверки: +- pod logs/describe; +- service endpoints; +- ingress controller events; +- метрики HPA/VPA. + +Навигация: [Наверх](#top) + +## 3. Стандартные команды диагностики + +### 3.1 Helm + +```bash +helm dependency update .helm +helm lint .helm +helm template my-app .helm --set global.env=prod +``` + +### 3.2 Kubernetes runtime + +```bash +kubectl -n get deploy,sts,job,cronjob,svc,ing,pdb,hpa,vpa +kubectl -n get pods +kubectl -n describe pod +kubectl -n logs -c +kubectl -n get events --sort-by=.metadata.creationTimestamp +``` + +### 3.3 Service/Ingress debug + +```bash +kubectl -n get endpoints +kubectl -n describe ingress +``` + +Навигация: [Наверх](#top) + +## 4. Частые ошибки и что делать + +## 4.1 Ошибка schema: `Invalid type` + +Причина: +- передан map/list вместо строки YAML (или наоборот); +- env-map там, где ожидался plain scalar. + +Действия: +1. Проверить поле в `docs/reference-values.md`. +2. Сверить пример в `docs/cookbook.md`. +3. Повторно запустить `helm lint`. + +## 4.2 Ошибка рендера: `__GroupVars__ is required` + +Причина: +- top-level custom group без `__GroupVars__`; +- schema трактует ключ как custom group. + +Действия: +1. Если это custom group, добавить: +```yaml +__GroupVars__: + type: apps-stateless +``` +2. Если это служебный ключ/секция, убедиться, что он описан в schema. + +## 4.3 Ошибка рендера: ambiguous regex env + +Причина: +- несколько regex-ключей окружений совпали одновременно. + +Действия: +1. Убрать пересечение regex. +2. Оставить один явный env-override и `_default`. + +## 4.4 Включен app, но не заданы контейнеры + +Признак: +- `fail` из шаблонов `apps-stateless`/`apps-stateful`/`apps-jobs`/`apps-cronjobs`. + +Действия: +1. Добавить `containers`. +2. Либо временно выключить ресурс `enabled: false`. + +## 4.5 Service есть, но трафика нет + +Причины: +- selector не совпадает с labels pod; +- нет endpoints; +- порт не совпадает (`targetPort` vs container port). + +Действия: +1. `kubectl get endpoints`. +2. Сверить selector и labels. +3. Проверить контейнерные порты. + +## 4.6 Ingress есть, но 404/502 + +Причины: +- неверный backend service/port; +- ingress class mismatch; +- TLS secret отсутствует. + +Действия: +1. `kubectl describe ingress`. +2. Проверить `ingressClassName`/`class`. +3. Проверить наличие секрета и сертификата. + +## 4.7 HPA не скейлит + +Причины: +- невалидные metrics; +- отсутствуют источники метрик; +- min/max реплики блокируют ожидаемое поведение. + +Действия: +1. Проверить объект HPA и его conditions. +2. Сверить `metrics` и `customMetricResources`. +3. Проверить доступность metrics API. + +## 4.8 VPA не влияет на pods + +Причины: +- `updateMode: Off`; +- конфликт ожиданий между HPA и VPA; +- ресурс применен, но policy не задает нужное поведение. + +Действия: +1. Проверить `updateMode`. +2. Проверить policy. +3. Согласовать autoscaling стратегию. + +Навигация: [Reference](reference-values.md) | [Parameter Index](parameter-index.md) | [Наверх](#top) + +## 5. Чеклист изменения values перед merge + +1. Изменения проходят schema (`helm lint`). +2. Изменения рендерятся в target env (`helm template ... --set global.env=`). +3. Проверены include-конфликты и приоритет override. +4. Для env-ключей нет неоднозначных regex. +5. Для ingress/service проверены имена backend и порты. +6. Для секретов исключены plaintext утечки в git (используйте `secret-values` или внешние хранилища). +7. Для HPA/VPA согласованы min/max/updateMode и metrics. + +Навигация: [Наверх](#top) + +## 6. Чеклист релиза + +1. Подтянуты зависимости чарта. +2. Отрендерен итоговый манифест для target env. +3. Нет неожиданных изменений в критичных ресурсах: +- Service selectors; +- Ingress host/path/tls; +- Stateful PVC/retention settings; +- ServiceAccount/RBAC. +4. Подготовлен rollback-план. + +Навигация: [Наверх](#top) + +## 7. Rollback стратегия + +При регрессии: +1. Откатить `values` к последнему рабочему коммиту. +2. Повторить рендер и деплой. +3. Если проблема в include-profile, зафиксировать hotfix в профиле. + +Рекомендации: +- держите small-batch изменения в values; +- не смешивайте в одном MR массовый refactor и функциональные изменения. + +Навигация: [Наверх](#top) + +## 8. Incident response шаблон + +Минимальный протокол: +1. Time started. +2. Затронутые сервисы/окружения. +3. Последний измененный commit в values/include. +4. Симптом/алерт. +5. Layer диагностики (schema/render/apply/runtime). +6. Временное восстановление (rollback/hotfix). +7. Root cause. +8. Permanent fix. +9. Action items. + +## 9. Hardening practices + +1. Обязательный `helm lint` + `helm template` в CI. +2. Обязательный code-review для include-профилей. +3. Запрет на “широкие” regex для env без необходимости. +4. Разделение common include-профилей по доменам: +- compute; +- networking; +- security; +- autoscaling. +5. Документирование нестандартных hooks рядом с группой. + +## 10. Сопровождение schema + +При добавлении нового поля/ресурса в библиотеку: +1. Обновить `tests/.helm/values.schema.json`. +2. Добавить пример в `tests/.helm/values.yaml`. +3. Обновить `docs/reference-values.md`. +4. При необходимости добавить рецепт в `docs/cookbook.md`. + +Это защищает от дрейфа между кодом библиотеки, примерами и документацией. + +## 11. Полезные артефакты в репозитории + +- Полные примеры: `tests/.helm/values.yaml` +- Schema: `tests/.helm/values.schema.json` +- Концепция: `docs/library-guide.md` +- Reference: `docs/reference-values.md` +- Cookbook: `docs/cookbook.md` + +Навигация: [Наверх](#top) diff --git a/docs/parameter-index.md b/docs/parameter-index.md new file mode 100644 index 0000000..88dc924 --- /dev/null +++ b/docs/parameter-index.md @@ -0,0 +1,49 @@ +# Helm Apps: Parameter Index + +Быстрые переходы по параметрам: описание + рабочий пример. + +## Core + +| Параметр | Описание | Пример | +|---|---|---| +| `global.env` | [Описание](reference-values.md#param-global-env) | [Пример](cookbook.md#example-global-env) | +| `global._includes` | [Описание](reference-values.md#param-global-includes) | [Пример](../README.md#example-global-includes-merge) | +| `global.release` | [Описание](reference-values.md#param-global-release) | [Пример](reference-values.md#example-global-release) | +| `_include` | [Описание](reference-values.md#param-include) | [Пример](../README.md#example-include-concat) | + +## Workload + +| Параметр | Описание | Пример | +|---|---|---| +| `containers` | [Описание](reference-values.md#param-containers) | [Пример](cookbook.md#example-basic-api) | +| `service` | [Описание](reference-values.md#param-service) | [Пример](cookbook.md#example-basic-api) | +| `releaseKey` | [Описание](reference-values.md#param-releasekey) | [Пример](reference-values.md#example-global-release) | +| `podDisruptionBudget` | [Описание](reference-values.md#param-pdb) | [Пример](../tests/.helm/values.yaml) | +| `serviceAccount` | [Описание](reference-values.md#param-serviceaccount) | [Пример](cookbook.md#example-serviceaccount) | + +## Containers Env/Config + +| Параметр | Описание | Пример | +|---|---|---| +| `envVars` | [Описание](reference-values.md#param-envvars) | [Пример](cookbook.md#example-basic-api) | +| `secretEnvVars` | [Описание](reference-values.md#param-secretenvvars) | [Пример](cookbook.md#example-secretenvvars) | +| `fromSecretsEnvVars` | [Описание](reference-values.md#param-fromsecretsenvvars) | [Пример](cookbook.md#example-fromsecretsenvvars) | +| `envYAML` | [Описание](reference-values.md#param-envyaml) | [Пример](../tests/.helm/values.yaml) | +| `configFiles` | [Описание](reference-values.md#param-configfiles) | [Пример](cookbook.md#example-configfiles) | +| `configFilesYAML` | [Описание](reference-values.md#param-configfilesyaml) | [Пример](cookbook.md#example-configfilesyaml) | + +## Networking and Scaling + +| Параметр | Описание | Пример | +|---|---|---| +| `ingress` (`host/paths/tls`) | [Описание](reference-values.md#param-ingress) | [Пример](cookbook.md#example-ingress-tls) | +| `verticalPodAutoscaler` | [Описание](reference-values.md#param-vpa) | [Пример](../tests/.helm/values.yaml) | +| `horizontalPodAutoscaler` | [Описание](reference-values.md#param-hpa) | [Пример](cookbook.md#example-hpa) | +| `horizontalPodAutoscaler.metrics` | [Описание](reference-values.md#param-hpa-metrics) | [Пример](cookbook.md#example-hpa) | + +## Related Docs + +- Общая концепция: [library-guide.md](library-guide.md) +- Полный референс: [reference-values.md](reference-values.md) +- Практические рецепты: [cookbook.md](cookbook.md) +- Операционная эксплуатация: [operations.md](operations.md) diff --git a/docs/reference-values.md b/docs/reference-values.md new file mode 100644 index 0000000..e4a4c98 --- /dev/null +++ b/docs/reference-values.md @@ -0,0 +1,744 @@ +# Helm Apps Library: Reference по values + + +Документ описывает практический референс структуры `values.yaml`. +Он дополняет `docs/library-guide.md` и должен читаться вместе с ним. + +Быстрая навигация: +- [Старт docs](README.md) +- [Handbook](library-guide.md) +- [Cookbook](cookbook.md) +- [Parameter Index](parameter-index.md) + +Оглавление: +- [1. Top-level ключи](#1-top-level-ключи) +- [2. global](#2-global) +- [5. containers / initContainers](#5-containers--initcontainers) +- [8. Config files](#8-config-files) +- [9. Service block](#9-service-block) +- [10. Ingress block](#10-ingress-block) +- [11. Autoscaling blocks](#11-autoscaling-blocks) +- [17. Cheat sheet](#17-тип-поля---поведение-рендера-cheat-sheet) + +## 1. Top-level ключи + +Поддерживаемые секции: +- `global` +- `apps-stateless` +- `apps-stateful` +- `apps-jobs` +- `apps-cronjobs` +- `apps-services` +- `apps-ingresses` +- `apps-network-policies` +- `apps-configmaps` +- `apps-secrets` +- `apps-pvcs` +- `apps-limit-range` +- `apps-certificates` +- `apps-dex-clients` +- `apps-dex-authenticators` +- `apps-custom-prometheus-rules` +- `apps-grafana-dashboards` +- `apps-kafka-strimzi` +- `apps-infra` +- произвольные custom-группы с `__GroupVars__` + +Служебные ключи, которые могут появляться в merged values: +- `helm-apps` + +## 2. `global` + + +Типичные поля: +- `env`: текущее окружение (`dev`, `prod`, `production`, etc.); +- `_includes`: библиотека include-блоков; +- `release`: декларативное управление версиями приложений; +- `validation.strict`: opt-in strict contract для проверки values; +- произвольные project-level переменные (`ci_url`, `baseUrl` и т.д.). + +Пример: + +```yaml +global: + env: production + ci_url: example.org + validation: + strict: false + _includes: + apps-stateless-defaultApp: + replicas: + _default: 2 + production: 4 +``` + +Примечание по `validation.strict`: +- В ветке `1.x` значение по умолчанию — `false` (совместимость). +- Флаг добавлен как контракт для постепенного перехода к более строгой валидации без breaking changes. +- Текущая реализация strict-check сначала покрывает `apps-network-policies` (неизвестные ключи дают fail). +- На top-level strict-check валидирует только `apps-*` имена: + - встроенные `apps-*` группы разрешены; + - custom-группы разрешены через `__GroupVars__.type`; + - неизвестная `apps-*` секция без `__GroupVars__` даёт fail. + +### 2.1 `global.release` + + + +`global.release` включает режим декларативных релизов: +- `enabled`: включает release-логику; +- `current`: имя текущего релиза; +- `autoEnableApps`: автоматически включает app, если для него найдена версия; +- `versions`: матрица `релиз -> appKey -> tag/version`. + +Дефолты и поведение: +- `enabled`: `false` по умолчанию; +- `autoEnableApps`: `true` по умолчанию; +- если версия для app не найдена в `versions.`, библиотека не проставляет `CurrentAppVersion` и не меняет стандартную логику рендера. + +Связанные app-параметры: +- `releaseKey` — ключ приложения в `global.release.versions.`. + - параметр опционален; + - если `releaseKey` не задан, библиотека использует `app.name`. + + +Пример: + +```yaml +global: + release: + enabled: true + current: "production-v1" + autoEnableApps: true + versions: + production-v1: + release-web: "3.19" + +apps-stateless: + api: + enabled: false + releaseKey: release-web + containers: + main: + image: + name: alpine +``` + +Поведение: +- библиотека выставляет `CurrentReleaseVersion` и `CurrentAppVersion`; +- если `image.staticTag` не задан, используется `CurrentAppVersion`; +- если `CurrentAppVersion` тоже не задан, image резолвится через стандартный путь `Values.werf.image`; +- в metadata добавляются аннотации: + - `helm-apps/release` + - `helm-apps/app-version` +- при `autoEnableApps=true` app автоматически включается, когда версия найдена в матрице релиза. + +### 2.2 `global._includes` + `_include`: примеры merge + + + +Ниже примеры, как библиотека объединяет include-профили. + +#### Пример A: Рекурсивный merge вложенных map + +```yaml +global: + _includes: + base: + service: + enabled: true + headless: false + net: + service: + ports: | + - name: http + port: 80 + +apps-stateless: + api: + _include: ["base", "net"] +``` + +Итог: +- `service.enabled=true` +- `service.headless=false` +- `service.ports` добавлен из `net` + +#### Пример B: Приоритет include по порядку + +```yaml +global: + _includes: + base: + replicas: 2 + prod: + replicas: 5 + +apps-stateless: + api: + _include: ["base", "prod"] +``` + +Итог: `replicas=5`. + +#### Пример C: Локальный override сильнее include + +```yaml +global: + _includes: + base: + replicas: 2 + +apps-stateless: + api: + _include: ["base"] + replicas: 3 +``` + +Итог: `replicas=3`. + +#### Пример D: Env-map поведение при merge include + +```yaml +global: + _includes: + base: + replicas: + _default: 2 + production: 4 + canary: + replicas: + _default: 1 + production: 2 + +apps-stateless: + api: + _include: ["base", "canary"] +``` + +Поведение в результате merge: +- ключ `production` будет взят из `base` (значение `4`); +- `_default` будет взят из `canary` (значение `1`). + +Вывод: для env-map обязательно проверяйте финальный рендер в нужном окружении. + +Навигация: [Parameter Index](parameter-index.md#core) | [Наверх](#top) + +#### Пример E: `_include`-списки конкатенируются + +```yaml +global: + _includes: + profile-a: + _include: ["base-a"] + replicas: 2 + profile-b: + _include: ["base-b"] + +apps-stateless: + api: + _include: ["profile-a", "profile-b"] +``` + +Итоговый include-chain для приложения объединяет `base-a` и `base-b`. + +Важно: +- это поведение относится к служебному ключу `_include`; +- обычные списковые параметры библиотеки, как правило, задаются строковым YAML-блоком (`|`), поэтому их merge как native list обычно не применяется. + +## 3. Общая форма приложения в `apps-*` + +```yaml +apps-stateless: + app-name: + _include: ["profile-name"] + enabled: true + name: "custom-name" + werfWeight: -10 + annotations: | + key: value + labels: | + tier: backend +``` + +Общие поля, которые могут встречаться в большинстве app-типов: +- `_include` +- `enabled` +- `name` +- `werfWeight` +- `releaseKey` +- `annotations` +- `labels` + +## 4. Workload app-поля + +Актуально для: +- `apps-stateless` +- `apps-stateful` +- `apps-jobs` +- `apps-cronjobs` + +### 4.1 Pod/workload common + +- `containers` +- `initContainers` +- `imagePullSecrets` +- `affinity` +- `tolerations` +- `nodeSelector` +- `volumes` +- `serviceAccount` +- `verticalPodAutoscaler` + +### 4.2 Stateless/Stateful + +Дополнительно: +- `replicas` +- `podDisruptionBudget` +- `service` +- `selector` +- `horizontalPodAutoscaler` (в основном для stateless) + +Stateful-specific: +- `service.name` (для headless service), +- `updateStrategy`, +- `persistentVolumeClaimRetentionPolicy`, +- `volumeClaimTemplates`. + +### 4.3 Jobs/CronJobs + +Общие job-поля: +- `backoffLimit` +- `activeDeadlineSeconds` +- `restartPolicy` +- `ttlSecondsAfterFinished` (в соответствующем API-блоке) + +Только cron: +- `schedule` +- `concurrencyPolicy` +- `startingDeadlineSeconds` +- `successfulJobsHistoryLimit` +- `failedJobsHistoryLimit` + +## 5. `containers` / `initContainers` + + +Форма: + +```yaml +containers: + main: + enabled: true + image: + name: app + staticTag: "1.0.0" + command: | + - /bin/app + args: | + - --serve +``` + +Поддерживаемые поля контейнера: + + + + +- `enabled` +- `name` +- `image.name` +- `image.staticTag` +- `image.generateSignatureBasedTag` +- `command` +- `args` +- `envVars` +- `envYAML` +- `env` +- `envFrom` +- `secretEnvVars` +- `fromSecretsEnvVars` +- `resources` +- `lifecycle` +- `livenessProbe` +- `readinessProbe` +- `startupProbe` +- `securityContext` +- `volumeMounts` +- `volumes` +- `ports` +- `configFiles` +- `configFilesYAML` +- `secretConfigFiles` +- `persistantVolumes` + +Навигация: [Parameter Index](parameter-index.md#containers-envconfig) | [Наверх](#top) + +## 6. Env-паттерн + + +Любое поле, поддерживающее env-map: + +```yaml +field: + _default: value + production: value2 + "^prod-.*$": value3 +``` + +Используйте: +- `_default` для базового значения; +- явный env-ключ для таргет окружения; +- regex только когда реально нужен паттерн. + +## 7. Ресурсы контейнера + +Форма: + +```yaml +resources: + requests: + mcpu: 100 + memoryMb: 256 + ephemeralStorageMb: 100 + limits: + mcpu: 500 + memoryMb: 512 +``` + +Поддержка env-map также применима к этим полям. + +## 8. Config files + + + +### 8.1 `configFiles` + +```yaml +configFiles: + app.yaml: + mountPath: /etc/app/app.yaml + content: | + key: value +``` + +### 8.2 `configFilesYAML` + +```yaml +configFilesYAML: + app.yaml: + mountPath: /etc/app/app.yaml + content: + key: + _default: value + production: prod-value +``` + +### 8.3 `secretConfigFiles` + +```yaml +secretConfigFiles: + token.txt: + mountPath: /etc/secret/token.txt + content: super-secret +``` + +Навигация: [Parameter Index](parameter-index.md#containers-envconfig) | [Наверх](#top) + +## 9. Service block + + +Используется: +- как nested `service` у workload; +- как отдельный объект в `apps-services`. + +Типовые поля: +- `enabled` +- `name` +- `ports` +- `selector` +- `type` +- `clusterIP` +- `sessionAffinity` +- `annotations` + +Навигация: [Parameter Index](parameter-index.md#workload) | [Наверх](#top) + +## 10. Ingress block + + +`apps-ingresses.`: +- `class` +- `ingressClassName` +- `host` +- `paths` +- `annotations` +- `tls.enabled` +- `tls.secret_name` +- `dexAuth` + +`dexAuth` поля: +- `enabled` +- `clusterDomain` + +Навигация: [Parameter Index](parameter-index.md#networking-and-scaling) | [Наверх](#top) + +## 11. Autoscaling blocks + + + +### 11.1 `verticalPodAutoscaler` + +- `enabled` +- `updateMode` +- `resourcePolicy` + +### 11.2 `horizontalPodAutoscaler` + +- `enabled` +- `minReplicas` +- `maxReplicas` +- `behavior` +- `metrics` +- `customMetricResources` + + +`customMetricResources.`: +- `enabled` +- `kind` +- `name` (optional) +- `query` + +Навигация: [Parameter Index](parameter-index.md#networking-and-scaling) | [Наверх](#top) + +## 12. `podDisruptionBudget` + + +Поля: +- `enabled` +- `maxUnavailable` +- `minAvailable` + +## 13. `serviceAccount` + + +Поля: +- `enabled` +- `name` +- `clusterRole` + +`clusterRole`: +- `name` +- `rules` + +Навигация: [Parameter Index](parameter-index.md#workload) | [Наверх](#top) + +## 14. Прочие `apps-*` секции + +### 14.1 `apps-configmaps` + +Поля app: +- `data` +- `binaryData` +- `envVars` + +### 14.2 `apps-secrets` + +Поля app: +- `type` +- `data` +- `envVars` + +### 14.3 `apps-pvcs` + +Поля app: +- `storageClassName` +- `accessModes` +- `resources` + +### 14.4 `apps-limit-range` + +Поля app: +- `limits` + +### 14.5 `apps-certificates` + +Поля app: +- `name` (optional override) +- `clusterIssuer` +- `host` +- `hosts` + +### 14.6 `apps-dex-clients` + +Поля app: +- `redirectURIs` (required для включенного ресурса) + +### 14.7 `apps-dex-authenticators` + +Поля app: +- `applicationDomain` +- `applicationIngressClassName` +- `applicationIngressCertificateSecretName` +- `allowedGroups` +- `sendAuthorizationHeader` +- `whitelistSourceRanges` +- `nodeSelector` +- `tolerations` + +### 14.8 `apps-custom-prometheus-rules` + +Поля app: +- `groups` + +Глубже: +- `groups..alerts..isTemplate` +- `groups..alerts..content` + +### 14.9 `apps-grafana-dashboards` + +Поля app: +- `folder` + +Dashboard definition читается из `dashboards/.json`. + +### 14.10 `apps-kafka-strimzi` + +Поля app (основные): +- `kafka` +- `zookeeper` +- `entityOperator` +- `exporter` +- `topics` + +Эта секция специализирована под Strimzi и обычно выносится в отдельный infra/service chart. + +### 14.11 `apps-infra` + +Содержит: +- `node-users` +- `node-groups` + +`node-users.`: +- `enabled` +- `uid` (required) +- `passwordHash` +- `sshPublicKey` +- `sshPublicKeys` +- `extraGroups` +- `nodeGroups` +- `isSudoer` +- `annotations` +- `labels` + +## 15. Custom-группы + +Форма: + +```yaml +group-name: + __GroupVars__: + type: apps-stateless + enabled: true + _preRenderGroupHook: | + {{/* hook */}} + _preRenderAppHook: | + {{/* hook */}} + app-a: + _include: ["apps-stateless-defaultApp"] +``` + +Важные поля `__GroupVars__`: +- `type` (required, может быть как строкой, так и env-map через `global.env`) +- `enabled` +- `_include` +- `_preRenderGroupHook` +- `_preRenderAppHook` + +### 15.1 Custom renderer через `__GroupVars__.type` + +`type` может указывать не только на встроенный `apps-*` рендерер, но и на пользовательский. + +Контракт: +1. В values: + - `__GroupVars__.type: my-custom-type` +2. В шаблонах chart приложения: + - `define "my-custom-type.render"` +3. Библиотека передает стандартный контекст (`$`, `$.CurrentApp`, `$.CurrentGroupVars`, `$.Values`). + +Важно: любые поля app из `group..*` доступны в custom renderer через `$.CurrentApp.*`. + +Полный набор полезных переменных в custom renderer: +- `$` (root context), +- `$.Values`, +- `$.CurrentApp`, +- `$.CurrentGroupVars`, +- `$.CurrentGroup`, +- `$.CurrentPath`, +- `$.Release`, +- `$.Capabilities`, +- `$.Files`. + +Пример с явным пробросом app-полей в `$.CurrentApp`: + +```yaml +custom-services: + __GroupVars__: + type: custom-services + service-a: + enabled: true + host: + ip: service-a.example.local + port: 8080 + extraLabels: + app.kubernetes.io/part-of: platform +``` + +```yaml +{{- define "custom-services.render" -}} +{{- $ := . -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $.CurrentApp.name | quote }} + labels: + app.kubernetes.io/name: {{ $.CurrentApp.name | quote }} + app.kubernetes.io/enabled: {{ printf "%v" $.CurrentApp.enabled | quote }} +{{- with $.CurrentApp.extraLabels }} +{{ toYaml . | indent 4 }} +{{- end }} +data: + kind: "custom-services" + host: {{ printf "%v:%v" $.CurrentApp.host.ip $.CurrentApp.host.port | quote }} +{{- end -}} +``` + +## 16. Полезные ссылки + +- Общая концепция: [docs/library-guide.md](library-guide.md) +- Практические рецепты: [docs/cookbook.md](cookbook.md) +- Индекс параметров: [docs/parameter-index.md](parameter-index.md) +- Рабочие примеры: [tests/.helm/values.yaml](../tests/.helm/values.yaml) +- JSON Schema: [tests/.helm/values.schema.json](../tests/.helm/values.schema.json) + +## 17. Тип поля -> поведение рендера (cheat sheet) + +Ниже быстрый справочник по самым частым типам полей. + +| Поле/группа | Ожидаемый тип в values | Как используется при рендере | +|---|---|---| +| `_include` | `array[string]` | Конкатенируется между include-профилями, затем применяется merge. +| `global.env` | `string` | Выбирает env-значение из map (`_default`, `production`, regex). +| `replicas`, `enabled`, `werfWeight`, `priorityClassName` | scalar или env-map scalar | Резолвится через `fl.value` как скаляр. +| `envVars.` / `secretEnvVars.` | scalar или env-map scalar | Рендерится как env var value. +| `command`, `args`, `ports`, `envFrom`, `affinity`, `tolerations`, `nodeSelector`, `volumes`, `paths`, `rules`, `resourcePolicy` | string или env-map string | Обычно передаются как YAML block string (`|`) и вставляются в манифест. +| `horizontalPodAutoscaler.metrics` | string или object | Поддерживает 2 режима: raw YAML строка или map-конфиг метрик. +| `configFiles..content` | string (обычно) | Контент ConfigMap/файла. +| `configFilesYAML..content` | object | Рекурсивно обрабатывается как YAML-дерево (с `_default` в узлах). +| `apps-*..data` / `binaryData` (ConfigMap/Secret) | string или object | Для ConfigMap/Secret может быть raw YAML string или map. + +Практика: +- если поле описано как Kubernetes-блок, используйте YAML строку (`|`); +- native YAML list в values запрещены (исключения: `_include`, `_include_files`); +- для env-значений используйте scalar/env-map; +- итог всегда проверяйте через `helm template ... --set global.env=`. + +Навигация: [Parameter Index](parameter-index.md) | [Наверх](#top) diff --git a/docs/use-case-map.md b/docs/use-case-map.md new file mode 100644 index 0000000..86e3892 --- /dev/null +++ b/docs/use-case-map.md @@ -0,0 +1,92 @@ +# Helm Apps: Use-Case Map + + +Карта для быстрого выбора решения: +- что нужно сделать; +- какие параметры использовать; +- где взять рабочий пример; +- что проверить перед merge/release. + +## Быстрая навигация + +- [Старт docs](README.md) +- [Parameter Index](parameter-index.md) +- [Reference](reference-values.md) +- [Cookbook](cookbook.md) +- [Operations](operations.md) + +## 1. Нужен обычный HTTP/API сервис + +- Параметры: [containers](reference-values.md#param-containers), [service](reference-values.md#param-service) +- Пример: [Базовый HTTP API](cookbook.md#example-basic-api) +- Проверки: `helm lint`, `helm template ... --set global.env=` + +## 2. Нужен внешний доступ через Ingress + TLS + +- Параметры: [ingress](reference-values.md#param-ingress), [global.env](reference-values.md#param-global-env) +- Пример: [API + Ingress + TLS](cookbook.md#example-ingress-tls) +- Проверки: backend service/port, ingress class, tls secret/certificate +- Ops: [Ingress 404/502](operations.md#46-ingress-есть-но-404502) + +## 3. Нужен CronJob или Job + +- Параметры: [containers](reference-values.md#param-containers), [global._includes/_include](reference-values.md#param-global-includes) +- Пример: [CronJob](cookbook.md#example-cronjob) +- Проверки: schedule, backoffLimit, restartPolicy, image tag + +## 4. Нужны секреты в env + +- Параметры: [secretEnvVars](reference-values.md#param-secretenvvars), [fromSecretsEnvVars](reference-values.md#param-fromsecretsenvvars) +- Примеры: + - [secretEnvVars](cookbook.md#example-secretenvvars) + - [fromSecretsEnvVars](cookbook.md#example-fromsecretsenvvars) +- Проверки: отсутствие plaintext в git, корректность ключей в Secret + +## 5. Нужны файловые конфиги в контейнере + +- Параметры: [configFiles](reference-values.md#param-configfiles), [configFilesYAML](reference-values.md#param-configfilesyaml) +- Примеры: + - [configFiles](cookbook.md#example-configfiles) + - [configFilesYAML](cookbook.md#example-configfilesyaml) +- Проверки: mountPath, формат content, итог рендера ConfigMap/Secret + +## 6. Нужен HPA/VPA + +- Параметры: [horizontalPodAutoscaler](reference-values.md#param-hpa), [hpa.metrics](reference-values.md#param-hpa-metrics), [verticalPodAutoscaler](reference-values.md#param-vpa) +- Пример: [HPA для API](cookbook.md#example-hpa) +- Проверки: min/max, metrics, updateMode, conflicts HPA vs VPA +- Ops: [HPA не скейлит](operations.md#47-hpa-не-скейлит), [VPA не влияет](operations.md#48-vpa-не-влияет-на-pods) + +## 7. Нужен ServiceAccount и RBAC + +- Параметры: [serviceAccount](reference-values.md#param-serviceaccount) +- Пример: [ServiceAccount + ClusterRole](cookbook.md#example-serviceaccount) +- Проверки: role rules, binding namespace, права на нужные API + +## 8. Нужны разные значения для разных окружений + +- Параметры: [global.env](reference-values.md#param-global-env), [_include](reference-values.md#param-include), [global._includes](reference-values.md#param-global-includes) +- Пример: [env recipe](cookbook.md#example-global-env) +- Проверки: + - env задается через `global.env`; + - нет конфликтных regex ключей; + - финальный рендер проверен в каждом target env. + +## 9. Нужно переиспользование и минимум дублирования + +- Параметры: [global._includes](reference-values.md#param-global-includes), [_include](reference-values.md#param-include) +- Пример merge: [README merge section](../README.md#example-global-includes-merge) +- Проверки: + - порядок include осознанный; + - локальные overrides минимальны и понятны; + - финальный рендер совпадает с ожиданием. + +## 10. Быстрый pre-merge чеклист + +1. Сверить параметры в [Parameter Index](parameter-index.md). +2. Прогнать `helm lint`. +3. Прогнать `helm template ... --set global.env=`. +4. Проверить соответствующий раздел в [Operations](operations.md). + +Навигация: [Наверх](#top) + diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh new file mode 100755 index 0000000..c1c7a2f --- /dev/null +++ b/scripts/ci-local.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT_DIR}" + +RUN_SNAPSHOT=1 +RUN_CONTRACTS=1 +RUN_API=1 + +usage() { + cat <<'EOF' +Usage: scripts/ci-local.sh [options] + +Runs local equivalent of .github/workflows/ci.yml:validate. + +Options: + --skip-snapshot Skip render snapshot diff check. + --skip-contracts Skip contracts checks. + --skip-api Skip Kubernetes API compatibility checks. + -h, --help Show this help. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-snapshot) + RUN_SNAPSHOT=0 + shift + ;; + --skip-contracts) + RUN_CONTRACTS=0 + shift + ;; + --skip-api) + RUN_API=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 2 + ;; + esac +done + +need_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required command: $1" >&2 + exit 1 + fi +} + +need_cmd werf + +if ! command -v kubeconform >/dev/null 2>&1; then + echo "Missing required command: kubeconform" >&2 + echo "Install: https://github.com/yannh/kubeconform" >&2 + exit 1 +fi + +if [[ "${RUN_SNAPSHOT}" -eq 1 ]] && ! command -v dyff >/dev/null 2>&1; then + echo "dyff not found: fallback to diff -u for snapshot check." +fi + +APPS_VERSION_FILE="charts/helm-apps/templates/_apps-version.tpl" +TESTS_LOCK="tests/.helm/Chart.lock" +CONTRACTS_LOCK="tests/contracts/Chart.lock" + +backup_file() { + local file="$1" + if [[ -f "${file}" ]]; then + cp "${file}" "${file}.bak.ci-local" + fi +} + +restore_file() { + local file="$1" + if [[ -f "${file}.bak.ci-local" ]]; then + mv "${file}.bak.ci-local" "${file}" + fi +} + +cleanup() { + restore_file "${APPS_VERSION_FILE}" + restore_file "${TESTS_LOCK}" + restore_file "${CONTRACTS_LOCK}" +} +trap cleanup EXIT + +backup_file "${APPS_VERSION_FILE}" +backup_file "${TESTS_LOCK}" +backup_file "${CONTRACTS_LOCK}" + +echo "==> Set library version in ${APPS_VERSION_FILE}" +LIB_VERSION="$(sed -n '/version/{s/version: //;p;}' charts/helm-apps/Chart.yaml)" +sed -i.bak "s/_FLANT_APPS_LIBRARY_VERSION_/${LIB_VERSION}/" "${APPS_VERSION_FILE}" +rm -f "${APPS_VERSION_FILE}.bak" + +echo "==> Update test chart dependencies" +werf helm dependency update tests/.helm + +echo "==> Validate values schema" +werf helm lint tests/.helm --values tests/.helm/values.yaml + +if [[ "${RUN_API}" -eq 1 ]]; then + echo "==> Verify Kubernetes API compatibility" + werf helm template tests tests/.helm \ + --set "global.env=prod" \ + --set "global._includes.apps-defaults.enabled=true" \ + --kube-version 1.29.0 > /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: policy/v1$' /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: batch/v1$' /tmp/tests_k8s_129.yaml + grep -q '^apiVersion: autoscaling/v2$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: policy/v1beta1$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: batch/v1beta1$' /tmp/tests_k8s_129.yaml + ! grep -q '^apiVersion: autoscaling/v2beta2$' /tmp/tests_k8s_129.yaml + kubeconform -strict -summary -ignore-missing-schemas -kubernetes-version 1.29.0 /tmp/tests_k8s_129.yaml + + werf helm template tests tests/.helm \ + --set "global.env=prod" \ + --set "global._includes.apps-defaults.enabled=true" \ + --kube-version 1.20.15 > /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: policy/v1beta1$' /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: batch/v1beta1$' /tmp/tests_k8s_120.yaml + grep -q '^apiVersion: autoscaling/v2beta2$' /tmp/tests_k8s_120.yaml + ! grep -q '^apiVersion: autoscaling/v2$' /tmp/tests_k8s_120.yaml + kubeconform -strict -summary -ignore-missing-schemas -kubernetes-version 1.20.15 /tmp/tests_k8s_120.yaml +fi + +if [[ "${RUN_SNAPSHOT}" -eq 1 ]]; then + echo "==> Render snapshot check" + ( + cd tests + if source "$(werf ci-env github --as-file)" >/dev/null 2>&1; then + echo "Using werf ci-env github context." + else + echo "Cannot load werf ci-env github context; using local context." + fi + + werf render --dev --set "global._includes.apps-defaults.enabled=true" --env=prod | sed '/werf.io\//d' > test_render_check.yaml + + if command -v dyff >/dev/null 2>&1; then + dyff between test_render.yaml test_render_check.yaml | tee /tmp/test_render_check + check_tests="$(sed 1,7d /tmp/test_render_check | wc -l | tr -d ' ')" + if [[ "${check_tests}" -gt "7" ]]; then + echo "Snapshot mismatch: dyff output lines=${check_tests}" >&2 + exit 1 + fi + else + diff -u test_render.yaml test_render_check.yaml + fi + ) +fi + +if [[ "${RUN_CONTRACTS}" -eq 1 ]]; then + echo "==> Update contract chart dependencies" + werf helm dependency update tests/contracts + + echo "==> Contract checks" + werf helm template contracts tests/contracts > /tmp/contracts_render.yaml + grep -q '"A": "2"' /tmp/contracts_render.yaml + grep -q '"LOCAL": "ok"' /tmp/contracts_render.yaml + grep -q '"key2": "local-value-2"' /tmp/contracts_render.yaml + grep -q '"key1": "value-1"' /tmp/contracts_render.yaml + grep -q '"fromBaseA": "A"' /tmp/contracts_render.yaml + grep -q '"fromBaseB": "B"' /tmp/contracts_render.yaml + grep -q '"ENV_SWITCH": "override-default"' /tmp/contracts_render.yaml + werf helm template contracts tests/contracts --set global.env=dev > /tmp/contracts_render_dev.yaml + grep -q '"ENV_SWITCH": "override-default"' /tmp/contracts_render_dev.yaml + grep -q 'paused: true' /tmp/contracts_render.yaml + grep -q 'resizePolicy:' /tmp/contracts_render.yaml + grep -q 'podFailurePolicy:' /tmp/contracts_render.yaml + grep -q 'defaultBackend:' /tmp/contracts_render.yaml + grep -q 'volumeMode: Filesystem' /tmp/contracts_render.yaml + grep -q 'immutable: true' /tmp/contracts_render.yaml + grep -q 'stringData:' /tmp/contracts_render.yaml + grep -q '^apiVersion: networking.k8s.io/v1$' /tmp/contracts_render.yaml + grep -q '^kind: NetworkPolicy$' /tmp/contracts_render.yaml + grep -q '^apiVersion: cilium.io/v2$' /tmp/contracts_render.yaml + grep -q '^kind: CiliumNetworkPolicy$' /tmp/contracts_render.yaml + grep -q '^apiVersion: projectcalico.org/v3$' /tmp/contracts_render.yaml + grep -q 'selector: "app == '\''compat-service'\''"' /tmp/contracts_render.yaml + grep -q 'kubernetes.io/metadata.name: ingress-nginx' /tmp/contracts_render.yaml + grep -q 'port: 53' /tmp/contracts_render.yaml + grep -q 'name: "release-auto-app"' /tmp/contracts_render.yaml + grep -q 'image: alpine:3.19' /tmp/contracts_render.yaml + grep -q 'helm-apps/release: "production-v1"' /tmp/contracts_render.yaml + grep -q 'helm-apps/app-version: "3.19"' /tmp/contracts_render.yaml + grep -q 'name: "compat-route"' /tmp/contracts_render.yaml + grep -q 'host: "route.example.com"' /tmp/contracts_render.yaml + + werf helm template contracts tests/contracts --set global.validation.strict=true > /tmp/contracts_render_strict.yaml + grep -Eq '"custom": ?"ok"|custom: ?"?ok"?' /tmp/contracts_render_strict.yaml + ! werf helm template contracts tests/contracts \ + --set global.validation.strict=true \ + --set apps-network-policies.compat-netpol.typoField=1 >/tmp/contracts_render_strict_fail.yaml + ! werf helm template contracts tests/contracts \ + --set global.validation.strict=true \ + --set apps-typo.bad.enabled=true >/tmp/contracts_render_strict_top_fail.yaml + + werf helm template contracts tests/contracts --kube-version 1.29.0 > /tmp/contracts_render_129.yaml + grep -q 'loadBalancerClass: "internal-vip"' /tmp/contracts_render_129.yaml + grep -q 'internalTrafficPolicy: "Local"' /tmp/contracts_render_129.yaml + + werf helm template contracts tests/contracts --kube-version 1.20.15 > /tmp/contracts_render_120.yaml + ! grep -q 'loadBalancerClass:' /tmp/contracts_render_120.yaml + ! grep -q 'internalTrafficPolicy:' /tmp/contracts_render_120.yaml + grep -q 'ipFamilyPolicy: "SingleStack"' /tmp/contracts_render_120.yaml + grep -q 'allocateLoadBalancerNodePorts: true' /tmp/contracts_render_120.yaml + + werf helm template contracts tests/contracts --kube-version 1.19.16 > /tmp/contracts_render_119.yaml + ! grep -q 'loadBalancerClass:' /tmp/contracts_render_119.yaml + ! grep -q 'internalTrafficPolicy:' /tmp/contracts_render_119.yaml + ! grep -q 'ipFamilyPolicy:' /tmp/contracts_render_119.yaml + ! grep -q 'ipFamilies:' /tmp/contracts_render_119.yaml + ! grep -q 'allocateLoadBalancerNodePorts:' /tmp/contracts_render_119.yaml + + cat > /tmp/contracts_invalid_native_list.yaml <<'EOF' +apps-stateless: + compat-service: + service: + ports: + - name: http + port: 80 + targetPort: 8080 +EOF + ! werf helm template contracts tests/contracts \ + --values /tmp/contracts_invalid_native_list.yaml \ + >/tmp/contracts_invalid_native_list.out 2>/tmp/contracts_invalid_native_list.err + grep -q "list value is not allowed at Values.apps-stateless.compat-service.service.ports" /tmp/contracts_invalid_native_list.err + + werf helm template contracts tests/contracts \ + --values tests/contracts/values.internal-compat.yaml > /tmp/contracts_internal_like.yaml + grep -q 'name: "compat-web"' /tmp/contracts_internal_like.yaml + grep -q 'image: alpine:1.2.3' /tmp/contracts_internal_like.yaml + grep -q 'helm-apps/release:' /tmp/contracts_internal_like.yaml + grep -q 'helm-apps/app-version: "1.2.3"' /tmp/contracts_internal_like.yaml + grep -q 'name: "compat-route"' /tmp/contracts_internal_like.yaml + grep -q 'host: "compat.example.com"' /tmp/contracts_internal_like.yaml +fi + +echo "Local CI validate checks passed." diff --git a/tests/.helm/Chart.lock b/tests/.helm/Chart.lock index 49cf84f..e0b4cb5 100644 --- a/tests/.helm/Chart.lock +++ b/tests/.helm/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: helm-apps repository: file://../../charts/helm-apps/ - version: 1.0.6 -digest: sha256:cdc41e0cff7749746c9a970b09861d84841b61edfa353523e12b6a9fcb6a902e -generated: "2022-09-28T20:16:33.909062+03:00" + version: 1.6.1 +digest: sha256:c78cbe4c3c383be4816798447f86a450297b7b3962a1ead595d59a2bf9101ca4 +generated: "2026-02-16T16:27:28.344362+03:00" diff --git a/tests/.helm/config/test-include-files.yaml b/tests/.helm/config/test-include-files.yaml new file mode 100644 index 0000000..d4bc08d --- /dev/null +++ b/tests/.helm/config/test-include-files.yaml @@ -0,0 +1,44 @@ +testIncludeVar: + _default: redaultvalue from file + prod: prod value from file +testIncludeVarMustOverwrited: + _default: default from file + prod: prod value from file +overwrited: value from file +testValue: value from file +testValueWithDefault: + _default: default value from file +level1: + testSlice: + _default: + - test1 + - test2 + prod: + - testProd + - testProd2 + testMap: + testProd1: 1 + mustDeleted: 1 + testMap2: + testNestedMap: + _default: + nestedMap: + test: 1 + prod: 1 + test: 1 + test: 1 + test2: 1 + test3: + _default: 1 + prod: 3 + deletedInProd: + _default: test + prod: null + deletedInProdToo: + test: + test2: + _default: test + prod: null + test3: + _default: null + diff --git a/tests/.helm/helm-apps-defaults.yaml b/tests/.helm/helm-apps-defaults.yaml new file mode 100644 index 0000000..95649ed --- /dev/null +++ b/tests/.helm/helm-apps-defaults.yaml @@ -0,0 +1,102 @@ +apps-defaults: + enabled: false +apps-default-library-app: + _include: ["apps-defaults"] + # CLIENT: ask if this is ok for a defaul + imagePullSecrets: | + - name: registrysecret +## Конфигурация по умолчанию для CronJob в целом. +apps-cronjobs-defaultCronJob: + _include: ["apps-default-library-app"] + concurrencyPolicy: "Forbid" + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 1 + backoffLimit: 0 + priorityClassName: + prod: "production-high" + restartPolicy: "Never" + startingDeadlineSeconds: 60 + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + resourcePolicy: | + {} + +apps-secrets-defaultSecret: + _include: ["apps-defaults"] + +apps-ingresses-defaultIngress: + _include: ["apps-defaults"] + class: "nginx" + +apps-jobs-defaultJob: + _include: ["apps-default-library-app"] + backoffLimit: 0 + priorityClassName: + prod: "production-high" + restartPolicy: "Never" + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + resourcePolicy: | + {} + +apps-stateful-defaultApp: + _include: ["apps-default-library-app"] + revisionHistoryLimit: 3 + terminationGracePeriodSeconds: + _default: 30 + prod: 60 + affinity: | + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: {{ include "fl.generateSelectorLabels" (list $ . .name) | nindent 22 }} + topologyKey: kubernetes.io/hostname + weight: 10 + priorityClassName: + prod: "production-medium" + podDisruptionBudget: + enabled: true + maxUnavailable: "15%" + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + service: + enabled: false + name: "{{ $.CurrentApp.name }}" + headless: true + +apps-stateless-defaultApp: + _include: ["apps-default-library-app"] + revisionHistoryLimit: 3 + strategy: + _default: | + rollingUpdate: + maxSurge: 20% + maxUnavailable: 50% + type: RollingUpdate + prod: | + rollingUpdate: + maxSurge: 20% + maxUnavailable: 25% + type: RollingUpdate + priorityClassName: + prod: "production-medium" + podDisruptionBudget: + enabled: true + maxUnavailable: "15%" + verticalPodAutoscaler: + enabled: true + updateMode: "Off" + resourcePolicy: | + {} + horizontalPodAutoscaler: + enabled: false + service: + enabled: false + name: "{{ $.CurrentApp.name }}" + +apps-configmaps-defaultConfigmap: + _include: ["apps-defaults"] diff --git a/tests/.helm/values.schema.json b/tests/.helm/values.schema.json new file mode 100644 index 0000000..f6b37b4 --- /dev/null +++ b/tests/.helm/values.schema.json @@ -0,0 +1,953 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://helm-apps.local/tests/.helm/values.schema.json", + "title": "helm-apps values", + "type": "object", + "properties": { + "global": { + "$ref": "#/$defs/global" + }, + "apps-configmaps": { + "$ref": "#/$defs/appMap" + }, + "apps-cronjobs": { + "$ref": "#/$defs/appMap" + }, + "apps-ingresses": { + "$ref": "#/$defs/appMap" + }, + "apps-jobs": { + "$ref": "#/$defs/appMap" + }, + "apps-secrets": { + "$ref": "#/$defs/appMap" + }, + "apps-stateful": { + "$ref": "#/$defs/appMap" + }, + "apps-stateless": { + "$ref": "#/$defs/appMap" + }, + "apps-custom-prometheus-rules": { + "$ref": "#/$defs/appMap" + }, + "apps-limit-range": { + "$ref": "#/$defs/appMap" + }, + "apps-pvcs": { + "$ref": "#/$defs/appMap" + }, + "apps-certificates": { + "$ref": "#/$defs/appMap" + }, + "apps-kafka-strimzi": { + "$ref": "#/$defs/appMap" + }, + "apps-dex-authenticators": { + "$ref": "#/$defs/appMap" + }, + "apps-dex-clients": { + "$ref": "#/$defs/appMap" + }, + "apps-grafana-dashboards": { + "$ref": "#/$defs/appMap" + }, + "apps-services": { + "$ref": "#/$defs/appMap" + }, + "apps-network-policies": { + "$ref": "#/$defs/networkPolicyAppMap" + }, + "apps-infra": { + "$ref": "#/$defs/appsInfra" + }, + "werf": { + "type": "object", + "additionalProperties": true + }, + "helm-apps": { + "type": "object", + "additionalProperties": true + } + }, + "$defs": { + "scalar": { + "type": ["string", "number", "integer", "boolean", "null"] + }, + "envStringValue": { + "description": "Строка или map по окружениям, где значения строковые.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": { + "type": ["string", "null"] + } + } + ] + }, + "yamlScalar": { + "oneOf": [ + { + "$ref": "#/$defs/scalar" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + }, + "envValue": { + "description": "Обычное значение или map вида {_default: ..., prod: ..., ^prod.*: ...}.", + "oneOf": [ + { + "$ref": "#/$defs/scalar" + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/scalar" + } + } + ] + }, + "envMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/yamlScalar" + } + }, + "resources": { + "type": "object", + "properties": { + "requests": { + "$ref": "#/$defs/resourcesGroup" + }, + "limits": { + "$ref": "#/$defs/resourcesGroup" + } + }, + "additionalProperties": false + }, + "resourcesGroup": { + "type": "object", + "properties": { + "mcpu": { + "$ref": "#/$defs/envValue" + }, + "memoryMb": { + "$ref": "#/$defs/envValue" + }, + "ephemeralStorageMb": { + "$ref": "#/$defs/envValue" + } + }, + "additionalProperties": true + }, + "image": { + "type": "object", + "properties": { + "name": { + "$ref": "#/$defs/envValue" + }, + "staticTag": { + "$ref": "#/$defs/envValue" + }, + "generateSignatureBasedTag": { + "$ref": "#/$defs/envValue" + } + }, + "required": ["name"], + "additionalProperties": true + }, + "configFile": { + "type": "object", + "properties": { + "name": { + "$ref": "#/$defs/envValue" + }, + "mountPath": { + "$ref": "#/$defs/envValue" + }, + "defaultMode": { + "$ref": "#/$defs/envValue" + }, + "content": { + "$ref": "#/$defs/yamlScalar" + } + }, + "additionalProperties": true + }, + "container": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "name": { + "$ref": "#/$defs/envValue" + }, + "image": { + "$ref": "#/$defs/image" + }, + "command": { + "$ref": "#/$defs/envStringValue" + }, + "args": { + "$ref": "#/$defs/envStringValue" + }, + "envVars": { + "$ref": "#/$defs/envMap" + }, + "envYAML": { + "type": "object", + "additionalProperties": true + }, + "secretEnvVars": { + "$ref": "#/$defs/envMap" + }, + "fromSecretsEnvVars": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/envValue" + } + } + }, + "env": { + "$ref": "#/$defs/envStringValue" + }, + "envFrom": { + "$ref": "#/$defs/envStringValue" + }, + "resources": { + "$ref": "#/$defs/resources" + }, + "lifecycle": { + "$ref": "#/$defs/envStringValue" + }, + "livenessProbe": { + "$ref": "#/$defs/envStringValue" + }, + "readinessProbe": { + "$ref": "#/$defs/envStringValue" + }, + "startupProbe": { + "$ref": "#/$defs/envStringValue" + }, + "securityContext": { + "$ref": "#/$defs/envStringValue" + }, + "volumeMounts": { + "$ref": "#/$defs/envStringValue" + }, + "volumes": { + "$ref": "#/$defs/envStringValue" + }, + "ports": { + "$ref": "#/$defs/envStringValue" + }, + "configFiles": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/configFile" + } + }, + "configFilesYAML": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/configFile" + } + }, + { + "type": "null" + } + ] + }, + "secretConfigFiles": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/configFile" + } + }, + "persistantVolumes": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "mountPath": { + "$ref": "#/$defs/envValue" + }, + "size": { + "$ref": "#/$defs/envValue" + }, + "storageClass": { + "$ref": "#/$defs/envValue" + }, + "accessModes": { + "$ref": "#/$defs/envStringValue" + } + }, + "required": ["mountPath", "size", "storageClass"], + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "vpa": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "updateMode": { + "$ref": "#/$defs/envValue" + }, + "resourcePolicy": { + "$ref": "#/$defs/envStringValue" + } + }, + "additionalProperties": true + }, + "hpa": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "minReplicas": { + "$ref": "#/$defs/envValue" + }, + "maxReplicas": { + "$ref": "#/$defs/envValue" + }, + "behavior": { + "$ref": "#/$defs/envStringValue" + }, + "metrics": { + "$ref": "#/$defs/yamlScalar" + }, + "customMetricResources": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "kind": { + "$ref": "#/$defs/envValue" + }, + "name": { + "$ref": "#/$defs/envValue" + }, + "query": { + "$ref": "#/$defs/envValue" + } + }, + "required": ["kind", "query"], + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "pdb": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "maxUnavailable": { + "$ref": "#/$defs/envValue" + }, + "minAvailable": { + "$ref": "#/$defs/envValue" + } + }, + "additionalProperties": true + }, + "service": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "name": { + "$ref": "#/$defs/envValue" + }, + "headless": { + "$ref": "#/$defs/envValue" + }, + "ports": { + "$ref": "#/$defs/envStringValue" + }, + "selector": { + "$ref": "#/$defs/envStringValue" + }, + "annotations": { + "$ref": "#/$defs/envStringValue" + } + }, + "additionalProperties": true + }, + "serviceAccount": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "name": { + "$ref": "#/$defs/envValue" + }, + "clusterRole": { + "type": "object", + "properties": { + "name": { + "$ref": "#/$defs/envValue" + }, + "rules": { + "$ref": "#/$defs/envStringValue" + } + }, + "required": ["name", "rules"], + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "tls": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "secret_name": { + "$ref": "#/$defs/envValue" + } + }, + "additionalProperties": true + }, + "app": { + "type": "object", + "properties": { + "_include": { + "type": "array", + "items": { + "type": "string" + } + }, + "__AppType__": { + "type": "string" + }, + "enabled": { + "$ref": "#/$defs/envValue" + }, + "name": { + "$ref": "#/$defs/envValue" + }, + "randomName": { + "$ref": "#/$defs/envValue" + }, + "alwaysRestart": { + "$ref": "#/$defs/envValue" + }, + "werfWeight": { + "$ref": "#/$defs/envValue" + }, + "releaseKey": { + "$ref": "#/$defs/envValue" + }, + "annotations": { + "$ref": "#/$defs/envStringValue" + }, + "labels": { + "$ref": "#/$defs/envStringValue" + }, + "selector": { + "$ref": "#/$defs/envStringValue" + }, + "containers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/container" + }, + { + "$ref": "#/$defs/yamlScalar" + }, + { + "type": "object", + "additionalProperties": true + } + ] + } + }, + "initContainers": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/container" + }, + { + "$ref": "#/$defs/yamlScalar" + }, + { + "type": "object", + "additionalProperties": true + } + ] + } + }, + "verticalPodAutoscaler": { + "$ref": "#/$defs/vpa" + }, + "horizontalPodAutoscaler": { + "$ref": "#/$defs/hpa" + }, + "podDisruptionBudget": { + "$ref": "#/$defs/pdb" + }, + "service": { + "anyOf": [ + { + "$ref": "#/$defs/service" + }, + { + "$ref": "#/$defs/envValue" + }, + { + "$ref": "#/$defs/envStringValue" + } + ] + }, + "serviceAccount": { + "$ref": "#/$defs/serviceAccount" + }, + "schedule": { + "$ref": "#/$defs/envValue" + }, + "concurrencyPolicy": { + "$ref": "#/$defs/envValue" + }, + "successfulJobsHistoryLimit": { + "$ref": "#/$defs/envValue" + }, + "failedJobsHistoryLimit": { + "$ref": "#/$defs/envValue" + }, + "startingDeadlineSeconds": { + "$ref": "#/$defs/envValue" + }, + "backoffLimit": { + "$ref": "#/$defs/envValue" + }, + "activeDeadlineSeconds": { + "$ref": "#/$defs/envValue" + }, + "restartPolicy": { + "$ref": "#/$defs/envValue" + }, + "priorityClassName": { + "$ref": "#/$defs/envValue" + }, + "affinity": { + "$ref": "#/$defs/envStringValue" + }, + "tolerations": { + "$ref": "#/$defs/envStringValue" + }, + "nodeSelector": { + "$ref": "#/$defs/yamlScalar" + }, + "topologySpreadConstraints": { + "$ref": "#/$defs/envStringValue" + }, + "volumes": { + "$ref": "#/$defs/envStringValue" + }, + "imagePullSecrets": { + "$ref": "#/$defs/envStringValue" + }, + "ingressClassName": { + "$ref": "#/$defs/envValue" + }, + "class": { + "$ref": "#/$defs/envValue" + }, + "host": { + "$ref": "#/$defs/envValue" + }, + "hosts": { + "$ref": "#/$defs/envStringValue" + }, + "paths": { + "$ref": "#/$defs/envStringValue" + }, + "tls": { + "$ref": "#/$defs/tls" + }, + "dexAuth": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "clusterDomain": { + "$ref": "#/$defs/envValue" + } + }, + "additionalProperties": true + }, + "type": { + "$ref": "#/$defs/envValue" + }, + "data": { + "$ref": "#/$defs/envStringValue" + }, + "binaryData": { + "$ref": "#/$defs/envStringValue" + }, + "envVars": { + "$ref": "#/$defs/envMap" + }, + "limits": { + "$ref": "#/$defs/envStringValue" + }, + "storageClassName": { + "$ref": "#/$defs/envValue" + }, + "accessModes": { + "$ref": "#/$defs/envStringValue" + }, + "resources": { + "$ref": "#/$defs/yamlScalar" + }, + "clusterIssuer": { + "$ref": "#/$defs/envValue" + }, + "groups": { + "type": "object", + "additionalProperties": true + }, + "folder": { + "$ref": "#/$defs/envValue" + }, + "redirectURIs": { + "$ref": "#/$defs/envStringValue" + }, + "applicationDomain": { + "$ref": "#/$defs/envValue" + }, + "applicationIngressCertificateSecretName": { + "$ref": "#/$defs/envValue" + }, + "applicationIngressClassName": { + "$ref": "#/$defs/envValue" + }, + "allowedGroups": { + "$ref": "#/$defs/envStringValue" + }, + "sendAuthorizationHeader": { + "$ref": "#/$defs/envValue" + }, + "kafka": { + "type": "object", + "additionalProperties": true + }, + "zookeeper": { + "type": "object", + "additionalProperties": true + }, + "topics": { + "type": "object", + "additionalProperties": true + }, + "entityOperator": { + "type": "object", + "additionalProperties": true + }, + "exporter": { + "type": "object", + "additionalProperties": true + }, + "deckhouseMetrics": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "appMap": { + "type": "object", + "properties": { + "__GroupVars__": { + "$ref": "#/$defs/customGroupVars" + } + }, + "patternProperties": { + "^[A-Za-z0-9][A-Za-z0-9_.-]*$": { + "$ref": "#/$defs/app" + } + }, + "additionalProperties": false + }, + "networkPolicyType": { + "description": "Тип реализации NetworkPolicy.", + "oneOf": [ + { + "type": "string", + "enum": ["kubernetes", "cilium", "calico"] + }, + { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": ["kubernetes", "cilium", "calico"] + } + } + ] + }, + "networkPolicyApp": { + "type": "object", + "properties": { + "_include": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "$ref": "#/$defs/envValue" + }, + "name": { + "$ref": "#/$defs/envValue" + }, + "annotations": { + "$ref": "#/$defs/envStringValue" + }, + "labels": { + "$ref": "#/$defs/envStringValue" + }, + "type": { + "$ref": "#/$defs/networkPolicyType" + }, + "apiVersion": { + "$ref": "#/$defs/envValue" + }, + "kind": { + "$ref": "#/$defs/envValue" + }, + "spec": { + "$ref": "#/$defs/yamlScalar" + }, + "podSelector": { + "$ref": "#/$defs/envStringValue" + }, + "policyTypes": { + "$ref": "#/$defs/envStringValue" + }, + "ingress": { + "$ref": "#/$defs/envStringValue" + }, + "egress": { + "$ref": "#/$defs/envStringValue" + }, + "endpointSelector": { + "$ref": "#/$defs/envStringValue" + }, + "ingressDeny": { + "$ref": "#/$defs/envStringValue" + }, + "egressDeny": { + "$ref": "#/$defs/envStringValue" + }, + "selector": { + "$ref": "#/$defs/envValue" + }, + "types": { + "$ref": "#/$defs/envStringValue" + }, + "extraSpec": { + "$ref": "#/$defs/yamlScalar" + } + }, + "additionalProperties": true + }, + "networkPolicyAppMap": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9][A-Za-z0-9_.-]*$": { + "$ref": "#/$defs/networkPolicyApp" + } + }, + "additionalProperties": false + }, + "customGroupVars": { + "type": "object", + "properties": { + "_include": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "$ref": "#/$defs/envValue" + }, + "type": { + "$ref": "#/$defs/envValue" + }, + "_preRenderGroupHook": { + "$ref": "#/$defs/envStringValue" + }, + "_preRenderAppHook": { + "$ref": "#/$defs/envStringValue" + } + }, + "required": ["type"], + "additionalProperties": true + }, + "customGroup": { + "type": "object", + "properties": { + "__GroupVars__": { + "$ref": "#/$defs/customGroupVars" + } + }, + "required": ["__GroupVars__"], + "patternProperties": { + "^[A-Za-z0-9][A-Za-z0-9_.-]*$": { + "$ref": "#/$defs/app" + } + }, + "additionalProperties": false + }, + "appsInfraNodeUser": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "uid": { + "$ref": "#/$defs/envValue" + }, + "passwordHash": { + "$ref": "#/$defs/envValue" + }, + "sshPublicKey": { + "$ref": "#/$defs/envValue" + }, + "sshPublicKeys": { + "$ref": "#/$defs/envStringValue" + }, + "extraGroups": { + "$ref": "#/$defs/envStringValue" + }, + "nodeGroups": { + "$ref": "#/$defs/envStringValue" + }, + "isSudoer": { + "$ref": "#/$defs/envValue" + }, + "labels": { + "$ref": "#/$defs/envStringValue" + }, + "annotations": { + "$ref": "#/$defs/envStringValue" + } + }, + "required": ["uid"], + "additionalProperties": true + }, + "appsInfra": { + "type": "object", + "properties": { + "node-users": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9][A-Za-z0-9_.-]*$": { + "$ref": "#/$defs/appsInfraNodeUser" + } + }, + "additionalProperties": false + }, + "node-groups": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9][A-Za-z0-9_.-]*$": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + }, + "global": { + "type": "object", + "properties": { + "ci_url": { + "$ref": "#/$defs/envValue" + }, + "env": { + "type": "string" + }, + "_includes": { + "type": "object", + "additionalProperties": true + }, + "validation": { + "type": "object", + "properties": { + "strict": { + "type": "boolean", + "description": "Opt-in strict mode contract. Default=false for 1.x compatibility." + } + }, + "additionalProperties": true + }, + "release": { + "type": "object", + "properties": { + "enabled": { + "$ref": "#/$defs/envValue" + }, + "current": { + "$ref": "#/$defs/envValue" + }, + "autoEnableApps": { + "$ref": "#/$defs/envValue" + }, + "versions": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/envValue" + } + } + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true +} diff --git a/tests/.helm/values.yaml b/tests/.helm/values.yaml index e50bb86..d96b607 100644 --- a/tests/.helm/values.yaml +++ b/tests/.helm/values.yaml @@ -1,5 +1,15 @@ global: ci_url: example.com + release: + enabled: false + current: "2026.02" + autoEnableApps: true + versions: + "2026.02": + app-1: "1.2.3" + # validation: + # # Opt-in strict checks (currently applied for apps-network-policies). + # strict: false ## Альтернатива ограниченным yaml-алиасам Helm'а. Даёт возможность не дублировать одну и ту же конфигурацию много раз. # # Здесь, в "global._includes", объявляются блоки конфигурации, которые потом можно использовать в любых values-файлах. @@ -19,109 +29,48 @@ global: # # Подробнее: https://github.com/flant/helm-charts/tree/master/.helm/charts/flant-lib#flexpandincludesinvalues-function _includes: - apps-defaults: - enabled: false - apps-default-library-app: - _include: ["apps-defaults"] - # CLIENT: ask if this is ok for a defaul - imagePullSecrets: | - - name: registrysecret - ## Конфигурация по умолчанию для CronJob в целом. - apps-cronjobs-defaultCronJob: - _include: ["apps-default-library-app"] - concurrencyPolicy: "Forbid" - successfulJobsHistoryLimit: 1 - failedJobsHistoryLimit: 1 - backoffLimit: 0 - priorityClassName: - prod: "production-high" - restartPolicy: "Never" - startingDeadlineSeconds: 60 - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - resourcePolicy: | - {} - - apps-secrets-defaultSecret: - _include: ["apps-defaults"] - - apps-ingresses-defaultIngress: - _include: ["apps-defaults"] - class: "nginx" + _include_from_file: helm-apps-defaults.yaml + test-include-from-file: + _include_from_file: config/test-include-from-file.yaml - apps-jobs-defaultJob: - _include: ["apps-default-library-app"] - backoffLimit: 0 - priorityClassName: - prod: "production-high" - restartPolicy: "Never" - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - resourcePolicy: | - {} - - apps-stateful-defaultApp: - _include: ["apps-default-library-app"] - revisionHistoryLimit: 3 - terminationGracePeriodSeconds: - _default: 30 - prod: 60 - affinity: | - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: {{ include "fl.generateSelectorLabels" (list $ . .name) | nindent 22 }} - topologyKey: kubernetes.io/hostname - weight: 10 - priorityClassName: - prod: "production-medium" - podDisruptionBudget: - enabled: true - maxUnavailable: "15%" - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - service: - enabled: false - name: "{{ $.CurrentApp.name }}" - headless: true +# Compatibility examples for real-world project layouts: +# top-level service keys that are not rendered by helm-apps directly, +# but should still pass schema validation. +jwtSigningMethod: rsa +_include_files: + - deployments-values.yaml + - values-app-versions.yaml - apps-stateless-defaultApp: - _include: ["apps-default-library-app"] - revisionHistoryLimit: 3 - strategy: - _default: | - rollingUpdate: - maxSurge: 20% - maxUnavailable: 50% - type: RollingUpdate - prod: | - rollingUpdate: - maxSurge: 20% - maxUnavailable: 25% - type: RollingUpdate - priorityClassName: - prod: "production-medium" - podDisruptionBudget: - enabled: true - maxUnavailable: "15%" - verticalPodAutoscaler: - enabled: true - updateMode: "Off" - resourcePolicy: | - {} - horizontalPodAutoscaler: - enabled: false - service: - enabled: false - name: "{{ $.CurrentApp.name }}" - - apps-configmaps-defaultConfigmap: - _include: ["apps-defaults"] +custom-services: + __GroupVars__: + type: custom-services + minio: + enabled: false + host: + ip: + _default: 127.0.0.1 + port: 9000 +apps-routes: + __GroupVars__: + type: + _default: apps-ingresses + paths: | + - path: / + pathType: Prefix + backend: + service: + name: compat-service + port: + number: 80 + _groupPreRenderHook: | + {{- if not (hasKey $.CurrentApp "paths") }} + {{- $_ := set $.CurrentApp "paths" $.CurrentGroupVars.paths }} + {{- end }} + route-disabled: + enabled: false + host: route.example.com + service: compat-service ## Имя чарта. Ниже перечисляются ConfigMaps для развертывания. # Указано в .helm/requirements.yaml в репозитории приложения в ключах dependencies.name или dependencies.alias. @@ -141,12 +90,20 @@ apps-configmaps: configline2 something.conf: | configline1 + binaryData: | + test.gz: jdbkjbkjsdbkjdbjdsbljl ## Содержание ConfigMap'а. Несекретные переменные окружения, пробрасываемые в контейнеры. # По итогу пробросится в ConfigMap.data. # https://github.com/flant/helm-charts/tree/master/.helm/charts/flant-lib#flgeneratecontainerenvvars-template envVars: TEST1: "val1" TEST2: "val2" + configmap-2: + _include: ["apps-configmaps-defaultConfigmap"] + data: + nginx.conf: | + configline1 + configline2 ## Имя чарта. Ниже перечисляются CronJob'ы для развертывания. # Указано в .helm/requirements.yaml в репозитории приложения в ключах dependencies.name или dependencies.alias. @@ -1923,6 +1880,8 @@ test-hpa: type: apps-stateless hpa-app: _include: ["apps-stateless-defaultApp"] + selector: | + app: my-selector containers: main: image: @@ -1978,7 +1937,13 @@ test-hpa: # https://deckhouse.io/ru/documentation/v1/modules/301-prometheus-metrics-adapter/cr.html query: 'sum(rate(sidekiq_jobs_enqueued_total{<<.LabelMatchers>>, queue="default"}[1m])) by (<<.GroupBy>>)' service: + enabled: true name: "{{ $.CurrentApp.name }}" + ports: | + - name: http + port: 80 + selector: | + app: my-selector apps-services: service-1: @@ -1988,7 +1953,7 @@ apps-services: port: 80 selector: | app: test-app - + test-env-yaml: __GroupVars__: type: apps-stateless @@ -1996,6 +1961,9 @@ test-env-yaml: _include: ["apps-stateless-defaultApp"] containers: test: + secretEnvVars: + TEST_APP_LEVEL1_TEST_ENV3: + _default: default value from envVars envVars: TESTAPP_LEVEL1_TESTENV3: _default: default value from envVars @@ -2023,3 +1991,152 @@ test-env-yaml: level2: test: _default: default value #ошибки нет, в описании перемнной обязательно должен быть ключ "_default" + configFiles: + conf: + mountPath: /config + content: | + test + configFilesYAML: + config: + mountPath: /config + content: + _include_files: ['config/test-include-{{ print "files" }}.yaml'] + overwrited: + _default: default value from values.yaml + testIncludeVar: + _default: default value from values.yaml + testIncludeVarMustOverwrited: + _default: default value from values.yaml + prod: prod value from values.yaml + level1: + testMap: #эта мапка перезапишется + _default: + test1: 2v + test2: 2v + prod: + testProd2: 2v + testMap2: #эта мапка смержится + testNestedMap: + nestedMap: + test: 3v + test: 2v + test2: + _default: 2v + prod: 3v + test3: + _default: 2v + +# Дополнительные примеры для покрытия кейсов, которые не были описаны выше. +# Все примеры ниже выключены (enabled: false), чтобы не влиять на текущий рендер тестов. + +apps-dex-authenticators: + dex-auth-example: + _include: ["apps-default-library-app"] + enabled: false + applicationDomain: "example.org" + applicationIngressClassName: "nginx" + applicationIngressCertificateSecretName: "example-tls" + allowedGroups: | + - team-admins + - team-devops + sendAuthorizationHeader: + _default: "false" + production: "true" + +apps-grafana-dashboards: + dashboard-example: + _include: ["apps-default-library-app"] + enabled: false + folder: "Custom" + +apps-network-policies: + netpol-example: + _include: ["apps-network-policies-defaultNetworkPolicy"] + enabled: false + type: kubernetes + # CNI-agnostic policy: only Kubernetes standard NetworkPolicy fields. + podSelector: | + matchLabels: + app: demo + policyTypes: | + - Ingress + - Egress + ingress: | + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + egress: | + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + cilium-netpol-example: + enabled: false + type: cilium + # For Cilium/other CNI-specific policies pass native spec directly. + spec: | + endpointSelector: + matchLabels: + app: demo + egress: + - toEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: kube-system + +apps-infra: + node-users: + infra-user-example: + enabled: false + uid: 2001 + isSudoer: true + sshPublicKeys: | + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDemoKey1 + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDemoKey2 + extraGroups: | + - wheel + - docker + nodeGroups: | + - worker + annotations: | + owner: platform-team + labels: | + role: ops + node-groups: + infra-group-example: + enabled: false + +test-env-overrides: + __GroupVars__: + type: apps-stateless + env-overrides-app: + _include: ["apps-stateless-defaultApp"] + enabled: false + containers: + main: + image: + name: nginx + staticTag: "1.27" + envVars: + SIMPLE_VALUE: + _default: "from-default" + production: "from-production" + REGEX_ENV_VALUE: + _default: "fallback" + "^prod.*$": "from-regex-prod" + "^dev.*$": "from-regex-dev" + DELETE_IN_PROD: + _default: "value-exists" + production: "" + envYAML: + nested: + level1: + timeoutSeconds: + _default: 30 + production: 60 + retries: + _default: 3 + "^dev.*$": 1 diff --git a/tests/contracts/Chart.lock b/tests/contracts/Chart.lock new file mode 100644 index 0000000..627bb2e --- /dev/null +++ b/tests/contracts/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: helm-apps + repository: file://../../charts/helm-apps/ + version: 1.6.1 +digest: sha256:c78cbe4c3c383be4816798447f86a450297b7b3962a1ead595d59a2bf9101ca4 +generated: "2026-02-16T16:27:29.256809+03:00" diff --git a/tests/contracts/Chart.yaml b/tests/contracts/Chart.yaml new file mode 100644 index 0000000..7f49383 --- /dev/null +++ b/tests/contracts/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: contracts +version: 1.0.0 +dependencies: + - name: helm-apps + repository: "file://../../charts/helm-apps/" + version: ~1 diff --git a/tests/contracts/templates/init-helm-apps-library.yaml b/tests/contracts/templates/init-helm-apps-library.yaml new file mode 100644 index 0000000..4e64ccc --- /dev/null +++ b/tests/contracts/templates/init-helm-apps-library.yaml @@ -0,0 +1 @@ +{{- include "apps-utils.init-library" $ }} diff --git a/tests/contracts/values.internal-compat.yaml b/tests/contracts/values.internal-compat.yaml new file mode 100644 index 0000000..4c84cdd --- /dev/null +++ b/tests/contracts/values.internal-compat.yaml @@ -0,0 +1,91 @@ +global: + env: dev + _includes: + internal-default-app: + enabled: false + _preRenderHook: | + {{- $_ := set $ "CurrentReleaseVersion" (include "fl.value" (list $ . $.Values.deploy.release)) }} + {{- $release := index $.Values.releases $.CurrentReleaseVersion }} + {{- if empty $release }}{{ fail (printf "Not such release! [%s]" $.CurrentReleaseVersion) }}{{ end }} + {{- $appVersion := index $release $.CurrentApp.name }} + {{- if $appVersion }} + {{- if $.Values.deploy.enabled }}{{ $_ := set $.CurrentApp "enabled" true }}{{ end }} + {{- $_ = set $.CurrentApp "CurrentAppVersion" (include "fl.value" (list $ . $appVersion)) }} + {{- end }} + +deploy: + enabled: true + release: "r1" + +releases: + r1: + compat-web: "1.2.3" + +jwtSigningMethod: rsa +_include_files: + - deployments-values.yaml + +custom-services: + __GroupVars__: + type: custom-services + minio: + enabled: false + host: + ip: + _default: 127.0.0.1 + port: 9000 + +apps-stateless: + __GroupVars__: + type: + _default: apps-stateless + _groupPreRenderHook: | + {{- range $_, $container := .containers }} + {{- if and (hasKey $container "resources") (kindIs "map" $container.resources) (hasKey $container.resources "limits") (kindIs "map" $container.resources.limits) }} + {{- if not (hasKey $container.resources "requests") }} + {{- $_ := set $container.resources "requests" dict }} + {{- end }} + {{- if hasKey $container.resources.limits "memoryMb" }} + {{- $_ := set $container.resources.requests "memoryMb" $container.resources.limits.memoryMb }} + {{- end }} + {{- end }} + {{- end }} + compat-web: + _include: ["internal-default-app"] + enabled: false + containers: + main: + image: + name: alpine + command: | + - sh + args: | + - -c + - sleep 3600 + ports: | + - name: http + containerPort: 8080 + resources: + limits: + memoryMb: "128" + +apps-routes-contract: + __GroupVars__: + type: + _default: apps-ingresses + paths: | + - path: / + pathType: Prefix + backend: + service: + name: compat-web + port: + number: 80 + _groupPreRenderHook: | + {{- if not (hasKey $.CurrentApp "paths") }} + {{- $_ := set $.CurrentApp "paths" $.CurrentGroupVars.paths }} + {{- end }} + compat-route: + enabled: true + host: compat.example.com + service: "compat-web" diff --git a/tests/contracts/values.yaml b/tests/contracts/values.yaml new file mode 100644 index 0000000..c5b3a1e --- /dev/null +++ b/tests/contracts/values.yaml @@ -0,0 +1,239 @@ +global: + env: production + release: + enabled: true + current: "production-v1" + autoEnableApps: true + versions: + production-v1: + release-web: "3.19" + _includes: + base-a: + data: + fromBaseA: "A" + base-b: + data: + fromBaseB: "B" + profile-base: + enabled: true + _include: ["base-a"] + envVars: + A: "1" + ENV_SWITCH: + _default: "base-default" + production: "base-production" + data: + key1: "value-1" + key2: "base-value-2" + profile-override: + _include: ["base-b"] + envVars: + A: "2" + ENV_SWITCH: + _default: "override-default" + data: + key2: "override-value-2" + key4: "value-4" + +jwtSigningMethod: rsa +_include_files: + - deployments-values.yaml + +apps-configmaps: + merge-contract: + _include: ["profile-base", "profile-override"] + enabled: true + envVars: + LOCAL: "ok" + data: + key2: "local-value-2" + key3: "value-3" + compat-config: + enabled: true + extraFields: | + immutable: true + data: + key: value + +apps-stateless: + release-auto-app: + enabled: false + releaseKey: release-web + containers: + main: + image: + name: alpine + command: | + - sh + args: | + - -c + - sleep 3600 + compat-service: + enabled: true + extraSpec: | + paused: true + podSpecExtra: | + os: + name: linux + containers: + main: + image: + name: alpine + staticTag: "3" + command: | + - sh + args: | + - -c + - sleep 3600 + ports: | + - name: http + containerPort: 8080 + extraFields: | + resizePolicy: + - resourceName: cpu + restartPolicy: NotRequired + service: + enabled: true + type: LoadBalancer + allocateLoadBalancerNodePorts: true + loadBalancerClass: internal-vip + internalTrafficPolicy: Local + ipFamilyPolicy: SingleStack + ipFamilies: | + - IPv4 + ports: | + - name: http + port: 80 + targetPort: 8080 + extraSpec: | + externalTrafficPolicy: Local + +apps-jobs: + compat-job: + enabled: true + containers: + main: + image: + name: alpine + staticTag: "3" + command: | + - sh + args: | + - -c + - echo test + jobTemplateExtraSpec: | + podFailurePolicy: + rules: [] + +apps-ingresses: + compat-ingress: + enabled: true + host: app.example.com + paths: | + - path: / + pathType: Prefix + backend: + service: + name: compat-service + port: + number: 80 + extraSpec: | + defaultBackend: + service: + name: compat-service + port: + number: 80 + +apps-network-policies: + compat-netpol: + enabled: true + type: kubernetes + podSelector: | + matchLabels: + app: compat-service + policyTypes: | + - Ingress + - Egress + ingress: | + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + egress: | + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + compat-cilium-netpol: + enabled: true + type: cilium + spec: | + endpointSelector: + matchLabels: + app: compat-service + egress: + - toEndpoints: + - matchLabels: + k8s:io.kubernetes.pod.namespace: kube-system + compat-calico-netpol: + enabled: true + type: calico + selector: "app == 'compat-service'" + ingress: | + - action: Allow + source: + selector: "kubernetes.io/metadata.name == 'ingress-nginx'" + egress: | + - action: Allow + destination: + selector: "kubernetes.io/metadata.name == 'kube-system'" + +apps-pvcs: + compat-pvc: + enabled: true + accessModes: | + - ReadWriteOnce + resources: | + requests: + storage: 1Gi + extraSpec: | + volumeMode: Filesystem + +apps-secrets: + compat-secret: + enabled: true + extraFields: | + stringData: + token: value + +custom-group-contract: + __GroupVars__: + type: apps-configmaps + custom-group-cm: + enabled: true + data: + custom: "ok" + +apps-routes-contract: + __GroupVars__: + type: + _default: apps-ingresses + paths: | + - path: / + pathType: Prefix + backend: + service: + name: compat-service + port: + number: 80 + _groupPreRenderHook: | + {{- if not (hasKey $.CurrentApp "paths") }} + {{- $_ := set $.CurrentApp "paths" $.CurrentGroupVars.paths }} + {{- end }} + compat-route: + enabled: true + host: route.example.com + service: "compat-service" diff --git a/tests/crds/compat-crds.yaml b/tests/crds/compat-crds.yaml new file mode 100644 index 0000000..0c336f1 --- /dev/null +++ b/tests/crds/compat-crds.yaml @@ -0,0 +1,326 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: certificates.cert-manager.io +spec: + group: cert-manager.io + scope: Namespaced + names: + plural: certificates + singular: certificate + kind: Certificate + listKind: CertificateList + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: verticalpodautoscalers.autoscaling.k8s.io + annotations: + api-approved.kubernetes.io: "https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler" +spec: + group: autoscaling.k8s.io + scope: Namespaced + names: + plural: verticalpodautoscalers + singular: verticalpodautoscaler + kind: VerticalPodAutoscaler + listKind: VerticalPodAutoscalerList + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkas.kafka.strimzi.io +spec: + group: kafka.strimzi.io + scope: Namespaced + names: + plural: kafkas + singular: kafka + kind: Kafka + listKind: KafkaList + versions: + - name: v1beta2 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkatopics.kafka.strimzi.io +spec: + group: kafka.strimzi.io + scope: Namespaced + names: + plural: kafkatopics + singular: kafkatopic + kind: KafkaTopic + listKind: KafkaTopicList + versions: + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: ciliumnetworkpolicies.cilium.io +spec: + group: cilium.io + scope: Namespaced + names: + plural: ciliumnetworkpolicies + singular: ciliumnetworkpolicy + kind: CiliumNetworkPolicy + listKind: CiliumNetworkPolicyList + versions: + - name: v2 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkpolicies.projectcalico.org +spec: + group: projectcalico.org + scope: Namespaced + names: + plural: networkpolicies + singular: networkpolicy + kind: NetworkPolicy + listKind: NetworkPolicyList + versions: + - name: v3 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: customprometheusrules.deckhouse.io +spec: + group: deckhouse.io + scope: Namespaced + names: + plural: customprometheusrules + singular: customprometheusrule + kind: CustomPrometheusRules + listKind: CustomPrometheusRulesList + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: grafanadashboarddefinitions.deckhouse.io +spec: + group: deckhouse.io + scope: Namespaced + names: + plural: grafanadashboarddefinitions + singular: grafanadashboarddefinition + kind: GrafanaDashboardDefinition + listKind: GrafanaDashboardDefinitionList + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: dexauthenticators.deckhouse.io +spec: + group: deckhouse.io + scope: Namespaced + names: + plural: dexauthenticators + singular: dexauthenticator + kind: DexAuthenticator + listKind: DexAuthenticatorList + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: dexclients.deckhouse.io +spec: + group: deckhouse.io + scope: Namespaced + names: + plural: dexclients + singular: dexclient + kind: DexClient + listKind: DexClientList + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: nodeusers.deckhouse.io +spec: + group: deckhouse.io + scope: Cluster + names: + plural: nodeusers + singular: nodeuser + kind: NodeUser + listKind: NodeUserList + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: nodegroups.deckhouse.io +spec: + group: deckhouse.io + scope: Cluster + names: + plural: nodegroups + singular: nodegroup + kind: NodeGroup + listKind: NodeGroupList + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: podmetrics.deckhouse.io +spec: + group: deckhouse.io + scope: Namespaced + names: + plural: podmetrics + singular: podmetric + kind: PodMetric + listKind: PodMetricList + versions: + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true diff --git a/tests/test_render.yaml b/tests/test_render.yaml index fac9c71..3992f37 100644 --- a/tests/test_render.yaml +++ b/tests/test_render.yaml @@ -117,7 +117,7 @@ metadata: spec: selector: matchLabels: - app: "hpa-app" + app: my-selector maxUnavailable: 15% --- # Helm Apps Library: apps-stateless.app-1.podDisruptionBudget @@ -248,6 +248,21 @@ metadata: chart: "tests" repo: "" --- +# Helm Apps Library: test-env-yaml.env-yaml-app.containers.test.secretEnvVars +apiVersion: v1 +kind: Secret +metadata: + name: "envs-containers-env-yaml-app-test" + annotations: + helm-apps/version: "TEST" + labels: + app: "env-yaml-app" + chart: "tests" + repo: "" +type: Opaque +data: + "TEST_APP_LEVEL1_TEST_ENV3": "ZGVmYXVsdCB2YWx1ZSBmcm9tIGVudlZhcnM=" +--- # Helm Apps Library: apps-stateless.app-1.initContainers.init-container-1.secret.conf apiVersion: v1 kind: Secret @@ -542,6 +557,54 @@ data: "secret.conf": "cGxhaW5UZXh0TGluZTEKcGxhaW5UZXh0TGluZTIK" "secret2.conf": "cGxhaW5UZXh0TGluZTEK" --- +# Helm Apps Library: test-env-yaml.env-yaml-app.containers.test.configFiles.conf +apiVersion: v1 +kind: ConfigMap +metadata: + name: "config-containers-env-yaml-app-test-conf" + annotations: + helm-apps/version: "TEST" + labels: + app: "env-yaml-app" + chart: "tests" + repo: "" +data: + "conf": | + test +--- +# Helm Apps Library: test-env-yaml.env-yaml-app.containers.test.configFilesYAML.config +apiVersion: v1 +kind: ConfigMap +metadata: + name: "config-yaml-containers-env-yaml-app-test-config" + annotations: + helm-apps/version: "TEST" + labels: + app: "env-yaml-app" + chart: "tests" + repo: "" +data: + "config": | + level1: + testMap: + testProd2: 2v + testMap2: + test: 2v + test2: 3v + test3: 3 + testNestedMap: + nestedMap: + prod: 1 + test: 1 + testSlice: + - testProd + - testProd2 + overwrited: default value from values.yaml + testIncludeVar: prod value from file + testIncludeVarMustOverwrited: prod value from values.yaml + testValue: value from file + testValueWithDefault: default value from file +--- # Helm Apps Library: apps-stateless.app-1.initContainers.init-container-1.configFiles.nginx.conf apiVersion: v1 kind: ConfigMap @@ -721,6 +784,23 @@ data: configline2 something.conf: | configline1 +binaryData: + test.gz: jdbkjbkjsdbkjdbjdsbljl +--- +# Source: tests/templates/init-helm-apps-library.yaml +# Helm Apps Library: apps-configmaps.configmap-2 +apiVersion: v1 +kind: ConfigMap +metadata: + name: "configmap-2" + annotations: + helm-apps/version: "TEST" + labels: + app: "configmap-2" + chart: "tests" + repo: "" +data: + "nginx.conf": "configline1\nconfigline2 \n" --- # Source: tests/templates/init-helm-apps-library.yaml # Helm Apps Library: apps-pvcs.test-pvc @@ -816,6 +896,24 @@ subjects: name: test-app-2 namespace: test-prod --- +# Helm Apps Library: test-hpa.hpa-app.service +apiVersion: v1 +kind: Service +metadata: + name: "hpa-app" + annotations: + helm-apps/version: "TEST" + labels: + app: "hpa-app" + chart: "tests" + repo: "" +spec: + ports: + - name: http + port: 80 + selector: + app: my-selector +--- # Helm Apps Library: apps-stateless.app-1.service apiVersion: v1 kind: Service @@ -1088,7 +1186,7 @@ kind: Deployment metadata: name: "env-yaml-app" annotations: - checksum/config: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + checksum/config: "f0b89172b9c460ce9fe6c12abe9d5a02d3210d97827b134b7d04abf3c499c822" helm-apps/version: "TEST" labels: app: "env-yaml-app" @@ -1104,7 +1202,7 @@ spec: metadata: name: "env-yaml-app" annotations: - checksum/config: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + checksum/config: "f0b89172b9c460ce9fe6c12abe9d5a02d3210d97827b134b7d04abf3c499c822" labels: app: "env-yaml-app" chart: "tests" @@ -1122,8 +1220,25 @@ spec: value: "from envYAML" - name: "TESTAPP_LEVEL1_TESTENV4" value: "prod value from envVars" + envFrom: + - secretRef: + name: "envs-containers-env-yaml-app-test" + volumeMounts: + - name: "config-containers-env-yaml-app-test-conf" + subPath: "conf" + mountPath: "/config" + - name: "config-yaml-containers-env-yaml-app-test-config" + subPath: "config" + mountPath: "/config" imagePullSecrets: - name: registrysecret + volumes: + - name: "config-containers-env-yaml-app-test-conf" + configMap: + name: "config-containers-env-yaml-app-test-conf" + - name: "config-yaml-containers-env-yaml-app-test-config" + configMap: + name: "config-yaml-containers-env-yaml-app-test-config" priorityClassName: "production-medium" selector: matchLabels: @@ -1212,7 +1327,7 @@ spec: priorityClassName: "production-medium" selector: matchLabels: - app: "hpa-app" + app: my-selector revisionHistoryLimit: 3 --- # Source: tests/templates/init-helm-apps-library.yaml