Build containers / Compute changes (push) Successful in 3s
Build containers / fedora-cosmic (rawhide) (push) Failing after 59s
Build containers / fedora-remote (44) (push) Successful in 11m18s
Build containers / fedora-cosmic (44) (push) Successful in 13m39s
Build containers / Prune old releases and tags (push) Failing after 25s
kernel-modules-extra is declared in the upstream top-level common.yaml, not under packages/, so the headless fedora-remote image still requested it while also listing it in exclude-packages, which made compose fail with 'Packages not found: kernel-modules-extra'. Scan top-level manifests as well, and drop keys whose block is left empty (comments/blanks do not count).
290 lines
9.7 KiB
Bash
Executable File
290 lines
9.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build a bootc OCI image from an image definition under images/<image>.
|
|
#
|
|
# Usage: build.sh <image> <distro> [arch]
|
|
#
|
|
# The per-image build.conf selects the build mode:
|
|
#
|
|
# upstream Clone the upstream Fedora manifest repo at a ref mapped from
|
|
# <distro>, overlay this image's manifests and support files, then
|
|
# compose the OCI image with `rpm-ostree compose image`.
|
|
#
|
|
# standalone Compose from this image's own manifest.yaml, wrapping it with the
|
|
# releasever/ref from build.conf.
|
|
set -euo pipefail
|
|
|
|
if [[ $# -lt 2 ]]; then
|
|
echo "Usage: $0 <image> <distro> [arch]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
IMAGE="$1"
|
|
DISTRO="$2"
|
|
ARCH="${3:-$(uname -m)}"
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
IMAGE_DIR="${REPO_ROOT}/images/${IMAGE}"
|
|
CONF="${IMAGE_DIR}/build.conf"
|
|
|
|
[[ -d "${IMAGE_DIR}" ]] || { echo "Unknown image: ${IMAGE}" >&2; exit 1; }
|
|
[[ -f "${CONF}" ]] || { echo "Missing build config: ${CONF}" >&2; exit 1; }
|
|
|
|
# Defaults, overridden by build.conf.
|
|
BUILD_MODE=""
|
|
UPSTREAM_REPO=""
|
|
TOKEN2=0
|
|
RUN_TEST_SH=0
|
|
RELEASEVER=""
|
|
REF=""
|
|
declare -A UPSTREAM_REFS=()
|
|
|
|
# shellcheck disable=SC1090
|
|
source "${CONF}"
|
|
|
|
BUILD_DIR="${REPO_ROOT}/build/${IMAGE}-${DISTRO}-${ARCH}"
|
|
OCI_ARCHIVE="${BUILD_DIR}/${IMAGE}-${DISTRO}-${ARCH}.ociarchive"
|
|
CACHE_DIR="${REPO_ROOT}/cache"
|
|
|
|
[[ -n "${BUILD_MODE}" ]] || { echo "BUILD_MODE not set in ${CONF}" >&2; exit 1; }
|
|
|
|
# Build IDs must be unique per build (release tags are derived from them), so
|
|
# include the CI run number when available, otherwise a timestamp.
|
|
if [[ -n "${GITHUB_RUN_NUMBER:-}" ]]; then
|
|
buildid="$(date '+%Y%m%d').${GITHUB_RUN_NUMBER}"
|
|
else
|
|
buildid="$(date '+%Y%m%d.%H%M%S')"
|
|
fi
|
|
echo "${buildid}" > "${REPO_ROOT}/.buildid"
|
|
|
|
rm -rf "${BUILD_DIR}"
|
|
mkdir -p "${BUILD_DIR}" "${CACHE_DIR}"
|
|
|
|
if [[ "${RUN_TEST_SH}" == "1" && -x "${IMAGE_DIR}/test.sh" ]]; then
|
|
echo "Running ${IMAGE_DIR}/test.sh"
|
|
( cd "${IMAGE_DIR}" && ./test.sh )
|
|
fi
|
|
|
|
# Download the latest token2-fido-bridge RPM from GitHub and turn it into a
|
|
# local yum repo so the manifest can install it.
|
|
setup_token2_repo() {
|
|
local dest="$1"
|
|
local api url
|
|
api="https://api.github.com/repos/token2/token2-fido-bridge/releases/latest"
|
|
url="$(curl -fsSL "${api}" \
|
|
| jq -r '.assets[] | select(.name | endswith(".rpm")) | .browser_download_url' \
|
|
| head -n1)"
|
|
if [[ -z "${url}" || "${url}" == "null" ]]; then
|
|
echo "ERROR: no token2-fido-bridge .rpm found in the latest GitHub release" >&2
|
|
exit 1
|
|
fi
|
|
mkdir -p "${dest}/token2-repo"
|
|
curl -fsSL "${url}" -o "${dest}/token2-repo/$(basename "${url}")"
|
|
createrepo_c "${dest}/token2-repo"
|
|
cat > "${dest}/token2-fido-bridge.repo" <<EOF
|
|
[token2-fido-bridge]
|
|
name=token2-fido-bridge
|
|
baseurl=file://${dest}/token2-repo
|
|
enabled=1
|
|
gpgcheck=0
|
|
EOF
|
|
}
|
|
|
|
# Drop packages listed in a blocklist from the cloned upstream manifests.
|
|
# rpm-ostree refuses (fatally) to exclude a package that an included manifest
|
|
# declares, so remove those lines at the source instead. Packages are declared
|
|
# both in packages/*.yaml and in the top-level variant manifests (e.g. the base
|
|
# kernel list lives in common.yaml), so scan both.
|
|
trim_upstream_packages() {
|
|
local root="$1"
|
|
local blocklist="$2"
|
|
local f
|
|
for f in "${root}"/*.yaml "${root}"/packages/*.yaml; do
|
|
[[ -f "${f}" ]] || continue
|
|
awk '
|
|
NR == FNR {
|
|
if ($0 ~ /^[[:space:]]*#/ || $0 ~ /^[[:space:]]*$/) next
|
|
skip[$0] = 1
|
|
next
|
|
}
|
|
{
|
|
line = $0
|
|
sub(/^[[:space:]]*-[[:space:]]*/, "", line)
|
|
if (line in skip) next
|
|
print
|
|
}
|
|
' "${blocklist}" "${f}" > "${f}.tmp"
|
|
# Drop top-level keys left with no list items or nested keys (e.g. if every
|
|
# entry under packages-x86_64: was removed). Comments and blank lines do not
|
|
# count as content, so a key followed only by comments is still dropped.
|
|
awk '
|
|
{ lines[NR] = $0 }
|
|
END {
|
|
for (i = 1; i <= NR; i++) {
|
|
if (lines[i] ~ /^[A-Za-z0-9_.-]+:$/) {
|
|
keep = 0
|
|
for (j = i + 1; j <= NR; j++) {
|
|
if (lines[j] ~ /^[A-Za-z0-9_.-]+:/) break
|
|
if (lines[j] ~ /^[[:space:]]+-[[:space:]]/) { keep = 1; break }
|
|
if (lines[j] ~ /^[[:space:]]+[A-Za-z0-9_.-]+:/) { keep = 1; break }
|
|
}
|
|
if (!keep) continue
|
|
}
|
|
print lines[i]
|
|
}
|
|
}
|
|
' "${f}.tmp" > "${f}.tmp2"
|
|
mv "${f}.tmp2" "${f}"
|
|
rm -f "${f}.tmp"
|
|
done
|
|
}
|
|
|
|
# Generate an exclude-packages snippet from the same blocklist. This blocks the
|
|
# removed packages from being pulled back in as recommends (e.g. linux-firmware
|
|
# recommends the split firmware packages).
|
|
generate_exclude_yaml() {
|
|
local blocklist="$1"
|
|
local out="$2"
|
|
{
|
|
echo "# Generated by build.sh from $(basename "${blocklist}") - do not edit."
|
|
echo "exclude-packages:"
|
|
grep -vE '^[[:space:]]*(#|$)' "${blocklist}" | sed 's/^/ - /'
|
|
} > "${out}"
|
|
}
|
|
|
|
# Seed the Kanidm unixd service account token from the KANIDM_UNIXD_TOKEN
|
|
# environment variable (a CI secret). The token is added to the image, so treat
|
|
# the image as sensitive: anyone who can pull it can read the token.
|
|
seed_kanidm_token() {
|
|
local dir="$1"
|
|
local out="${dir}/kanidm-token.yaml"
|
|
if [[ -n "${KANIDM_UNIXD_TOKEN:-}" ]]; then
|
|
printf '%s\n' "${KANIDM_UNIXD_TOKEN}" > "${dir}/kanidm-unixd-token"
|
|
chmod 0600 "${dir}/kanidm-unixd-token"
|
|
# kanidm-unixd is DynamicUser=yes, so it cannot read the root-only token
|
|
# file directly; hand it over as a systemd credential instead.
|
|
cat > "${dir}/kanidm-unixd-token.conf" <<'EOF'
|
|
[Service]
|
|
LoadCredential=unixd_token:/etc/kanidm/unixd_token
|
|
Environment=KANIDM_SERVICE_ACCOUNT_TOKEN_PATH=%d/unixd_token
|
|
EOF
|
|
cat > "${out}" <<'EOF'
|
|
# Generated by build.sh from the KANIDM_UNIXD_TOKEN secret.
|
|
add-files:
|
|
- ["kanidm-unixd-token", "/etc/kanidm/unixd_token"]
|
|
- ["kanidm-unixd-token.conf", "/usr/lib/systemd/system/kanidm-unixd.service.d/10-token.conf"]
|
|
EOF
|
|
else
|
|
cat > "${out}" <<'EOF'
|
|
# Generated by build.sh: no Kanidm token seeded (KANIDM_UNIXD_TOKEN unset).
|
|
add-files: []
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
case "${BUILD_MODE}" in
|
|
upstream)
|
|
[[ -n "${UPSTREAM_REPO}" ]] || { echo "UPSTREAM_REPO not set in ${CONF}" >&2; exit 1; }
|
|
upstream_ref="${UPSTREAM_REFS[${DISTRO}]:-}"
|
|
[[ -n "${upstream_ref}" ]] || { echo "No upstream ref configured for distro '${DISTRO}'" >&2; exit 1; }
|
|
|
|
git clone --depth 1 --branch "${upstream_ref}" "${UPSTREAM_REPO}" "${BUILD_DIR}/upstream"
|
|
|
|
# Overlay our manifests/support files into the upstream tree so that
|
|
# relative includes and *.repo discovery keep working.
|
|
cp "${IMAGE_DIR}/manifest.yaml" "${BUILD_DIR}/upstream/"
|
|
cp "${IMAGE_DIR}/custom.yaml" "${BUILD_DIR}/upstream/"
|
|
if compgen -G "${IMAGE_DIR}/repos/*.repo" >/dev/null; then
|
|
cp "${IMAGE_DIR}"/repos/*.repo "${BUILD_DIR}/upstream/"
|
|
fi
|
|
if compgen -G "${IMAGE_DIR}/repos/${DISTRO}/*.repo" >/dev/null; then
|
|
cp "${IMAGE_DIR}"/repos/"${DISTRO}"/*.repo "${BUILD_DIR}/upstream/"
|
|
fi
|
|
if [[ -d "${IMAGE_DIR}/files" ]]; then
|
|
cp -a "${IMAGE_DIR}/files/." "${BUILD_DIR}/upstream/"
|
|
fi
|
|
# Shared overlay (files/repos used by multiple images).
|
|
if [[ -d "${REPO_ROOT}/images/shared/files" ]]; then
|
|
cp -a "${REPO_ROOT}/images/shared/files/." "${BUILD_DIR}/upstream/"
|
|
fi
|
|
if compgen -G "${REPO_ROOT}/images/shared/repos/*.repo" >/dev/null; then
|
|
cp "${REPO_ROOT}"/images/shared/repos/*.repo "${BUILD_DIR}/upstream/"
|
|
fi
|
|
if compgen -G "${REPO_ROOT}/images/shared/repos/${DISTRO}/*.repo" >/dev/null; then
|
|
cp "${REPO_ROOT}"/images/shared/repos/"${DISTRO}"/*.repo "${BUILD_DIR}/upstream/"
|
|
fi
|
|
|
|
if [[ "${TOKEN2}" == "1" ]]; then
|
|
setup_token2_repo "${BUILD_DIR}/upstream"
|
|
fi
|
|
|
|
if [[ -f "${IMAGE_DIR}/upstream-exclude.txt" ]]; then
|
|
trim_upstream_packages "${BUILD_DIR}/upstream" "${IMAGE_DIR}/upstream-exclude.txt"
|
|
generate_exclude_yaml "${IMAGE_DIR}/upstream-exclude.txt" \
|
|
"${BUILD_DIR}/upstream/hardware-exclude.yaml"
|
|
fi
|
|
|
|
seed_kanidm_token "${BUILD_DIR}/upstream"
|
|
|
|
MANIFEST="${BUILD_DIR}/upstream/manifest.yaml"
|
|
;;
|
|
standalone)
|
|
[[ -n "${RELEASEVER}" ]] || { echo "RELEASEVER not set in ${CONF}" >&2; exit 1; }
|
|
[[ -n "${REF}" ]] || { echo "REF not set in ${CONF}" >&2; exit 1; }
|
|
|
|
# Build from a copy so we never mutate the checked-in image definition.
|
|
mkdir -p "${BUILD_DIR}/images"
|
|
cp -a "${IMAGE_DIR}" "${BUILD_DIR}/images/${IMAGE}"
|
|
|
|
local_dir="${BUILD_DIR}/images/${IMAGE}"
|
|
if compgen -G "${local_dir}/repos/*.repo" >/dev/null; then
|
|
cp "${local_dir}"/repos/*.repo "${local_dir}/"
|
|
fi
|
|
if compgen -G "${local_dir}/repos/${DISTRO}/*.repo" >/dev/null; then
|
|
cp "${local_dir}"/repos/"${DISTRO}"/*.repo "${local_dir}/"
|
|
fi
|
|
if [[ -d "${REPO_ROOT}/images/shared/files" ]]; then
|
|
cp -a "${REPO_ROOT}/images/shared/files/." "${local_dir}/"
|
|
fi
|
|
|
|
if [[ "${TOKEN2}" == "1" ]]; then
|
|
setup_token2_repo "${local_dir}"
|
|
fi
|
|
|
|
cat > "${local_dir}/.build-manifest.yaml" <<EOF
|
|
releasever: ${RELEASEVER}
|
|
ref: ${REF}
|
|
include:
|
|
- manifest.yaml
|
|
EOF
|
|
seed_kanidm_token "${local_dir}"
|
|
MANIFEST="${local_dir}/.build-manifest.yaml"
|
|
;;
|
|
*)
|
|
echo "Unknown BUILD_MODE: ${BUILD_MODE}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
echo "Composing ${IMAGE} ${DISTRO} (${ARCH}) build ${buildid} ..."
|
|
|
|
RPM_OSTREE=(rpm-ostree)
|
|
if [[ "${EUID}" -ne 0 ]]; then
|
|
RPM_OSTREE=(sudo -E rpm-ostree)
|
|
fi
|
|
|
|
"${RPM_OSTREE[@]}" compose image \
|
|
--cachedir="${CACHE_DIR}" \
|
|
--initialize \
|
|
--max-layers=256 \
|
|
"${MANIFEST}" \
|
|
"${OCI_ARCHIVE}"
|
|
|
|
# The finalize.d hook records the installed package list next to the treefile.
|
|
manifest_dir="$(dirname "${MANIFEST}")"
|
|
if [[ -f "${manifest_dir}/packages-current.txt" ]]; then
|
|
cp "${manifest_dir}/packages-current.txt" "${BUILD_DIR}/packages-current.txt"
|
|
echo "Package list: ${BUILD_DIR}/packages-current.txt"
|
|
fi
|
|
|
|
echo "Built: ${OCI_ARCHIVE}"
|