#!/usr/bin/env bash
set -eu -o pipefail
shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later."
function debian_print_help() {
cat <<HELP
$(basename "${BASH_SOURCE[0]}"): Update the Debian image location in the specified templates
Usage:
$(basename "${BASH_SOURCE[0]}") [--backports[=<bool>]] [--daily[=<bool>]] [--timestamped[=<bool>]] [--version <version>] <template.yaml>...
Description:
This script updates the Debian image location in the specified templates.
If the image location in the template contains a release date in the URL, the script replaces it with the latest available date.
If no flags are specified, the script uses the version from the image location basename in the template.
Image location basename format: debian-<version>[-backports]-genericcloud-<arch>[-daily][-<timestamp>].qcow2
Published Debian image information is fetched from the following URLs:
https://cloud.debian.org/images/cloud/<codename>[-backports]/[daily/](latest|<timestamp>)/debian-<version>[-backports]-genericcloud-<arch>[-daily][-<timestamp>].json
The downloaded JSON file will be cached in the Lima cache directory.
Examples:
Update the Debian image location in templates/**.yaml:
$ $(basename "${BASH_SOURCE[0]}") templates/**.yaml
Update the Debian image location in ~/.lima/debian/lima.yaml:
$ $(basename "${BASH_SOURCE[0]}") ~/.lima/debian/lima.yaml
$ limactl factory-reset debian
Update the Debian image location to debian-13-genericcloud-<arch>.qcow2 in ~/.lima/debian/lima.yaml:
$ $(basename "${BASH_SOURCE[0]}") --version trixie ~/.lima/debian/lima.yaml
$ limactl factory-reset debian
Flags:
--backports[=<bool>] Use the backports image
The boolean value can be true, false, 1, or 0
--daily[=<bool>] Use the daily image
--timestamped[=<bool>] Use the timestamped image
--version <version> Use the specified version
The version can be a codename, version number, or alias (testing, stable, oldstable)
-h, --help Print this help message
HELP
}
readonly debian_base_url=https://cloud.debian.org/images/cloud/
readonly debian_target_vendor=genericcloud
readonly -A debian_version_to_codename=(
[10]=buster
[11]=bullseye
[12]=bookworm
[13]=trixie
[14]=forky
)
declare -A debian_codename_to_version
function debian_setup_codename_to_version() {
local version codename
for version in "${!debian_version_to_codename[@]}"; do
codename=${debian_version_to_codename[${version}]}
debian_codename_to_version[${codename}]="${version}"
done
readonly -A debian_codename_to_version
}
debian_setup_codename_to_version
readonly -A debian_alias_to_codename=(
[testing]=trixie
[stable]=bookworm
[oldstable]=bullseye
)
function debian_downloaded_json() {
local url_spec=$1 json_url_spec json_url
json_url_spec=$(jq -r '. | del(.timestamp) | .file_extension = "json"' <<<"${url_spec}") || error_exit "Failed to create JSON URL spec"
json_url=$(debian_location_from_url_spec "${json_url_spec}")
download_to_cache "${json_url}"
}
function debian_digest_from_upload_entry() {
local upload_entry=$1 debian_digest digest
debian_digest=$(jq -e -r '.metadata.annotations."cloud.debian.org/digest"' <<<"${upload_entry}") ||
error_exit "Failed to get the digest from ${upload_entry}"
case "${debian_digest%:*}" in
sha512) digest=$(echo "${debian_digest#*:}==" | base64 -d | xxd -p -c -) ||
error_exit "Failed to decode the digest from ${debian_digest}" ;;
*) error_exit "Unsupported digest type: ${debian_digest%:*}" ;;
esac
echo "${debian_digest/:*/:}${digest}"
}
function debian_image_url_timestamped() {
local url_spec=$1 debian_downloaded_json jq_filter upload_entry timestamp timestamped_url_spec location arch digest
debian_downloaded_json=$(debian_downloaded_json "${url_spec}")
jq_filter='
[.items[]|select(.kind == "Upload")|
select(.metadata.labels."upload.cloud.debian.org/image-format" == $ARGS.named.url_spec.image_format)]|first
'
upload_entry=$(jq -e -r --argjson url_spec "${url_spec}" "${jq_filter}" "${debian_downloaded_json}") ||
error_exit "Failed to find the upload entry from ${debian_downloaded_json}"
timestamp=$(jq -e -r '.metadata.labels."cloud.debian.org/version"' <<<"${upload_entry}") ||
error_exit "Failed to get the timestamp from ${upload_entry}"
timestamped_url_spec=$(json_vars timestamp <<<"${url_spec}")
location=$(debian_location_from_url_spec "${timestamped_url_spec}")
location=$(validate_url_without_redirect "${location}")
arch=$(jq -e -r '.arch' <<<"${url_spec}") || error_exit "missing arch in ${url_spec}"
arch=$(limayaml_arch "${arch}")
digest=$(debian_digest_from_upload_entry "${upload_entry}")
json_vars location arch digest
}
function debian_image_url_not_timestamped() {
local url_spec=$1 location arch
location=$(debian_location_from_url_spec "${url_spec}")
location=$(validate_url_without_redirect "${location}")
arch=$(jq -e -r '.arch' <<<"${url_spec}") || error_exit "missing arch in ${url_spec}"
arch=$(limayaml_arch "${arch}")
json_vars location arch
}
function debian_version_resolve_aliases() {
local version=$1
[[ -v debian_alias_to_codename[${version}] ]] && version=${debian_alias_to_codename[${version}]}
[[ -v debian_codename_to_version[${version}] ]] && version=${debian_codename_to_version[${version}]}
[[ -v debian_version_to_codename[${version}] ]] || error_exit "Unsupported version: ${version}"
[[ -z ${version} ]] || echo "${version}"
}
function debian_arch_from_location_basename() {
local location=$1 location_basename arch
location_basename=$(basename "${location}")
location_basename=${location_basename/-backports/}
arch=$(echo "${location_basename}" | cut -d- -f4 | cut -d. -f1)
[[ -n ${arch} ]] || error_exit "Failed to get arch from ${location}"
echo "${arch}"
}
function debian_file_extension_from_location_basename() {
local location=$1 location_basename file_extension
location_basename=$(basename "${location}")
file_extension=$(echo "${location_basename}" | cut -d. -f2-)
[[ -n ${file_extension} ]] || error_exit "Failed to get file extension from ${location}"
echo "${file_extension}"
}
function debian_image_format_from_file_extension() {
local file_extension=$1
case "${file_extension}" in
json) echo "json" ;;
qcow2) echo "qcow2" ;;
raw) echo "raw" ;;
tar.xz) echo "internal" ;;
*) error_exit "Unsupported file extension: ${file_extension}" ;;
esac
}
function debian_url_spec_from_location() {
local location=$1 backports=false daily=false timestamp='' codename version='' arch file_extension image_format
local -r timestamp_pattern='[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]'
case "${location}" in
${debian_base_url}*-backports/*) backports=true ;;&
${debian_base_url}*/daily/*) daily=true ;;&
${debian_base_url}*/${timestamp_pattern}/*) [[ ${location} =~ ${timestamp_pattern} ]] && timestamp=${BASH_REMATCH[0]} ;;
${debian_base_url}*/latest/*) timestamp='' ;;
*)
return 1
;;
esac
codename=$(echo "${location#"${debian_base_url}"}" | cut -d/ -f1 | cut -d- -f1)
[[ -v debian_codename_to_version[${codename}] ]] || error_exit "Unknown codename: ${codename}"
version=${debian_codename_to_version[${codename}]}
arch=$(debian_arch_from_location_basename "${location}")
file_extension=$(debian_file_extension_from_location_basename "${location}")
image_format=$(debian_image_format_from_file_extension "${file_extension}")
json_vars backports daily timestamp version arch file_extension image_format
}
function debian_location_from_url_spec() {
local url_spec=$1 base_url version backports daily timestamp arch file_extension
base_url=${debian_base_url}
version=$(jq -e -r '.version' <<<"${url_spec}")
[[ -v debian_version_to_codename[${version}] ]] || error_exit "Unsupported version: ${version}"
base_url+=${debian_version_to_codename[${version}]}
backports=$(jq -r 'if .backports then "-backports" else empty end' <<<"${url_spec}")
base_url+=${backports}/
daily=$(jq -r 'if .daily then "daily" else empty end' <<<"${url_spec}")
base_url+=${daily:+${daily}/}
timestamp=$(jq -r 'if .timestamp then .timestamp else empty end' <<<"${url_spec}")
base_url+=${timestamp:-latest}/
arch=$(jq -e -r '.arch' <<<"${url_spec}")
file_extension=$(jq -e -r '.file_extension' <<<"${url_spec}")
base_url+=debian-${version}${backports}-${debian_target_vendor}-${arch}${daily:+-${daily}}${timestamp:+-${timestamp}}.${file_extension}
echo "${base_url}"
}
function debian_cache_key_for_image_kernel_overriding() {
local location=$1 kernel_location=${2:-null} url_spec with_kernel='' version backports arch daily timestamped file_extension
url_spec=$(debian_url_spec_from_location "${location}")
[[ ${kernel_location} != "null" ]] && with_kernel=_with_kernel
version=$(jq -r '.version|if . then "-\(.)" else empty end' <<<"${url_spec}")
backports=$(jq -r 'if .backports then "-backports" else empty end' <<<"${url_spec}")
arch=$(jq -e -r '.arch' <<<"${url_spec}")
daily=$(jq -r 'if .daily then "-daily" else empty end' <<<"${url_spec}")
timestamped=$(jq -r 'if .timestamp then "-timestamped" else empty end' <<<"${url_spec}")
file_extension=$(jq -e -r '.file_extension' <<<"${url_spec}")
echo "debian${with_kernel}${version}${backports}-${debian_target_vendor}-${arch}${daily}${timestamped}.${file_extension}"
}
function debian_image_entry_for_image_kernel_overriding() {
local location=$1 kernel_location=$2 overriding=${3:-"{}"} url_spec timestamped
[[ ${kernel_location} == "null" ]] || error_exit "Updating image with kernel is not supported"
url_spec=$(debian_url_spec_from_location "${location}" | jq -r ". + ${overriding}")
timestamped=$(jq -r 'if .timestamp then "timestamped" else "not_timestamped" end' <<<"${url_spec}")
local image_entry
image_entry=$(debian_image_url_"${timestamped}" "${url_spec}")
if [[ -z ${image_entry} ]]; then
error_exit "Failed to get the ${url_spec} image location for ${location}"
elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then
echo "Image location is up-to-date: ${location}" >&2
else
echo "${image_entry}"
fi
}
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
scriptdir=$(dirname "${BASH_SOURCE[0]}")
. "${scriptdir}/cache-common-inc.sh"
. "${scriptdir}/update-template.sh"
else
if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then
SUPPORTED_DISTRIBUTIONS+=("debian")
else
declare -a SUPPORTED_DISTRIBUTIONS=("debian")
fi
function debian_cache_key_for_image_kernel() { debian_cache_key_for_image_kernel_overriding "$@"; }
function debian_image_entry_for_image_kernel() { debian_image_entry_for_image_kernel_overriding "$@"; }
return 0
fi
declare -a templates=()
declare overriding="{}"
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
debian_print_help
exit 0
;;
-d | --debug) set -x ;;
--backports | --daily | --timestamped)
overriding=$(json "${1#--}" true <<<"${overriding}")
;;
--backports=* | --daily=* | --timestamped=*)
overriding=$(
key=${1#--} value=$(validate_boolean "${1#*=}")
json "${key%%=*}" "${value}" <<<"${overriding}"
)
;;
--version)
if [[ -n $2 && $2 != -* ]]; then
overriding=$(
version=$(debian_version_resolve_aliases "$2")
json_vars version <<<"${overriding}"
)
shift
else
error_exit "--version requires a value"
fi
;;
--version=*)
overriding=$(
version=$(debian_version_resolve_aliases "${1#*=}")
json_vars version <<<"${overriding}"
)
;;
*.yaml) templates+=("$1") ;;
*)
error_exit "Unknown argument: $1"
;;
esac
shift
[[ -z ${overriding} ]] && overriding="{}"
done
if [[ ${#templates[@]} -eq 0 ]]; then
debian_print_help
exit 0
fi
declare -A image_entry_cache=()
for template in "${templates[@]}"; do
echo "Processing ${template}"
yq_filter="
.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv
"
parsed=$(yq eval "${yq_filter}" "${template}")
arr=()
while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}"
locations=("${arr[@]}")
for ((index = 0; index < ${#locations[@]}; index++)); do
[[ ${locations[index]} != "null" ]] || continue
set -e
IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}"
set +e
cache_key=$(
set -e # Enable 'set -e' for the next command.
debian_cache_key_for_image_kernel_overriding "${location}" "${kernel_location}"
)
[[ $? -eq 0 ]] || continue
image_entry=$(
set -e # Enable 'set -e' for the next command.
if [[ -v image_entry_cache[${cache_key}] ]]; then
echo "${image_entry_cache[${cache_key}]}"
else
debian_image_entry_for_image_kernel_overriding "${location}" "${kernel_location}" "${overriding}"
fi
)
[[ $? -eq 0 ]] || continue
set -e
image_entry_cache[${cache_key}]="${image_entry}"
if [[ -n ${image_entry} ]]; then
[[ ${kernel_cmdline} != "null" ]] &&
jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null &&
image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}")
echo "${image_entry}" | jq
limactl edit --log-level error --set "
.images[${index}] = ${image_entry}|
(.images[${index}] | ..) style = \"double\"
" "${template}"
fi
done
done