name: render-manifests-dispatch on: schedule: - cron: '0 15 * * *' workflow_dispatch: env: CLUSTER: cl01tl BASE_BRANCH: manifests BRANCH_NAME: auto/update-manifests ASSIGNEE: alexlebens MAIN_DIR: /workspace/alexlebens/infrastructure/infrastructure MANIFEST_DIR: /workspace/alexlebens/infrastructure/infrastructure-manifests jobs: render-manifests-dispatch: runs-on: ubuntu-js steps: - name: Checkout Main uses: actions/checkout@v6 with: path: infrastructure fetch-depth: 0 - name: Checkout Manifests uses: actions/checkout@v6 with: ref: manifests path: infrastructure-manifests - name: Set up Helm uses: azure/setup-helm@v4 with: token: ${{ secrets.GITEA_TOKEN }} version: v3.17.2 # Pending https://github.com/helm/helm/pull/30743 cache: true - name: Configure Kubeconfig uses: azure/k8s-set-context@v4 with: method: kubeconfig kubeconfig: ${{ secrets.KUBECONFIG }} - name: Cache Helm Dependencies uses: actions/cache@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: Prepare Manifest Branch run: | cd "${MANIFEST_DIR}" echo "" echo ">> Configure git to use gitea-bot as user ..." git config user.name "gitea-bot" git config user.email "gitea-bot@alexlebens.net" echo "" echo ">> Checking if PR branch exists ..." if git ls-remote --exit-code --heads origin "${BRANCH_NAME}" > /dev/null 2>&1; then echo "" echo ">> Branch '${BRANCH_NAME}' exists, pulling changes ..." git fetch origin "${BRANCH_NAME}" git checkout "${BRANCH_NAME}" git pull --rebase else echo "" echo ">> Branch '${BRANCH_NAME}' does not exist, creating ..." git checkout -b "${BRANCH_NAME}" fi echo "----" - name: Check which Directories have Changes id: check-dir-changes run: | cd "${MAIN_DIR}" echo "" echo ">> Triggered on dispatch, will check all paths ..." # Extract names of charts RENDER_DIR=$(find "clusters/${CLUSTER}/helm" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort -u) if [ -n "${RENDER_DIR}" ]; then echo "" echo ">> Directories to Render:" echo "${RENDER_DIR}" echo "----" echo "changes-detected=true" >> "$GITEA_OUTPUT" echo "render-dir<> "$GITEA_OUTPUT" echo "${RENDER_DIR}" >> "$GITEA_OUTPUT" echo "EOF" >> "$GITEA_OUTPUT" else echo ">> No directories found" echo "changes-detected=false" >> "$GITEA_OUTPUT" fi - name: Add Repositories if: steps.check-dir-changes.outputs.changes-detected == 'true' env: RENDER_DIR: ${{ steps.check-dir-changes.outputs.render-dir }} run: | cd "${MAIN_DIR}" echo "" echo ">> Adding repositories for chart dependencies ..." for DIR in ${RENDER_DIR}; do helm dependency list --max-col-width 120 "${MAIN_DIR}/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 "" 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 "----" - name: Remove Changed Manifest Files if: steps.check-dir-changes.outputs.changes-detected == 'true' env: RENDER_DIR: ${{ steps.check-dir-changes.outputs.render-dir }} run: | cd "${MANIFEST_DIR}" echo "" echo ">> Remove manfiest files and rebuild from source ..." for DIR in ${RENDER_DIR}; do CHART_PATH=${MANIFEST_DIR}/clusters/${CLUSTER}/manifests/${DIR} echo "${CHART_PATH}" rm -rf ${CHART_PATH}/* done echo "----" - name: Render Helm Manifests id: render-manifests if: steps.check-dir-changes.outputs.changes-detected == 'true' env: RENDER_DIR: ${{ steps.check-dir-changes.outputs.render-dir }} run: | cd ${MAIN_DIR} echo "" echo ">> Rendering Manifests ..." render_chart() { local DIR="$1" local CHART_PATH="${MAIN_DIR}/clusters/${CLUSTER}/helm/${DIR}" local CHART_NAME=$(basename "${CHART_PATH}") echo "" echo ">> Rendering ..." echo ">> Chart: ${CHART_NAME}" echo ">> Path: ${CHART_PATH}" if [ -f "${CHART_PATH}/Chart.yaml" ]; then local OUTPUT_FOLDER="${MANIFEST_DIR}/clusters/${CLUSTER}/manifests/${CHART_NAME}/" mkdir -p "${OUTPUT_FOLDER}" cd "${CHART_PATH}" echo "" echo ">> Updating helm dependencies ..." helm dependency update --skip-refresh > /dev/null echo "" echo ">> Linting helm chart ..." helm lint --namespace "${CHART_NAME}" --quiet local NAMESPACE="${CHART_NAME}" case "${CHART_NAME}" in "stack") NAMESPACE="argocd" echo "" echo ">> Special Rendering into 'argocd' namespace ..." ;; "cilium" | "coredns" | "metrics-server" | "prometheus-operator-crds") NAMESPACE="kube-system" echo "" echo ">> Special Rendering for ${CHART_NAME} into 'kube-system' namespace ..." ;; *) echo "" echo ">> Standard Rendering for ${CHART_NAME} ..." esac echo "" echo ">> Formating rendered template ..." local TEMPLATE TEMPLATE=$(helm template "${CHART_NAME}" ./ --namespace "${NAMESPACE}" --include-crds --dry-run=server --api-versions "gateway.networking.k8s.io/v1/HTTPRoute") # 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 ">> Manifests for ${CHART_NAME} rendered to ${OUTPUT_FOLDER}:" ls $OUTPUT_FOLDER echo "" else echo "" echo ">> Directory ${CHART_PATH} does not contain a Chart.yaml. Skipping ..." echo "" fi } export -f render_chart export MAIN_DIR CLUSTER MANIFEST_DIR # Run rendering in parallel for DIR in ${RENDER_DIR}; do echo "${DIR}" done | xargs -n 1 -P 4 -I {} bash -c 'render_chart "$@"' _ {} echo "----" - name: Check for Changes id: check-changes if: steps.check-dir-changes.outputs.changes-detected == 'true' run: | cd "${MANIFEST_DIR}" GIT_CHANGES=$(git status --porcelain) if [ -n "${GIT_CHANGES}" ]; then echo "" echo ">> Changes detected" git status --porcelain echo "changes-detected=true" >> $GITEA_OUTPUT else echo "" echo ">> No changes detected, skipping PR creation" fi echo "----" - name: Commit and Push Changes id: commit-push if: steps.check-changes.outputs.changes-detected == 'true' run: | cd "${MANIFEST_DIR}" echo "" echo ">> Commiting changes to ${BRANCH_NAME} ..." git add . git commit -m "chore: Update manifests after change" REPO_URL="${{ secrets.REPO_URL }}/${{ gitea.repository }}" echo "" echo ">> Pushing changes to ${REPO_URL} ..." git push -u "https://oauth2:${{ secrets.BOT_TOKEN }}@${REPO_URL#*://}" "${BRANCH_NAME}" echo "----" echo "HEAD_BRANCH=${BRANCH_NAME}" >> "$GITEA_OUTPUT" echo "push=true" >> "$GITEA_OUTPUT" - name: Check for Pull Request id: check-for-pull-requst if: steps.commit-push.outputs.push == 'true' env: GITEA_TOKEN: ${{ secrets.BOT_TOKEN }} GITEA_URL: ${{ secrets.REPO_URL }} HEAD_BRANCH: ${{ steps.commit-push.outputs.HEAD_BRANCH }} run: | cd ${MANIFEST_DIR} API_ENDPOINT="${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/pulls?base_branch=${BASE_BRANCH}&state=open&page=1" echo ">> Checking if PR from branch ${HEAD_BRANCH} into ${BASE_BRANCH}" echo ">> With Endpoint of:" echo "$API_ENDPOINT" HTTP_STATUS=$( curl -X GET \ --silent \ --write-out '%{http_code}' \ --output response_body.json \ --dump-header response_headers.txt \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ "$API_ENDPOINT" 2> response_errors.txt ) echo ">> HTTP Status Code: $HTTP_STATUS" echo ">> Response Output ..." echo "----" cat response_body.json echo "----" cat response_headers.txt echo "----" cat response_errors.txt echo "----" if [ "$HTTP_STATUS" == "200" ] && [ "$(cat response_body.json | jq -r .[0].state)" == "open" ]; then echo ">> Pull Request has been found open, will update" PR_INDEX=$(cat response_body.json | jq -r .[0].number) echo "pull-request-exists=${PR_INDEX}" >> $GITEA_OUTPUT echo "pull-request-index=true" >> $GITEA_OUTPUT elif [ "$HTTP_STATUS" == "200" ] && [ "$(cat response_body.json | jq -r .[0].state)" == "closed" ]; then echo ">> Pull Request found, but was closed" echo "pull-request-exists=false" >> $GITEA_OUTPUT else echo ">> Pull Request not found" echo "pull-request-exists=false" >> $GITEA_OUTPUT fi echo "----" - name: Create Pull Request id: create-pull-request if: steps.commit-push.outputs.push == 'true' && steps.check-for-pull-requst.outputs.pull-request-exists == 'false' env: GITEA_TOKEN: ${{ secrets.BOT_TOKEN }} GITEA_URL: ${{ secrets.REPO_URL }} HEAD_BRANCH: ${{ steps.commit-push.outputs.HEAD_BRANCH }} run: | cd ${MANIFEST_DIR} API_ENDPOINT="${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/pulls" PAYLOAD=$( jq -n \ --arg head "${HEAD_BRANCH}" \ --arg base "${BASE_BRANCH}" \ --arg assignee "${ASSIGNEE}" \ --arg title "Automated Manifest Update" \ --arg body "This PR contains newly rendered Kubernetes manifests automatically generated by the CI workflow." \ '{head: $head, base: $base, assignee: $assignee, title: $title, body: $body}' ) echo ">> Creating PR from branch ${HEAD_BRANCH} into ${BASE_BRANCH}" echo ">> With Endpoint of:" echo "$API_ENDPOINT" echo ">> With Payload of:" echo "$PAYLOAD" HTTP_STATUS=$( curl -X POST \ --silent \ --write-out '%{http_code}' \ --output response_body.json \ --dump-header response_headers.txt \ --data "$PAYLOAD" \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ "$API_ENDPOINT" 2> response_errors.txt ) echo ">> HTTP Status Code: $HTTP_STATUS" echo ">> Response Output ..." echo "----" cat response_body.json echo "----" cat response_headers.txt echo "----" cat response_errors.txt echo "----" if [ "$HTTP_STATUS" == "201" ]; then echo ">> Pull Request created successfully!" PR_URL=$(cat response_body.json | jq -r .html_url) echo "pull-request-url=${PR_URL}" >> $GITEA_OUTPUT PR_ID=$(cat response_body.json | jq -r .id) echo "pull-request-id=${PR_ID}" >> $GITEA_OUTPUT echo "pull-request-operation=created" >> $GITEA_OUTPUT elif [ "$HTTP_STATUS" == "422" ]; then echo ">> Failed to create PR (HTTP 422: Unprocessable Entity), PR may already exist" elif [ "$HTTP_STATUS" == "409" ]; then echo ">> Failed to create PR (HTTP 409: Conflict), PR already exists" else echo ">> Failed to create PR, HTTP status code: $HTTP_STATUS" exit 1 fi echo "----" - name: ntfy Created uses: niniyas/ntfy-action@master if: steps.create-pull-request.outputs.pull-request-operation == 'created' with: url: "${{ secrets.NTFY_URL }}" topic: "${{ secrets.NTFY_TOPIC }}" title: "Manifest Render PR Created - Infrastructure" priority: 3 headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}' tags: action,successfully,completed details: "Manifest rendering for Infrastructure has created a new Pull Request with ID: ${{ steps.create-pull-request.outputs.pull-request-id }}!" icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png" actions: '[{"action": "view", "label": "Open Gitea", "url": "${{ steps.create-pull-request.outputs.pull-request-url }}", "clear": true}]' - name: ntfy Failed uses: niniyas/ntfy-action@master if: failure() with: url: "${{ secrets.NTFY_URL }}" topic: "${{ secrets.NTFY_TOPIC }}" title: "Manifest Render Failure - Infrastructure" priority: 4 headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}' tags: action,failed details: "Manifest rendering for Infrastructure has failed!" icon: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png" actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/infrastructure/actions?workflow=render-manifests.yaml", "clear": true}]' image: true