diff --git a/resources/org/homelab/build-tools.Dockerfile b/resources/org/homelab/build-tools.Dockerfile new file mode 100644 index 0000000..bbcb5bb --- /dev/null +++ b/resources/org/homelab/build-tools.Dockerfile @@ -0,0 +1,13 @@ +# Custom docker-cli image for dind-pod.yaml's docker-cli container — +# bakes in everything the pipeline stages need at runtime (git, yq, +# bash, python3 + pip/venv for runHooks' python hooks, curl) so nothing +# gets apk-installed or curl-downloaded on every single build. Rebuild +# and push this (see the one-off build commands in the commit that +# added this file) whenever this list changes; dind-pod.yaml pins the +# resulting image tag explicitly, so a rebuild doesn't silently roll +# out until that pin is also bumped. +FROM docker:27-cli + +RUN apk add --no-cache git bash python3 py3-pip py3-virtualenv curl \ + && curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \ + && chmod +x /usr/local/bin/yq diff --git a/resources/org/homelab/dind-pod.yaml b/resources/org/homelab/dind-pod.yaml index 726e68d..fab5a60 100644 --- a/resources/org/homelab/dind-pod.yaml +++ b/resources/org/homelab/dind-pod.yaml @@ -33,7 +33,14 @@ spec: - name: docker-graph-storage mountPath: /var/lib/docker - name: docker-cli - image: docker:27-cli + # Custom image (see build-tools.Dockerfile in this same directory) + # — bakes in git/yq/bash/python3+pip/venv/curl so nothing needs + # apk-installing or curl-downloading on every single build (was + # slow and, per its own point, defeats the purpose of a + # reproducible pipeline to be quietly downloading a tool binary + # fresh on every run). Versioned tag, not :latest — a rebuild of + # the tools image doesn't roll out until this pin is bumped too. + image: harbor.192.168.1.7.nip.io/homelab/build-tools:1 command: ["cat"] tty: true env: diff --git a/src/com/homelab/stages/runHooks.groovy b/src/com/homelab/stages/runHooks.groovy index 6c4660d..b7c5f98 100644 --- a/src/com/homelab/stages/runHooks.groovy +++ b/src/com/homelab/stages/runHooks.groovy @@ -93,21 +93,15 @@ def buildRunCommand(String script, String interpreter, String requirements) { } boolean isPython = (interp == 'python3' || interp == 'python' || script.endsWith('.py')) - // docker:27-cli is minimal Alpine — only `sh` is guaranteed present. - // bash/python3 need installing on demand, unlike the real system's - // pod images which already bundle a full toolchain. - String installLine = '' - if (isPython) { - installLine = 'apk add --no-cache python3 py3-pip py3-virtualenv >/dev/null' - } else if (interp == 'bash') { - installLine = 'apk add --no-cache bash >/dev/null' - } + // bash/python3+pip/venv are baked into the docker-cli image (see + // build-tools.Dockerfile) — used to apk-install these on demand on + // every single hook invocation, unlike the real system's pod images + // which already bundle a full toolchain. if (requirements && isPython) { String py = interp ?: 'python3' return """ set -e - ${installLine} ${py} -m venv .hook_venv . .hook_venv/bin/activate pip install --quiet --disable-pip-version-check -r ${requirements} @@ -116,7 +110,7 @@ def buildRunCommand(String script, String interpreter, String requirements) { } if (interp) { - return "set -e\n${installLine}\n${interp} ${script}" + return "set -e\n${interp} ${script}" } // No interpreter resolved — rely on the script's shebang. return "set -e\nchmod +x ${script}\n./${script}" diff --git a/src/com/homelab/stages/syncArgoApp.groovy b/src/com/homelab/stages/syncArgoApp.groovy index 7a09e39..0860e09 100644 --- a/src/com/homelab/stages/syncArgoApp.groovy +++ b/src/com/homelab/stages/syncArgoApp.groovy @@ -39,7 +39,6 @@ def run(Map config) { echo "ARGOCD_TOKEN is empty — the jenkins-ci account token has not been generated yet. See secretstores/argocd-jenkins-ci-token.yaml for the one-time bootstrap steps." >&2 exit 1 fi - apk add --no-cache curl >/dev/null curl -sf -X POST \\ -H "Authorization: Bearer \$ARGOCD_TOKEN" \\ -H "Content-Type: application/json" \\ diff --git a/src/com/homelab/stages/updateHelmTag.groovy b/src/com/homelab/stages/updateHelmTag.groovy index ae33e13..43a5d71 100644 --- a/src/com/homelab/stages/updateHelmTag.groovy +++ b/src/com/homelab/stages/updateHelmTag.groovy @@ -25,46 +25,39 @@ package com.homelab.stages def run(Map config) { def valuesFile = "values/${config.repo_name}/${config.service_name}/values.yaml" stage(stageName('Update Helm chart image tag')) { - withCredentials([usernamePassword(credentialsId: config.gitea_cred ?: 'gitea-ci-credentials', usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) { - dir('helm-chart-repo') { - deleteDir() - // Plain split, not replaceFirst — replaceFirst's *replacement* - // argument is also parsed as regex-replacement syntax, where - // ${name} means "substitute named capture group", not literal - // text. Since \${GIT_USER}/\${GIT_PASS} are meant to stay - // literal here (so the *shell* expands them from the - // credential-bound env vars at sh-step time, not Groovy — - // otherwise the secret value would land in a Groovy-processed - // string and defeat withCredentials' masking), that collided - // with a pattern that has no such named group and threw - // `IllegalArgumentException: named capturing group is - // missing trailing '}'`. - def urlParts = config.helm_repo_url.split('://', 2) - def authedUrl = "${urlParts[0]}://\${GIT_USER}:\${GIT_PASS}@${urlParts[1]}" - sh """ - git clone ${authedUrl} . - # This sh step isn't wrapped in container('docker-cli') - # (unlike runHooks/buildDocker), so it runs in the - # auto-injected jnlp agent container by default — a - # Debian-based jenkins/inbound-agent image, not Alpine, - # confirmed by `apk: not found` right after git clone - # worked fine in the same step. No package manager - # assumption is safe here since the underlying - # container/distro isn't pinned — fetch the static - # mikefarah/yq binary directly instead, to /tmp (always - # writable, unlike /usr/local/bin under a non-root - # agent user). - if ! command -v yq >/dev/null 2>&1; then - curl -sL -o /tmp/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 - chmod +x /tmp/yq - fi - YQ=\$(command -v yq || echo /tmp/yq) - \$YQ -i '${config.image_tag_yq_path} = "${env.TAG}"' ${valuesFile} - git config user.email 'jenkins-ci@homelab.local' - git config user.name 'jenkins-ci' - git commit -am 'ci: bump ${config.repo_name}/${config.service_name} image tag to ${env.TAG}' - git push origin main - """ + // git and yq are both baked into the docker-cli image now (see + // build-tools.Dockerfile) — this used to run unwrapped, defaulting + // to the auto-injected jnlp agent container (which has git but no + // yq, confirmed by `apk: not found` when trying to install yq + // on demand there), needing a curl-downloaded yq fallback to + // /tmp every single build. Wrapping in container('docker-cli') + // means both tools are simply already there. + container('docker-cli') { + withCredentials([usernamePassword(credentialsId: config.gitea_cred ?: 'gitea-ci-credentials', usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) { + dir('helm-chart-repo') { + deleteDir() + // Plain split, not replaceFirst — replaceFirst's *replacement* + // argument is also parsed as regex-replacement syntax, where + // ${name} means "substitute named capture group", not literal + // text. Since \${GIT_USER}/\${GIT_PASS} are meant to stay + // literal here (so the *shell* expands them from the + // credential-bound env vars at sh-step time, not Groovy — + // otherwise the secret value would land in a Groovy-processed + // string and defeat withCredentials' masking), that collided + // with a pattern that has no such named group and threw + // `IllegalArgumentException: named capturing group is + // missing trailing '}'`. + def urlParts = config.helm_repo_url.split('://', 2) + def authedUrl = "${urlParts[0]}://\${GIT_USER}:\${GIT_PASS}@${urlParts[1]}" + sh """ + git clone ${authedUrl} . + yq -i '${config.image_tag_yq_path} = "${env.TAG}"' ${valuesFile} + git config user.email 'jenkins-ci@homelab.local' + git config user.name 'jenkins-ci' + git commit -am 'ci: bump ${config.repo_name}/${config.service_name} image tag to ${env.TAG}' + git push origin main + """ + } } } }