Multi Arch Docker Image Pipeline
Mit dieser Bitbucket Pipeline kannst du Multi Architektur Docker Images Bauen und im Docker Hub veröffentlichen sowie bei Fehlern und Erfolg Discord Webhook Nachrichten senden. Die Pipeline verwendet die Container Structure Tests, um die Docker Images zu testen. Die Tests werden in einem JUnit-Format ausgegeben und können in Bitbucket Pipelines angezeigt werden.
Für die Pipeline verwende ich mein eigenes Docker Image, welches auch über diese Pipeline gebaut wird. Das Docker Image ist auf Docker Hub verfügbar und kann in der Pipeline verwendet werden.
wimdevgroup/docker-buildx-pipeline:latest
Die Pipeline benötigt einen Bitbucket Runner, um die Images zu bauen da Bitbucket das bauen von multi architektur Images nicht unterstützt. Daher wird der bau prozess auf einen eigenen Runner/Server ausgelagert.
Die verwendung vom Discord Webhook ist optional und kann entfernt werden. Die Pipeline ohne Discord Webhook werde ich auch zur verfügung stellen.
Voraussetzungen
- Docker Hub Account
- Bitbucket
- Self Hosted Bitbucket Runner
- Ein System mit Docker und Docker Buildx auf dem der Bitbucket Runner läuft
- Discord Webhook URL
Schritte
1. Repositroy Forken
Forke Repository
2. Server einrichten
Docker und Docker Buildx auf dem Server installieren sowie auch den Bitbucket Runner einrichten.
3. Repository vorbereiten
Repository Variablen setzten
Docker Hub Username und Passwort
DOCKERHUB_USERNAME=your-dockerhub-username
DOCKERHUB_PASSWORD=your-dockerhub-password
SSH Server, User, Passwort und Pfad
SSH_USER=your-ssh-user
SSH_PASSWORD=your-ssh-password
SSH_HOST=your-ssh-host
SSH_PATH=your-ssh-path
Discord Webhook URL
DISCORD_WEBHOOK_URL=your-discord-webhook-url
Ordnerstruktur des Repositorys
.
├── README.md
├── bitbucket-pipelines.yml
├── build.sh
├── container
│ └── <IMAGE-NAME>
│ ├── .Dockerfile
│ ├── .<TAG>.env
│ └── .version
├── container-structure-test-junit-compat.xsl
└── notification.sh
Bitucket Pipeline
bitbucket-pipelines.yml |
---|
| image: wimdevgroup/docker-buildx-pipeline:latest
definitions:
strings:
cst-install: &cst-install |
curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64;
chmod +x container-structure-test-linux-amd64;
mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test;
export PATH=$PATH:/usr/local/bin/
steps:
- step: &build-step
after-script:
- |
for report in $(find . -type f -name 'report*.xml'); do
xsltproc --output "${report}.tmp" container-structure-test-junit-compat.xsl "${report}";
mv "${report}.tmp" "${report}";
done
if [[ BITBUCKET_EXIT_CODE -eq 0 ]]; then
DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} BITBUCKET_WORKSPACE=${BITBUCKET_WORKSPACE} BITBUCKET_REPO_FULL_NAME=${BITBUCKET_REPO_FULL_NAME} CONTAINER=${container} BITBUCKET_PIPELINE_UUID=${BITBUCKET_PIPELINE_UUID} PIPELINE_STATE=1 ./notification.sh
else
DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} BITBUCKET_WORKSPACE=${BITBUCKET_WORKSPACE} BITBUCKET_REPO_FULL_NAME=${BITBUCKET_REPO_FULL_NAME} CONTAINER=${container} BITBUCKET_PIPELINE_UUID=${BITBUCKET_PIPELINE_UUID} PIPELINE_STATE=2 ./notification.sh
fi
pipelines:
tags:
release/*:
- step:
<<: *build-step
name: Build and publish docker
runs-on:
- 'self.hosted'
- 'workspace'
deployment: production
script:
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} BITBUCKET_WORKSPACE=${BITBUCKET_WORKSPACE} BITBUCKET_REPO_FULL_NAME=${BITBUCKET_REPO_FULL_NAME} CONTAINER=${container} BITBUCKET_PIPELINE_UUID=${BITBUCKET_PIPELINE_UUID} PIPELINE_STATE=0 ./notification.sh
- *cst-install
- export SSH_KNOWN_HOSTS=$(ssh-keyscan -H ${SSH_HOST})
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- ping -c 3 ${SSH_HOST}
- sshpass -p "${SSH_PASSWORD}" rsync -avz --progress build.sh container-structure-test-junit-compat.xsl "${SSH_USER}@${SSH_HOST}:pipeline/"
- sshpass -p ${SSH_PASSWORD} rsync -avz --progress container/ "${SSH_USER}@${SSH_HOST}:pipeline/container/"
- sshpass -p ${SSH_PASSWORD} ssh ${SSH_USER}@${SSH_HOST} "cd ${SSH_PATH} && DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME} DOCKERHUB_PASSWORD=${DOCKERHUB_PASSWORD} ./build.sh ${container} --push"
- sshpass -p ${SSH_PASSWORD} ssh ${SSH_USER}@${SSH_HOST} "rm -rf pipeline"
pull-requests:
'**':
- step:
<<: *build-step
name: Test build
runs-on:
- 'self.hosted'
- 'workspace'
script:
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} BITBUCKET_WORKSPACE=${BITBUCKET_WORKSPACE} BITBUCKET_REPO_FULL_NAME=${BITBUCKET_REPO_FULL_NAME} CONTAINER=${container} BITBUCKET_PIPELINE_UUID=${BITBUCKET_PIPELINE_UUID} PIPELINE_STATE=0 ./notification.sh
- *cst-install
- export SSH_KNOWN_HOSTS=$(ssh-keyscan -H ${SSH_HOST})
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- ping -c 3 ${SSH_HOST}
- sshpass -p "${SSH_PASSWORD}" rsync -avz --progress build.sh container-structure-test-junit-compat.xsl "${SSH_USER}@${SSH_HOST}:pipeline/"
- sshpass -p ${SSH_PASSWORD} rsync -avz --progress container/ "${SSH_USER}@${SSH_HOST}:pipeline/container/"
- sshpass -p ${SSH_PASSWORD} ssh ${SSH_USER}@${SSH_HOST} "cd ${SSH_PATH} && DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME} DOCKERHUB_PASSWORD=${DOCKERHUB_PASSWORD} ./build.sh ${container} --push"
- sshpass -p ${SSH_PASSWORD} ssh ${SSH_USER}@${SSH_HOST} "rm -rf pipeline"
branches:
main:
- step:
name: Auto tag release
script:
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} BITBUCKET_WORKSPACE=${BITBUCKET_WORKSPACE} BITBUCKET_REPO_FULL_NAME=${BITBUCKET_REPO_FULL_NAME} CONTAINER=${container} BITBUCKET_PIPELINE_UUID=${BITBUCKET_PIPELINE_UUID} PIPELINE_STATE=3 ./notification.sh
- apt-get update && apt-get install -y --no-install-recommends git
- git tag -a "release/${BITBUCKET_BUILD_NUMBER}" -m "release/${BITBUCKET_BUILD_NUMBER} on ${BITBUCKET_COMMIT}" "${BITBUCKET_COMMIT}"
- git push origin "release/${BITBUCKET_BUILD_NUMBER}"
custom:
build-and-deploy:
- variables:
- name: container
- step:
<<: *build-step
name: Manual build and deploy
runs-on:
- 'self.hosted'
- 'workspace'
deployment: production
script:
- *cst-install
- chmod +x ./notification.sh
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} BITBUCKET_WORKSPACE=${BITBUCKET_WORKSPACE} BITBUCKET_REPO_FULL_NAME=${BITBUCKET_REPO_FULL_NAME} CONTAINER=${container} BITBUCKET_PIPELINE_UUID=${BITBUCKET_PIPELINE_UUID} PIPELINE_STATE=0 ./notification.sh
- export SSH_KNOWN_HOSTS=$(ssh-keyscan -H ${SSH_HOST})
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- ping -c 3 ${SSH_HOST}
- sshpass -p "${SSH_PASSWORD}" rsync -avz --progress build.sh container-structure-test-junit-compat.xsl notification.sh "${SSH_USER}@${SSH_HOST}:pipeline/"
- sshpass -p ${SSH_PASSWORD} rsync -avz --progress container/ "${SSH_USER}@${SSH_HOST}:pipeline/container/"
- sshpass -p ${SSH_PASSWORD} ssh ${SSH_USER}@${SSH_HOST} "cd ${SSH_PATH} && DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME} DOCKERHUB_PASSWORD=${DOCKERHUB_PASSWORD} ./build.sh ${container} --push"
- sshpass -p ${SSH_PASSWORD} ssh ${SSH_USER}@${SSH_HOST} "rm -rf pipeline"
|
build.sh
build.sh |
---|
| #!/bin/bash
# @see https://stackoverflow.com/a/20460402
stringContains() {
case $2 in
*$1* )
return 0
;;
*)
return 1
;;
esac;
}
if [ -z ${DOCKERHUB_USERNAME+x} ]; then
echo "[ERROR] DOCKERHUB_USERNAME is not provided. Username to a hub.docker.com profile expected"
exit 1
fi;
if ! [ -x "$(command -v envsubst)" ]; then
echo "[ERROR] envsubst is not installed."
exit 1
fi;
if ! [ -x "$(command -v container-structure-test)" ]; then
echo "[ERROR] container-structure-test is not installed."
exit 1
fi;
if [[ "$@" == *"--push" ]]; then
if [ -z ${DOCKERHUB_PASSWORD+x} ]; then
echo "[ERROR] DOCKERHUB_PASSWORD is not provided. Password to the hub.docker.com profile ${DOCKERHUB_USERNAME} expected"
exit 1
fi;
echo "[INFO] Logging into hub.docker.com"
echo ${DOCKERHUB_PASSWORD} | docker login --username "${DOCKERHUB_USERNAME}" --password-stdin
echo "[INFO] Logged into hub.docker.com Successfully"
fi
echo "[INFO] Check for cloud buildx builder version"
if !(docker buildx version); then
echo "[ERROR] Failed to Check for cloud buildx builder version"
exit 1
fi;
echo "[INFO] Check for cloud buildx builder"
if !(docker buildx ls); then
echo "[ERROR] Failed to Check for cloud buildx builder"
exit 1
fi;
echo "[INFO] Check for cloud buildx builder"
if !(docker buildx ls); then
echo "[ERROR] Failed to Check for cloud buildx builder"
exit 1
fi;
containerdir=container
if ! [ -z "$1" ] && [ -d "${containerdir}/$1" ]; then
containerdir=${containerdir}/$1
fi
echo "[INFO] Search for images in ${containerdir}"
for dockerfile in $(find "${containerdir}" -type f -name .Dockerfile -exec ls {} \;); do
dockerdir=$(dirname $dockerfile)
versionfile="${dockerdir}/.version"
testreportdir="${containerdir}/test-results/"
mkdir -p "${testreportdir}"
if ! [ -f "$versionfile" ]; then
echo "[ERROR] No version file (${versionfile}) created"
exit 1
fi;
if [ $(cat "$versionfile" | wc -m) == "0" ]; then
echo "[INFO] No version defined in ${versionfile}"
fi;
VERSION=$(cat $versionfile)
DOCKERIMAGE=$(basename $dockerdir)
echo "[INFO] Process ${DOCKERIMAGE}@${VERSION} (${dockerdir})"
for tagfile in $(find "$dockerdir" -maxdepth 1 -type f -name '.*.env' -exec ls {} \;); do
tagfilename=$(basename "$tagfile" .env)
TAG=${tagfilename:1}
echo "[INFO] Process ${DOCKERIMAGE}:${TAG}"
echo "[INFO] Reset build directory"
if [ -d .build ]; then
rm -rf .build
fi;
if !(mkdir .build); then
echo "[ERROR] Cannot create build directory"
exit 1
fi;
echo "[INFO] Process image template"
envcontent=$(grep -v "^#" "$tagfile" | sed -e 's/^\\s*(.*?)\\s*$/\\1/' | sed -e 's/^$//')
if [ -z ${envcontent+x} -o $(echo "$envcontent" | wc -m) -lt 2 ]; then
echo "[INFO] Empty environment provided"
cat "$dockerfile" | TAG=$TAG envsubst > .build/Dockerfile
else
cat "$dockerfile" | (export $(echo "$envcontent" | xargs) && TAG=$TAG envsubst > .build/Dockerfile)
fi;
if [ $(ls -1 "${dockerdir}" | wc -m) == "0" ]; then
echo "[INFO] No data files to copy"
else
if !(cp -a ${dockerdir}/* .build); then
echo "[ERROR] Cannot copy data to build directory"
exit 1
else
echo "[INFO] Copy data to build directory"
fi;
fi;
if [[ "$VERSION" =~ ^[0-9]+$ ]]; then
echo "VERSION enthält nur Zahlen."
if [[ "${TAG}" == "latest" ]]; then
tagversion="${VERSION}"
taglatest="latest"
echo "[INFO] Tag latest ${tagversion}"
else
tagversion="${TAG}-${VERSION}"
taglatest="${TAG}-latest"
echo "[INFO] Tag ${tagversion}"
fi;
else
tagversion="${TAG}"
fi
echo "[INFO] Build image"
if !(docker buildx build --builder builder --tag "${DOCKERHUB_USERNAME}/${DOCKERIMAGE}:${tagversion}" --platform linux/amd64,linux/arm64 --push .build); then
echo "[ERROR] Failed building image"
exit 1
fi;
echo "[INFO] Prepare test files"
[[ ! -d "${containerdir}/tests" ]] || rm -rf "${containerdir}/tests"
mkdir -p "${containerdir}/tests/"
for testfile in $(find "${containerdir}/.tests" -type f -name '*.yml' -exec ls {} \;); do
testfile_basename=$(basename "${testfile}")
# number of words determines, whether a version is in the file name
testfile_basename_words="${testfile_basename//./ }"
testfile_basename_n_words=$(echo "${testfile_basename_words}" | wc -w)
if [[ ${testfile_basename_n_words} -ne 2 ]]; then # file has at least two dots and therefore has a tag in it
if ! stringContains ".${TAG}.yml" "${testfile_basename}"; then # the filename has not the tag in question in it? then skip
echo "[INFO] Skip ${testfile} as it seems not to match the tag, that needs to be tested (testfile_basename_n_words: ${testfile_basename_n_words} ; testfile_basename_words: ${testfile_basename_words} ; TAG: ${TAG} ; testfile_basename: ${testfile_basename})"
continue
fi
echo "[INFO] Take ${testfile} as it seems to match the tag, that needs to be tested (testfile_basename_n_words: ${testfile_basename_n_words} ; testfile_basename_words: ${testfile_basename_words} ; TAG: ${TAG} ; testfile_basename: ${testfile_basename})"
else
echo "[INFO] Take ${testfile} as it seems to be used on any tag (testfile_basename_n_words: ${testfile_basename_n_words} ; testfile_basename_words: ${testfile_basename_words})"
fi
if [ -z ${envcontent+x} -o $(echo "$envcontent" | wc -m) -lt 2 ]; then # no env vars to replace
cat "${testfile}" | TAG=$TAG envsubst > "${testfile/.tests/tests}"
else
cat "${testfile}" | (export $(echo "$envcontent" | xargs) && TAG=$TAG envsubst > "${testfile/.tests/tests}")
fi;
done
for testfile in $(find "${containerdir}/tests" -type f -name '*.yml' -exec ls {} \;); do
testfilename=$(basename "$testfile" .yml)
reportfile=${testreportdir}/report-${DOCKERIMAGE}-${tagfilename}-${testfilename}.xml
echo "[INFO] Test image against ${testfilename}"
if !(container-structure-test test --image "${DOCKERHUB_USERNAME}/${DOCKERIMAGE}:${tagversion}" --config "${testfile}" --output junit --test-report "${reportfile}") then
echo "[ERROR] Failed testing image"
exit 1
fi;
done
done;
done;
|
notification.sh
notification.sh |
---|
| #!/bin/bash
# @see https://stackoverflow.com/a/20460402
stringContains() {
case $2 in
*$1* )
return 0
;;
*)
return 1
;;
esac;
}
if [ -z ${DISCORD_WEBHOOK_URL+x} ]; then
echo "[ERROR] DISCORD_WEBHOOK_URL is not provided. Webhook to a discord.com channel expected."
exit 1
fi;
if [ -z ${BITBUCKET_WORKSPACE+x} ]; then
echo "[ERROR] BITBUCKET_WORKSPACE is not provided."
exit 1
fi;
if [ -z ${BITBUCKET_REPO_FULL_NAME+x} ]; then
echo "[INFO] BITBUCKET_REPO_FULL_NAME is not provided."
BITBUCKET_REPO_FULL_NAME="unknown"
fi;
if [ -z ${container+x} ]; then
echo "[INFO] container is not provided."
DOCKER_IMAGE="All containers"
else
DOCKER_IMAGE=$container
fi;
if [ -z ${PIPELINE_STATE+x} ]; then
echo "[ERROR] $PIPELINE_STATE is not provided."
exit 1
fi;
if [ $PIPELINE_STATE -eq 1 ]; then
echo "[INFO] Pipeline is successful"
PIPELINE_STATE="successful"
PIPELINE_STATE_EMOTE="✅"
COLOR=3066993
elif [ $PIPELINE_STATE -eq 2 ]; then
echo "[INFO] Pipeline is failed"
PIPELINE_STATE="failed"
PIPELINE_STATE_EMOTE="❌"
COLOR=15158332
elif [ $PIPELINE_STATE -eq 3 ]; then
echo "[INFO] Automatic tag release"
PIPELINE_STATE="Automatic tag release"
PIPELINE_STATE_EMOTE="🔄"
COLOR=3447003
else
echo "[INFO] Pipeline is in progress"
PIPELINE_STATE="in progress"
PIPELINE_STATE_EMOTE="🔄"
COLOR=3447003
fi
# Textvariablen und Testdaten
USERNAME="$BITBUCKET_WORKSPACE Bitbucket Pipeline for Repository $BITBUCKET_REPO_FULL_NAME"
MESSAGE="@everyone Pipeline $PIPELINE_STATE!"
DATA_TITLE="$PIPELINE_STATE_EMOTE Pipeline Status $PIPELINE_STATE $PIPELINE_STATE_EMOTE"
DATA="**Docker Image**\\n$DOCKER_IMAGE\\n\u200B\\n**Status**\\n$PIPELINE_STATE\\n\u200B\\n[$BITBUCKET_REPO_FULL_NAME](https://bitbucket.org/$BITBUCKET_REPO_FULL_NAME/pipelines/results/$BITBUCKET_PIPELINE_UUID)"
IMAGE_URL="https://images.wimwenigerkind.com/wimwenigerkind-transparent-icon.png" # Ersetze dies mit der Bild-URL für das Profilbild
# JSON-Payload ausklappen und klar strukturiert ins Skript integrieren
PAYLOAD="{
\"username\": \"$USERNAME\",
\"avatar_url\": \"$IMAGE_URL\",
\"content\": \"$MESSAGE\",
\"embeds\": [
{
\"title\": \"$DATA_TITLE\",
\"description\": \"$DATA\",
\"color\": $COLOR
}
]
}"
# Webhook senden
curl -X POST -H "Content-Type: application/json" -d "$PAYLOAD" $DISCORD_WEBHOOK_URL
echo "Webhook mit benutzerdefiniertem Profilbild gesendet!"
|
container-structure-test-junit-compat.xsl
container-structure-test-junit-compat.xsl |
---|
| <xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output
omit-xml-declaration="yes"
indent="yes"
/>
<xsl:strip-space elements="*" />
<!-- recursive through all nodes -->
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
<!-- copy <testsuite> and add attributes "failure" and "tests" based in the testsuite children nodes -->
<xsl:template match="testsuite">
<testsuite
failures="{count(testcase/failure)}"
tests="{count(testcase)}"
>
<xsl:apply-templates select="node()|@*" />
</testsuite>
</xsl:template>
</xsl:stylesheet>
|