ci: refactor harbor arm build pipeline

This commit is contained in:
bstein 2025-12-18 01:26:53 -03:00
parent 8f3b22eb58
commit 75bda19605
2 changed files with 125 additions and 113 deletions

226
Jenkinsfile vendored
View File

@ -1,6 +1,10 @@
pipeline { pipeline {
triggers { parameters {
pollSCM('H/5 * * * *') string(
name: 'HARBOR_VERSION',
defaultValue: '',
description: 'Harbor tag to build (e.g. v2.14.1). Leave empty to build the latest release.'
)
} }
agent { agent {
kubernetes { kubernetes {
@ -12,52 +16,29 @@ kind: Pod
spec: spec:
nodeSelector: nodeSelector:
kubernetes.io/arch: arm64 kubernetes.io/arch: arm64
hardware: rpi5
containers: containers:
- name: dind - name: builder
image: docker:27-dind image: quay.io/podman/stable:v5.2
tty: true
command: ["cat"]
securityContext: securityContext:
privileged: true privileged: true
env: env:
- name: DOCKER_TLS_CERTDIR - name: DOCKER_CONFIG
value: "" value: /root/.docker
command: - name: REGISTRY_AUTH_FILE
- /bin/sh value: /root/.docker/config.json
- -c - name: XDG_RUNTIME_DIR
args: value: /var/run/containers
- |
set -euo pipefail
apk add --no-cache dnsmasq
cat > /etc/dnsmasq.conf <<'EOF'
filter-AAAA
# Use multiple upstream DNS servers to avoid transient resolver outages.
# Keep cluster DNS first for internal names, then fall back to public resolvers.
server=10.43.0.10
server=1.1.1.1
server=8.8.8.8
EOF
dnsmasq
echo "nameserver 127.0.0.1" > /etc/resolv.conf
exec dockerd-entrypoint.sh --mtu=1400
volumeMounts:
- name: dind-storage
mountPath: /var/lib/docker
- name: builder
image: docker:27
command: ["cat"]
tty: true
env:
- name: DOCKER_HOST
value: tcp://localhost:2375
- name: DOCKER_TLS_CERTDIR
value: ""
volumeMounts: volumeMounts:
- name: workspace-volume - name: workspace-volume
mountPath: /home/jenkins/agent mountPath: /home/jenkins/agent
- name: docker-config-writable
mountPath: /root/.docker
- name: docker-config-secret - name: docker-config-secret
mountPath: /docker-config mountPath: /docker-config
- name: containers-storage
mountPath: /var/lib/containers
- name: podman-runtime
mountPath: /var/run/containers
volumes: volumes:
- name: docker-config-secret - name: docker-config-secret
secret: secret:
@ -65,130 +46,147 @@ spec:
items: items:
- key: .dockerconfigjson - key: .dockerconfigjson
path: config.json path: config.json
- name: docker-config-writable
emptyDir: {}
- name: workspace-volume - name: workspace-volume
emptyDir: {} emptyDir: {}
- name: dind-storage - name: containers-storage
emptyDir: {}
- name: podman-runtime
emptyDir: {} emptyDir: {}
""" """
} }
} }
environment { environment {
VERSION = 'v2.14.1'
TAG_SUFFIX = '-arm64'
// Avoid `REGISTRY` here because Harbor's Makefiles use `REGISTRY=registry` and
// run sub-makes with `-e`, so environment variables override make vars.
IMAGE_NAMESPACE = 'registry.bstein.dev/infra' IMAGE_NAMESPACE = 'registry.bstein.dev/infra'
HARBOR_TARBALL = "https://github.com/goharbor/harbor/archive/refs/tags/${VERSION}.tar.gz" TAG_SUFFIX = '-arm64'
REGISTRY_URL = 'registry.bstein.dev'
} }
options { options {
disableConcurrentBuilds() disableConcurrentBuilds()
timestamps()
} }
stages { stages {
stage('Checkout Jenkinsfile') { stage('Checkout pipeline repo') {
steps { steps {
git credentialsId: 'gitea-pat', url: 'https://scm.bstein.dev/bstein/harbor-arm-build.git' git credentialsId: 'gitea-pat', url: 'https://scm.bstein.dev/bstein/harbor-arm-build.git'
} }
} }
stage('Prep toolchain') { stage('Setup tooling') {
steps { steps {
container('builder') { container('builder') {
sh ''' sh '''
set -euo pipefail set -euo pipefail
apk add --no-cache bash curl make tar gzip git coreutils go ncurses microdnf -y install git make curl tar gzip jq golang podman-docker python3
mkdir -p "${DOCKER_CONFIG}"
cp /docker-config/config.json "${DOCKER_CONFIG}/config.json"
# Make the Docker CLI invoke podman (buildah backend).
ln -sf /usr/bin/podman /usr/local/bin/docker
mkdir -p "${XDG_RUNTIME_DIR}"
''' '''
} }
} }
} }
stage('Fetch harbor source') { stage('Resolve version') {
steps { steps {
container('builder') { container('builder') {
sh ''' script {
def resolved = sh(
label: 'pick version',
returnStdout: true,
script: '''
set -euo pipefail
if [ -n "${HARBOR_VERSION:-}" ]; then
echo "${HARBOR_VERSION}"
exit 0
fi
curl -sSL https://api.github.com/repos/goharbor/harbor/releases/latest | python3 - <<'PY'
import json, sys
data = json.load(sys.stdin)
print(data.get("tag_name", "").strip())
PY
'''
).trim()
env.HARBOR_VERSION_RESOLVED = resolved
env.HARBOR_SRC_DIR = "harbor-src/harbor-${resolved.startsWith('v') ? resolved.substring(1) : resolved}"
}
}
}
}
stage('Fetch source') {
steps {
container('builder') {
sh """
set -euo pipefail set -euo pipefail
rm -rf harbor-src rm -rf harbor-src
mkdir -p harbor-src mkdir -p harbor-src
curl -sSL "${HARBOR_TARBALL}" | tar xz -C harbor-src curl -sSL "https://github.com/goharbor/harbor/archive/refs/tags/${env.HARBOR_VERSION_RESOLVED}.tar.gz" | tar xz -C harbor-src
''' ls -la harbor-src
"""
} }
} }
} }
stage('Build & push arm64 images') { stage('Build & push (podman/buildah)') {
steps { steps {
container('builder') { container('builder') {
sh ''' sh """
set -euo pipefail set -euo pipefail
VERSIONTAG="${VERSION}${TAG_SUFFIX}.${BUILD_NUMBER}" export VERSIONTAG=\"${env.HARBOR_VERSION_RESOLVED}${TAG_SUFFIX}.${BUILD_NUMBER}\"
export VERSIONTAG export BASEIMAGETAG=\"${env.HARBOR_VERSION_RESOLVED}${TAG_SUFFIX}.${BUILD_NUMBER}\"
mkdir -p /root/.docker export IMAGENAMESPACE=\"${IMAGE_NAMESPACE}\"
cp /docker-config/config.json /root/.docker/config.json export BASEIMAGENAMESPACE=\"${IMAGE_NAMESPACE}\"
export DOCKERNETWORK=host
# Harbor's build uses git metadata; Jenkins workspace ownership can trigger export PULL_BASE_FROM_DOCKERHUB=false
# Git's safe.directory protection in containerized builds. export BUILD_BASE=true
export BUILDTRIVYADP=false
export BUILD_INSTALLER=true
export BUILDAH_ISOLATION=chroot
export REGISTRY_AUTH_FILE=\"${DOCKER_CONFIG}/config.json\"
git config --global --add safe.directory '*' git config --global --add safe.directory '*'
cd \"${env.HARBOR_SRC_DIR}\"
# `harbor-src/` itself matches `harbor-*`, so exclude it.
SRC_DIR=$(find harbor-src -mindepth 1 -maxdepth 1 -type d -name "harbor-*" | head -n1)
cd "${SRC_DIR}"
export DOCKER_BUILDKIT=1
echo "Sanity-checking Makefile vars..."
make -n \
VERSIONTAG="${VERSIONTAG}" \
BASEIMAGETAG="${VERSIONTAG}" \
IMAGENAMESPACE="${IMAGE_NAMESPACE}" \
BASEIMAGENAMESPACE="${IMAGE_NAMESPACE}" \
DOCKERNETWORK=host \
PULL_BASE_FROM_DOCKERHUB=false \
BUILD_BASE=true \
BUILDTRIVYADP=false \
BUILD_INSTALLER=true \
build \
| grep -E 'make -f .*/make/photon/Makefile build|IMAGENAMESPACE=|BASEIMAGENAMESPACE=|VERSIONTAG=|BASEIMAGETAG=|PULL_BASE_FROM_DOCKERHUB=' \
|| true
# Harbor's root Makefile hard-sets many defaults (e.g. IMAGENAMESPACE=goharbor,
# PULL_BASE_FROM_DOCKERHUB=true). Environment exports are not sufficient because
# makefile assignments override the environment. Pass overrides on the make
# command line so they win.
make compile make compile
make \ make \\
VERSIONTAG="${VERSIONTAG}" \ VERSIONTAG=\"${env.HARBOR_VERSION_RESOLVED}${TAG_SUFFIX}.${BUILD_NUMBER}\" \\
BASEIMAGETAG="${VERSIONTAG}" \ BASEIMAGETAG=\"${env.HARBOR_VERSION_RESOLVED}${TAG_SUFFIX}.${BUILD_NUMBER}\" \\
IMAGENAMESPACE="${IMAGE_NAMESPACE}" \ IMAGENAMESPACE=\"${IMAGE_NAMESPACE}\" \\
BASEIMAGENAMESPACE="${IMAGE_NAMESPACE}" \ BASEIMAGENAMESPACE=\"${IMAGE_NAMESPACE}\" \\
DOCKERNETWORK=host \ DOCKERNETWORK=host \\
PULL_BASE_FROM_DOCKERHUB=false \ PULL_BASE_FROM_DOCKERHUB=false \\
BUILD_BASE=true \ BUILD_BASE=true \\
BUILDTRIVYADP=false \ BUILDTRIVYADP=false \\
BUILD_INSTALLER=true \ BUILD_INSTALLER=true \\
build build
# Retag a few upstream image names to our internal naming convention for pair in \\
# (so Helm values can keep using `harbor-*` consistently). \"prepare:harbor-prepare\" \\
docker tag "${IMAGE_NAMESPACE}/prepare:${VERSIONTAG}" "${IMAGE_NAMESPACE}/harbor-prepare:${VERSIONTAG}" || true \"redis-photon:harbor-redis\" \\
docker tag "${IMAGE_NAMESPACE}/redis-photon:${VERSIONTAG}" "${IMAGE_NAMESPACE}/harbor-redis:${VERSIONTAG}" || true \"nginx-photon:harbor-nginx\" \\
docker tag "${IMAGE_NAMESPACE}/nginx-photon:${VERSIONTAG}" "${IMAGE_NAMESPACE}/harbor-nginx:${VERSIONTAG}" || true \"registry-photon:harbor-registry\"; do
docker tag "${IMAGE_NAMESPACE}/registry-photon:${VERSIONTAG}" "${IMAGE_NAMESPACE}/harbor-registry:${VERSIONTAG}" || true src=\"${IMAGE_NAMESPACE}/$(echo "$pair" | cut -d: -f1):${env.HARBOR_VERSION_RESOLVED}${TAG_SUFFIX}.${BUILD_NUMBER}\"
dst=\"${IMAGE_NAMESPACE}/$(echo "$pair" | cut -d: -f2):${env.HARBOR_VERSION_RESOLVED}${TAG_SUFFIX}.${BUILD_NUMBER}\"
# Push every image we just built for this tag under our namespace. if podman image exists \"$src\"; then
docker images --format '{{.Repository}}:{{.Tag}}' \ podman tag \"$src\" \"$dst\" || true
| awk -v ns="${IMAGE_NAMESPACE}/" -v tag="${VERSIONTAG}" 'index($0, ns)==1 && $0 ~ ":"tag"$"' \ fi
| sort -u \
| while read -r img; do
echo "Pushing ${img}"
docker push "${img}"
done done
'''
podman images --format '{{.Repository}}:{{.Tag}}' \\
| awk -v ns=\"${IMAGE_NAMESPACE}/\" -v tag=\"${env.HARBOR_VERSION_RESOLVED}${TAG_SUFFIX}.${BUILD_NUMBER}\" 'index($0, ns)==1 && $0 ~ ":"tag"$"' \\
| sort -u \\
| while read -r img; do
echo \"Pushing ${img}\"
podman push \"${img}\"
done
"""
} }
} }
} }
} }
post { post {
always { echo 'done' } always {
echo "done"
}
} }
} }

View File

@ -1,2 +1,16 @@
# harbor-arm-build # harbor-arm-build
Jenkins pipeline: build arm images and push to Harbor.
Jenkins pipeline for building an arm64 Harbor release straight from upstream and
pushing the full image set into `registry.bstein.dev/infra`.
Key behavior:
- Parameter `HARBOR_VERSION` (e.g. `v2.14.1`). If left empty, the pipeline
fetches the latest Harbor GitHub release tag.
- Runs on an arm64 node with podman/buildah (Docker CLI shim via `podman-docker`);
no dind is used.
- Builds all Harbor images with `IMAGENAMESPACE=registry.bstein.dev/infra` and
tags `${HARBOR_VERSION}-arm64.$BUILD_NUMBER`, then pushes them with the
`harbor-robot-pipeline` credentials.
Trigger manually in Jenkins when a new Harbor version needs to be published to
the internal registry.