name: lint-test-helm on: pull_request: branches: - main paths: - 'clusters/cl01tl/helm/**' push: branches: - main paths: - 'clusters/cl01tl/helm/**' env: CLUSTER: cl01tl BASE_BRANCH: "origin/${{ github.base_ref }}" KUBECONFORM_VERSION: "v0.6.7" ARGOCD_VERSION: "v3.3.6" jobs: lint-helm: runs-on: ubuntu-js outputs: chart-dir: ${{ steps.check-dir-changes.outputs.chart-dir }} chart-dir-csv: ${{ steps.check-dir-changes.outputs.chart-dir-csv }} changes-detected: ${{ steps.check-dir-changes.outputs.changes-detected }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Check Branch Exists id: check-branch-exists if: github.event_name == 'pull_request' uses: GuillaumeFalourd/branch-exists@650358876c774d6ccbd581b5553eb636dab79a97 # v1.2 with: branch: ${{ github.base_ref }} - name: Report Branch Exists id: branch-exists if: github.event_name == 'push' || steps.check-branch-exists.outputs.exists == 'true' && github.event_name == 'pull_request' run: | if [ "${{ github.event_name }}" == "push" ]; then echo ">> Action is from a push event, will continue with linting" else echo ">> Branch ${{ github.base_ref }} exists, will continue with linting" fi echo "" echo "----" echo "exists=true" >> $GITHUB_OUTPUT - name: Set Up Helm if: steps.branch-exists.outputs.exists == 'true' uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 with: token: ${{ secrets.GITEA_TOKEN }} # renovate: datasource=github-releases depName=helm/helm version: v4.1.3 cache: true - name: Cache Helm Dependencies if: steps.branch-exists.outputs.exists == 'true' uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: | ~/.cache/helm ~/.config/helm key: helm-cache-${{ runner.os }}-${{ hashFiles('infrastructure/clusters/cl01tl/helm/**/Chart.yaml', 'infrastructure/clusters/cl01tl/helm/**/Chart.lock') }} restore-keys: | helm-cache-${{ runner.os }}- - name: Check Directories for Changes id: check-dir-changes if: steps.branch-exists.outputs.exists == 'true' run: | echo ">> Target branch for diff is: ${BASE_BRANCH}" if [ "${{ github.event_name }}" == "pull_request" ]; then DIFF_TARGET="${BASE_BRANCH}" echo "" echo ">> Checking for changes in a pull request ..." else DIFF_TARGET="${{ github.event.before }}..HEAD" echo "" echo ">> Checking for changes from a push ..." fi CHANGED_CHARTS=$(git diff --name-only "${DIFF_TARGET}" | grep -E "^clusters/${CLUSTER}/helm/" | awk -F '/' '{print $4}' | sort -u || true) if [ -n "${CHANGED_CHARTS}" ]; then echo "" echo ">> Chart to Lint:" echo "" echo "${CHANGED_CHARTS}" CHANGED_CHARTS_CSV=$(echo "${CHANGED_CHARTS}" | paste -sd ',' -) echo "" echo "----" echo "changes-detected=true" >> $GITHUB_OUTPUT echo "chart-dir-csv=${CHANGED_CHARTS_CSV}" >> $GITHUB_OUTPUT echo "chart-dir<> $GITHUB_OUTPUT echo "${CHANGED_CHARTS}" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT else echo "" echo ">> Did not find any helm charts files to lint" echo "" echo "----" echo "changes-detected=false" >> $GITHUB_OUTPUT fi - name: Add Repositories if: steps.check-dir-changes.outputs.changes-detected == 'true' env: CHANGED_CHARTS: ${{ steps.check-dir-changes.outputs.chart-dir }} run: | echo ">> Adding repositories for chart dependencies ..." echo "" for DIR in ${CHANGED_CHARTS}; do helm dependency list --max-col-width 120 clusters/${CLUSTER}/helm/${DIR} 2> /dev/null \ | tail -n +2 \ | awk 'NF > 0 { print $1, $3 }' \ | while read -r REPO_NAME REPO_URL; do if [[ "${REPO_URL}" == oci://* ]]; then echo ">> Ignoring OCI repo: ${REPO_URL}" elif [[ -n "${REPO_NAME}" && -n "${REPO_URL}" ]]; then helm repo add "${REPO_NAME}" "${REPO_URL}" fi done || true done if helm repo list > /dev/null 2>&1; then echo "" echo ">> Update repository cache ..." helm repo update fi echo "" echo "----" - name: Lint Helm Chart id: lint if: steps.check-dir-changes.outputs.changes-detected == 'true' env: CHANGED_CHARTS: ${{ steps.check-dir-changes.outputs.chart-dir }} run: | EXIT_CODE=0 FAILED_CHARTS="" echo ">> Running linting on changed charts ..." for DIR in ${CHANGED_CHARTS}; do CHART_PATH="clusters/${CLUSTER}/helm/${DIR}" CHART_NAME=$(basename "${CHART_PATH}") if [ -f "${CHART_PATH}/Chart.yaml" ]; then echo "" echo ">> Building helm dependency for ${CHART_NAME} ..." helm dependency build "${CHART_PATH}" --skip-refresh echo "" echo ">> Linting helm chart ${CHART_NAME} ..." if ! helm lint "${CHART_PATH}" --namespace "default"; then EXIT_CODE=1 if [ -z "${FAILED_CHARTS}" ]; then FAILED_CHARTS="${DIR}" else FAILED_CHARTS="${FAILED_CHARTS}, ${DIR}" fi fi else echo "" echo ">> Directory ${CHART_PATH} does not contain a Chart.yaml. Skipping ..." fi done echo "" echo "----" echo "failed-charts=${FAILED_CHARTS}" >> "$GITHUB_OUTPUT" exit $EXIT_CODE - name: ntfy Failed uses: niniyas/ntfy-action@96acac57fdc91d4c4f50b78486c1ed6f03f9f61c # master if: failure() with: url: '${{ secrets.NTFY_URL }}' topic: '${{ secrets.NTFY_TOPIC }}' title: 'Helm Test Failure' priority: 3 headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}' tags: action,failed details: "Helm linting for cluster '${{ env.CLUSTER }}' failed on charts: ${{ steps.lint.outputs.failed-charts }}" icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png' actions: '[{"action": "view", "label": "View Run", "url": "${{ vars.USER_URL }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", "clear": true}]' image: true validate-kubeconform: needs: lint-helm runs-on: ubuntu-js if: | needs.lint-helm.result == 'success' && needs.lint-helm.outputs.changes-detected == 'true' && github.event_name == 'pull_request' steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Cache Kubeconform id: cache-kubeconform uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: /usr/local/bin/kubeconform key: ${{ runner.os }}-kubeconform-${{ env.KUBECONFORM_VERSION }} restore-keys: | ${{ runner.os }}-kubeconform- - name: Install Kubeconform if: steps.cache-kubeconform.outputs.cache-hit != 'true' run: | echo ">> Downloading Kubeconform ${{ env.KUBECONFORM_VERSION }} ..." wget -q https://github.com/yannh/kubeconform/releases/download/${{ env.KUBECONFORM_VERSION }}/kubeconform-linux-amd64.tar.gz echo "" echo ">> Extracting Kubeconform ..." tar xf kubeconform-linux-amd64.tar.gz echo "" echo ">> Installing Kubeconform ..." sudo mv kubeconform /usr/local/bin/ - name: Verify installation run: | echo "" echo ">> Verifying installation ..." kubeconform -v echo "" echo "----" - name: Set Up Helm uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 with: token: ${{ secrets.GITEA_TOKEN }} # renovate: datasource=github-releases depName=helm/helm version: v4.1.3 cache: true - name: Cache Helm Dependencies uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: | ~/.cache/helm ~/.config/helm key: helm-cache-${{ runner.os }}-${{ hashFiles('infrastructure/clusters/cl01tl/helm/**/Chart.yaml', 'infrastructure/clusters/cl01tl/helm/**/Chart.lock') }} restore-keys: | helm-cache-${{ runner.os }}- - name: Add Repositories env: CHANGED_CHARTS: ${{ needs.lint-helm.outputs.chart-dir }} run: | echo ">> Adding repositories for chart dependencies ..." echo "" for DIR in ${CHANGED_CHARTS}; do helm dependency list --max-col-width 120 clusters/${CLUSTER}/helm/${DIR} 2> /dev/null \ | tail -n +2 \ | awk 'NF > 0 { print $1, $3 }' \ | while read -r REPO_NAME REPO_URL; do if [[ "${REPO_URL}" == oci://* ]]; then echo ">> Ignoring OCI repo: ${REPO_URL}" elif [[ -n "${REPO_NAME}" && -n "${REPO_URL}" ]]; then helm repo add "${REPO_NAME}" "${REPO_URL}" fi done || true done if helm repo list > /dev/null 2>&1; then echo "" echo ">> Update repository cache ..." helm repo update fi echo "" echo "----" - name: Validate Rendered Templates id: validate env: CHANGED_CHARTS: ${{ needs.lint-helm.outputs.chart-dir }} run: | SCHEMA_LOCATIONS="-schema-location default -schema-location https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json" EXIT_CODE=0 FAILED_CHARTS="" for DIR in ${CHANGED_CHARTS}; do CHART_PATH="clusters/${CLUSTER}/helm/${DIR}" echo "" echo ">> Validating: ${DIR}" helm dependency build "${CHART_PATH}" --skip-refresh if ! helm template "${DIR}" "${CHART_PATH}" --include-crds --namespace default --api-versions "gateway.networking.k8s.io/v1/HTTPRoute,monitoring.coreos.com/v1,monitoring.coreos.com/v1/ServiceMonitor" | \ kubeconform \ ${SCHEMA_LOCATIONS} \ -ignore-missing-schemas \ -strict \ -summary; then EXIT_CODE=1 if [ -z "${FAILED_CHARTS}" ]; then FAILED_CHARTS="${DIR}" else FAILED_CHARTS="${FAILED_CHARTS}, ${DIR}" fi fi done echo "" echo "----" echo "failed-charts=${FAILED_CHARTS}" >> "$GITHUB_OUTPUT" exit $EXIT_CODE - name: ntfy Failed uses: niniyas/ntfy-action@96acac57fdc91d4c4f50b78486c1ed6f03f9f61c # master if: failure() with: url: '${{ secrets.NTFY_URL }}' topic: '${{ secrets.NTFY_TOPIC }}' title: 'Kubeconform Test Failure' priority: 3 headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}' tags: action,failed details: "Kubeconform for cluster '${{ env.CLUSTER }}' failed on charts: ${{ steps.validate.outputs.failed-charts }}" icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png' actions: '[{"action": "view", "label": "View Run", "url": "${{ vars.USER_URL }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", "clear": true}]' image: true # argo-diff: # needs: lint-helm # runs-on: ubuntu-js # if: | # needs.lint-helm.result == 'success' && # needs.lint-helm.outputs.changes-detected == 'true' && # github.event_name == 'pull_request' # steps: # - name: Checkout # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # with: # fetch-depth: 0 # - name: Cache ArgoCD CLI # id: cache-argocd # uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 # with: # path: /usr/local/bin/argocd # key: ${{ runner.os }}-argocd-${{ env.ARGOCD_VERSION }} # restore-keys: | # ${{ runner.os }}-argocd- # - name: Install ArgoCD CLI # if: steps.cache-argocd.outputs.cache-hit != 'true' # run: | # echo ">> Downloading ArgoCD CLI, version: ${{ env.ARGOCD_VERSION }} ..." # curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/download/${{ env.ARGOCD_VERSION }}/argocd-linux-amd64 # echo "" # echo ">> Installing ArgoCD CLI ..." # sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd # echo "" # echo "----" # - name: Verify installation # run: | # echo "" # echo ">> Verifying installation ..." # argocd version --client # echo "" # echo "----" # - name: Set Up Helm # uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 # with: # token: ${{ secrets.GITEA_TOKEN }} # # renovate: datasource=github-releases depName=helm/helm # version: v4.1.3 # cache: true # - name: Cache Helm Dependencies # uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 # with: # path: | # ~/.cache/helm # ~/.config/helm # key: helm-cache-${{ runner.os }}-${{ hashFiles('infrastructure/clusters/cl01tl/helm/**/Chart.yaml', 'infrastructure/clusters/cl01tl/helm/**/Chart.lock') }} # restore-keys: | # helm-cache-${{ runner.os }}- # - name: Add Repositories # env: # CHANGED_CHARTS: ${{ needs.lint-helm.outputs.chart-dir }} # run: | # echo ">> Adding repositories for chart dependencies ..." # echo "" # for DIR in ${CHANGED_CHARTS}; do # helm dependency list --max-col-width 120 clusters/${CLUSTER}/helm/${DIR} 2> /dev/null \ # | tail -n +2 \ # | awk 'NF > 0 { print $1, $3 }' \ # | while read -r REPO_NAME REPO_URL; do # if [[ "${REPO_URL}" == oci://* ]]; then # echo ">> Ignoring OCI repo: ${REPO_URL}" # elif [[ -n "${REPO_NAME}" && -n "${REPO_URL}" ]]; then # helm repo add "${REPO_NAME}" "${REPO_URL}" # fi # done || true # done # if helm repo list > /dev/null 2>&1; then # echo "" # echo ">> Update repository cache ..." # helm repo update # fi # echo "" # echo "----" # - name: Render Templates # id: render # env: # CHANGED_CHARTS: ${{ needs.lint-helm.outputs.chart-dir }} # run: | # for APP_NAME in ${CHANGED_CHARTS}; do # echo ">> Render templates for ${APP_NAME} ..." # CHART_PATH="clusters/${CLUSTER}/helm/${APP_NAME}" # OUTPUT_FOLDER="clusters/${CLUSTER}/manifests/${APP_NAME}/" # helm dependency build "${CHART_PATH}" --skip-refresh # NAMESPACE="${APP_NAME}" # case "${APP_NAME}" in # "stack") # NAMESPACE="argocd" # echo ">> Special Rendering into 'argocd' namespace ..." # ;; # "cilium" | "coredns" | "metrics-server") # NAMESPACE="kube-system" # echo ">> Special Rendering for ${APP_NAME} into 'kube-system' namespace ..." # ;; # *) # echo ">> Standard Rendering ..." # esac # TEMPLATE=$(helm template "${APP_NAME}" "${CHART_PATH}" --include-crds --namespace "${NAMESPACE}" --include-crds --api-versions "gateway.networking.k8s.io/v1/HTTPRoute,monitoring.coreos.com/v1,monitoring.coreos.com/v1/ServiceMonitor") # # Format and split rendered template # echo "${TEMPLATE}" | yq '... comments=""' | yq 'select(. != null)' | yq -s '"'"${OUTPUT_FOLDER}"'" + .kind + "-" + .metadata.name + ".yaml"' # # Strip comments again to ensure formatting correctness # for file in "$OUTPUT_FOLDER"/*; do # yq -i '... comments=""' $file # done # echo "" # echo ">> Templates in output folder: ${OUTPUT_FOLDER}" # ls ${OUTPUT_FOLDER} # done # echo "----" # - name: Run App Diff # id: diff # env: # ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }} # ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }} # CHANGED_CHARTS: ${{ needs.lint-helm.outputs.chart-dir }} # run: | # FAILED_CHARTS="" # DIFF_FOUND="false" # for APP_NAME in ${CHANGED_CHARTS}; do # echo ">> Running argocd app diff for ${APP_NAME} ..." # argocd app diff "${APP_NAME}" \ # --server "${ARGOCD_SERVER}" \ # --revision ${{ gitea.sha }} \ # --diff-exit-code 0 \ # --local "clusters/${CLUSTER}/manifests/${APP_NAME}" \ # --local-repo-root "." \ # --grpc-web > "diff_output_${APP_NAME}.txt" # if [ -s "diff_output_${APP_NAME}.txt" ]; then # echo ">> Argo diff:" # echo "" # cat diff_output_${APP_NAME}.txt # echo "" # DIFF_FOUND="true" # else # echo ">> No Argo diff found for ${APP_NAME}" # rm "diff_output_${APP_NAME}.txt" # fi # done # echo "----" # echo "diff-detected=${DIFF_FOUND}" >> "$GITHUB_OUTPUT" # echo "failed-charts=${FAILED_CHARTS}" >> "$GITHUB_OUTPUT" # exit $OVERALL_EXIT_CODE # - name: Post Diff # if: | # always() && # steps.diff.outputs.diff-detected == 'true' && # gitea.event.pull_request.number != null # env: # GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} # run: | # COMMENT_BODY="### ArgoCD Diff Results # " # for f in diff_output_*.txt; do # APP_NAME=$(echo $f | sed 's/diff_output_//;s/.txt//') # DIFF_CONTENT=$(cat "$f") # COMMENT_BODY="${COMMENT_BODY} # #### App: ${APP_NAME} # " # if [ -z "$DIFF_CONTENT" ]; then # COMMENT_BODY="${COMMENT_BODY} No changes detected." # else # COMMENT_BODY="${COMMENT_BODY} # \`\`\`diff # ${DIFF_CONTENT} # \`\`\`" # fi # done # curl -X 'POST' \ # "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/issues/${{ gitea.event.pull_request.number }}/comments" \ # -H "Authorization: token ${GITEA_TOKEN}" \ # -H "Content-Type: application/json" \ # -d "$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')" # - name: ntfy Failed # uses: niniyas/ntfy-action@96acac57fdc91d4c4f50b78486c1ed6f03f9f61c # master # if: failure() # with: # url: '${{ secrets.NTFY_URL }}' # topic: '${{ secrets.NTFY_TOPIC }}' # title: 'ArgoCD Diff Failure' # priority: 3 # headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}' # tags: action,failed # details: "ArgoCD diff for cluster '${{ env.CLUSTER }}' failed on charts: ${{ steps.diff.outputs.failed-charts }}" # icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png' # actions: '[{"action": "view", "label": "View Run", "url": "${{ vars.USER_URL }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", "clear": true}]' # image: true