mirror of
https://github.com/flatcar/scripts.git
synced 2025-08-06 20:47:00 +02:00
This helps reading the scripts later. Signed-off-by: Mathieu Tortuyaux <mtortuyaux@microsoft.com>
507 lines
16 KiB
Bash
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
|
|
}
|
|
# --
|