flatcar-scripts/ci-automation/ci_automation_common.sh
Mathieu Tortuyaux fd42a17e10
ci_automation_common.sh: use long option name
This helps reading the scripts later.

Signed-off-by: Mathieu Tortuyaux <mtortuyaux@microsoft.com>
2025-07-24 09:33:10 +02:00

507 lines
16 KiB
Bash

#!/bin/bash
#
# Copyright (c) 2021 The Flatcar Maintainers.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# CI automation common functions.
source ci-automation/ci-config.env
: ${docker:=docker}
: ${TEST_WORK_DIR:='__TESTS__'}
function check_version_string() {
local version="$1"
if [[ ! "${version}" =~ ^(main|alpha|beta|stable|lts)-[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then
echo "ERROR: invalid version '${version}', must start with 'main', 'alpha', 'beta', 'stable' or 'lts', followed by a dash and three dot-separated numbers, optionally followed by a dash and a non-empty build ID"
exit 1
fi
}
# --
function update_and_push_version() {
local version=${1}
local target_branch=${2:-}
# set up author and email so git does not complain when tagging
if ! git config --get user.name >/dev/null 2>&1 ; then
git config user.name "${CI_GIT_AUTHOR}"
fi
if ! git config --get user.email >/dev/null 2>&1 ; then
git config user.email "${CI_GIT_EMAIL}"
fi
# Add and commit local changes
git add "sdk_container/.repo/manifests/version.txt"
git commit --signoff --allow-empty -m "New version: ${version}"
git fetch --all --tags --force
local -i ret=0
git diff --quiet --exit-code "${version}" 2>/dev/null || ret=${?}
# This will return != 0 if
# - the remote tag does not exist (rc: 127)
# - the tag does not exist locally (rc: 128)
# - the remote tag has changes compared to the local tree (rc: 1)
if [[ ret -eq 0 ]]; then
# this means that we created an empty commit above, reusing the tag gets rid of it
echo "Reusing existing tag" >&2
git checkout --force "${version}"
return
elif [[ ret -eq 1 ]]; then
echo "Remote tag exists already and is not equal" >&2
return 1
elif [[ ret -ne 127 && ret -ne 128 ]]; then
echo "Error: Unexpected git diff return code (${ret})" >&2
return 1
fi
local -a TAG_ARGS=()
if [[ ${SIGN-0} = 1 ]]; then
TAG_ARGS=("--sign" "--message=${version}")
fi
git tag --force "${TAG_ARGS[@]}" "${version}"
if [[ -n ${target_branch} ]]; then
git push origin "HEAD:${target_branch}"
fi
git push origin "${version}"
}
# --
function check_bincache_images_existence() {
case ${CIA_DEBUGIMAGESEXIST:-} in
'yes') return 0;;
'no' ) return 1;;
'') curl --head --fail --fail-early --silent --show-error --location "${@}" || return 1;;
*) echo "Invalid CIA_DEBUGIMAGESEXIST value (${CIA_DEBUGIMAGESEXIST@Q})" >&2; exit 1;;
esac
}
# --
function copy_from_buildcache() {
local what="$1"
local where_to="$2"
mkdir -p "$where_to"
curl --fail --silent --show-error --location --retry-delay 1 --retry 60 \
--retry-connrefused --retry-max-time 60 --connect-timeout 20 \
--remote-name --output-dir "${where_to}" "https://${BUILDCACHE_SERVER}/${what}"
}
# --
function gen_sshcmd() {
echo -n "ssh -o BatchMode=yes"
echo -n " -o StrictHostKeyChecking=no"
echo -n " -o UserKnownHostsFile=/dev/null"
echo " -o NumberOfPasswordPrompts=0"
}
# --
function copy_dir_from_buildcache() {
local remote_path="${BUILDCACHE_PATH_PREFIX}/$1"
local local_path="$2"
local sshcmd="$(gen_sshcmd)"
mkdir -p "${local_path}"
rsync --partial -a -e "${sshcmd}" "${BUILDCACHE_USER}@${BUILDCACHE_SERVER}:${remote_path}" \
"${local_path}"
}
# --
function copy_to_buildcache() {
local remote_path="${BUILDCACHE_PATH_PREFIX}/$1"
shift
local sshcmd="$(gen_sshcmd)"
$sshcmd "${BUILDCACHE_USER}@${BUILDCACHE_SERVER}" \
"mkdir -p ${remote_path}"
rsync --partial -a -e "${sshcmd}" "$@" \
"${BUILDCACHE_USER}@${BUILDCACHE_SERVER}:${remote_path}"
}
# --
function image_exists_locally() {
local name="$1"
local version="$2"
local image="${name}:${version}"
local image_exists="$($docker images "${image}" \
--no-trunc --format '{{.Repository}}:{{.Tag}}')"
[ "${image}" = "${image_exists}" ]
}
# --
# Derive docker-safe image version string from vernum.
# Keep in sync with sdk_lib/sdk_container_common.sh
function vernum_to_docker_image_version() {
local vernum="$1"
echo "$vernum" | sed 's/[+]/-/g'
}
# --
# Return the full name (repo+name+tag) of an image. Useful for SDK images
# pulled from the registry (which have the registry pre-pended)
function docker_image_fullname() {
local image="$1"
local version="$2"
$docker images --no-trunc --format '{{.Repository}}:{{.Tag}}' \
| grep -E "^(${CONTAINER_REGISTRY}/)*${image}:${version}$"
}
# --
function docker_image_to_buildcache() {
local image="$1"
local version="$2"
# strip potential container registry prefix
local tarball="$(basename "$image")-${version}.tar.zst"
local id_file="$(basename "$image")-${version}.id"
$docker save "${image}":"${version}" | zstd -T0 -o "${tarball}"
# It is necessary to chmod from 0600 to 0644 to make it readable
# afterwards by rsync during the release process. Zstd sets the mode
# of the output file to 0600 in case of temporary files to avoid race
# condition. See also https://github.com/facebook/zstd/pull/1644,
# https://github.com/facebook/zstd/pull/3432.
chmod 0644 "${tarball}"
# Cut the "sha256:" prefix that is present in Docker but not in Podman
$docker image inspect "${image}":"${version}" | jq -r '.[].Id' | sed 's/^sha256://' > "${id_file}"
create_digests "${SIGNER:-}" "${tarball}" "${id_file}"
sign_artifacts "${SIGNER:-}" "${tarball}"* "${id_file}"*
copy_to_buildcache "containers/${version}" "${tarball}"* "${id_file}"*
}
# --
function docker_commit_to_buildcache() {
local container="$1"
local image_name="$2"
local image_version="$3"
$docker commit "${container}" "${image_name}:${image_version}"
docker_image_to_buildcache "${image_name}" "${image_version}"
}
# --
function docker_image_from_buildcache() {
local name="$1"
local version="$2"
local compr="${3:-zst}"
local tgz="${name}-${version}.tar.${compr}"
local id_file="${name}-${version}.id"
local id_file_url="https://${BUILDCACHE_SERVER}/containers/${version}/${id_file}"
local id_file_url_release="https://mirror.release.flatcar-linux.net/containers/${version}/${id_file}"
local local_image=""
if image_exists_locally "${name}" "${version}" ; then
local_image="${name}:${version}"
elif image_exists_locally "${CONTAINER_REGISTRY}/${name}" "${version}" ; then
local_image="${CONTAINER_REGISTRY}/${name}:${version}"
fi
if [[ -n "${local_image}" ]] ; then
local image_id=""
image_id=$($docker image inspect "${local_image}" | jq -r '.[].Id' | sed 's/^sha256://')
local remote_id=""
remote_id=$(curl --fail --silent --show-error --location --retry-delay 1 \
--retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 \
"${id_file_url}" \
|| curl --fail --silent --show-error --location --retry-delay 1 \
--retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 \
"${id_file_url_release}" \
|| echo "not found")
if [ "${image_id}" = "${remote_id}" ]; then
echo "Local image is up-to-date" >&2
return
fi
echo "Local image outdated, downloading..." >&2
fi
# First try bincache then release to allow a bincache overwrite
local url="https://${BUILDCACHE_SERVER}/containers/${version}/${tgz}"
local url_release="https://mirror.release.flatcar-linux.net/containers/${version}/${tgz}"
curl --fail --silent --show-error --location --retry-delay 1 --retry 60 \
--retry-connrefused --retry-max-time 60 --connect-timeout 20 \
--remote-name "${url}" \
|| curl --fail --silent --show-error --location --retry-delay 1 --retry 60 \
--retry-connrefused --retry-max-time 60 --connect-timeout 20 \
--remote-name "${url_release}"
# zstd can handle zlib as well :)
zstd -d -c ${tgz} | $docker load
rm "${tgz}"
}
# --
function docker_image_from_registry_or_buildcache() {
local image="$1"
local version="$2"
if $docker pull "${CONTAINER_REGISTRY}/${image}:${version}" ; then
return
fi
echo "Falling back to tar ball download..." >&2
docker_image_from_buildcache "${image}" "${version}" zst || \
docker_image_from_buildcache "${image}" "${version}" gz
}
# --
# Called by vendor test in case of complete failure not eligible for
# reruns (like trying to run tests on unsupported architecture).
function break_retest_cycle() {
local work_dir=$(dirname "${PWD}")
local dir=$(basename "${work_dir}")
if [[ "${dir}" != "${TEST_WORK_DIR}" ]]; then
echo "Not breaking retest cycle, expected test work dir to be a parent directory" >&2
return
fi
touch "${work_dir}/break_retests"
}
# --
# Called by test runner to see if the retest cycle should be broken.
function retest_cycle_broken() {
# Using the reverse boolean logic here!
local broken=1
if [[ -f "${TEST_WORK_DIR}/break_retests" ]]; then
broken=0
rm -f "${TEST_WORK_DIR}/break_retests"
fi
return ${broken}
}
# --
# Substitutes fields in the passed template and prints the
# result. Followed by the template, the parameters used for
# replacement are in alphabetical order: arch, channel, proto and
# vernum.
function url_from_template() {
local template="${1}"; shift
local arch="${1}"; shift
local channel="${1}"; shift
local proto="${1}"; shift
local vernum="${1}"; shift
local url="${template}"
url="${url//@ARCH@/${arch}}"
url="${url//@CHANNEL@/${channel}}"
url="${url//@PROTO@/${proto}}"
url="${url//@VERNUM@/${vernum}}"
echo "${url}"
}
# --
# Puts a secret into a file, while trying for the secret to not end up
# on a filesystem at all. A path to the file with the secret in /proc
# in put into the chosen variable. The secret is assumed to be
# base64-encoded.
#
# Typical use:
# secret_file=''
# secret_to_file secret_file "${some_secret}"
#
# Parameters:
# 1 - name of the variable where the path is stored
# 2 - the secret to store in the file
function secret_to_file() {
local config_var_name="${1}"; shift
local secret="${1}"; shift
local tmpfile=$(mktemp)
local -n config_ref="${config_var_name}"
local fd
exec {fd}<>"${tmpfile}"
rm -f "${tmpfile}"
echo "${secret}" | base64 --decode >&${fd}
# Use BASHPID because we may be in a subshell but $$ is only the main shell's PID
config_ref="/proc/${BASHPID}/fd/${fd}"
}
# --
# Creates signatures for the passed files and directories. In case of
# directory, all files inside are signed. Files ending with .asc or
# .sig or .gpg are ignored, though. This function is a noop if signer
# is empty.
#
# Typical use:
# sign_artifacts "${SIGNER}" artifact.tar.gz
# copy_to_buildcache "artifacts/directory" artifact.tar.gz*
#
# Parameters:
#
# 1 - signer whose key is expected to be already imported into the
# keyring
# @ - files and directories to sign
function sign_artifacts() {
local signer="${1}"; shift
# rest of the parameters are directories/files to sign
local to_sign=()
local file
if [[ -z "${signer}" ]]; then
return
fi
list_files to_sign 'asc,gpg,sig' "${@}"
for file in "${to_sign[@]}"; do
gpg --batch --local-user "${signer}" \
--output "${file}.sig" \
--detach-sign "${file}"
done
}
# --
# Creates digests files and armored ASCII files out of them for the
# passed files and directories. In case of directory, all files inside
# it are processed. No new digests file is created if there is one
# already for the processed file. Same for armored ASCII file. Files
# ending with .asc or .sig or .gpg or .DIGESTS are not processed. The
# armored ASCII files won't be created if the signer is empty.
#
# Typical use:
# create_digests "${SIGNER}" artifact.tar.gz
# sign_artifacts "${SIGNER}" artifact.tar.gz*
# copy_to_buildcache "artifacts/directory" artifact.tar.gz*
#
# Parameters:
#
# 1 - signer whose key is expected to be already imported into the
# keyring
# @ - files and directories to create digests for
function create_digests() {
local signer="${1}"; shift
# rest of the parameters are files or directories to create
# digests for
local to_digest=()
local file
local df
local fbn
local hash_type
local output
local af
list_files to_digest 'asc,gpg,sig,DIGESTS' "${@}"
for file in "${to_digest[@]}"; do
df="${file}.DIGESTS"
if [[ ! -e "${df}" ]]; then
touch "${df}"
fbn=$(basename "${file}")
# TODO: modernize - drop md5 and sha1, add b2
for hash_type in md5 sha1 sha512; do
echo "# ${hash_type} HASH" | tr "a-z" "A-Z" >>"${df}"
output=$("${hash_type}sum" "${file}")
echo "${output%% *} ${fbn}" >>"${df}"
done
fi
if [[ -z "${signer}" ]]; then
continue
fi
af="${df}.asc"
if [[ ! -e "${af}" ]]; then
gpg --batch --local-user "${signer}" \
--output "${af}" \
--clearsign "${df}"
fi
done
}
# --
# Puts a filtered list of files from the passed files and directories
# in the passed variable. The filtering is done by ignoring files that
# end with the passed extensions. The extensions list should not
# contain the leading dot.
#
# Typical use:
# local all_files=()
# local ignored_extensions='sh,py,pl' # ignore the shell, python and perl scripts
# list_files all_files "${ignored_extensions}" "${directories_and_files[@]}"
#
# Parameters:
#
# 1 - name of an array variable where the filtered files will be stored
# 2 - comma-separated list of extensions that will be used for filtering files
# @ - files and directories to scan for files
function list_files() {
local files_variable_name="${1}"; shift
local ignored_extensions="${1}"; shift
# rest of the parameters are files or directories to list
local -n files="${files_variable_name}"
local file
local tmp_files
local pattern=''
if [[ -n "${ignored_extensions}" ]]; then
pattern='\.('"${ignored_extensions//,/|}"')$'
fi
files=()
for file; do
tmp_files=()
if [[ -d "${file}" ]]; then
readarray -d '' tmp_files < <(find "${file}" ! -type d -print0)
elif [[ -e "${file}" ]]; then
tmp_files+=( "${file}" )
fi
if [[ -z "${pattern}" ]]; then
files+=( "${tmp_files[@]}" )
continue
fi
for file in "${tmp_files[@]}"; do
if [[ "${file}" =~ ${pattern} ]]; then
continue
fi
files+=( "${file}" )
done
done
}
# --
# Applies ../scripts.patch to the current repo.
function apply_local_patches() {
local patch_file="../scripts.patch"
local patch_id
echo "Looking for local patches ${patch_file}"
patch_id=$(test -e "${patch_file}" && { cat "${patch_file}" | git patch-id | cut -d ' ' -f 1 ; } || true)
if [ "${patch_id}" != "" ]; then
if git "${dirarg[@]}" log --no-merges -p HEAD | git patch-id | cut -d ' ' -f 1 | grep -q "${patch_id}"; then
echo "Skipping already applied ${patch_file}"
else
echo "Applying ${patch_file}"
GIT_COMMITTER_NAME="Flatcar Buildbot" GIT_COMMITTER_EMAIL="buildbot@flatcar-linux.org" git am -3 "$PWD/${patch_file}"
fi
fi
}
# --
# Returns 0 if passed value is either "1" or "true" or "t", otherwise
# returns 1.
function bool_is_true() {
case "${1}" in
1|true|t|yes|y)
return 0
;;
*)
return 1
;;
esac
}
# --