#!/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 ubuntu_print_help() {
cat <<HELP
$(basename "${BASH_SOURCE[0]}"): Update the Ubuntu image location in the specified templates
Usage:
$(basename "${BASH_SOURCE[0]}") [--flavor <flavor>|--minimal|--server] [--version <version>] <template.yaml>...
Description:
This script updates the Ubuntu 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 flavor and version from the image location basename in the template.
Image location basename format: ubuntu-<version>-<flavor>-cloudimg-<arch>.img
Released Ubuntu image information is fetched from the following URLs:
Server: https://cloud-images.ubuntu.com/releases/stream/v1/com.ubuntu.cloud:released:download.json
Minimal: https://cloud-images.ubuntu.com/minimal/releases/stream/v1/com.ubuntu.cloud:released:download.json
The downloaded JSON file will be cached in the Lima cache directory.
Examples:
Update the Ubuntu image location in templates/**.yaml:
$ $(basename "${BASH_SOURCE[0]}") templates/**.yaml
Update the Ubuntu image location in ~/.lima/ubuntu/lima.yaml:
$ $(basename "${BASH_SOURCE[0]}") ~/.lima/ubuntu/lima.yaml
$ limactl factory-reset ubuntu
Update the Ubuntu image location to ubuntu-24.04-minimal-cloudimg-<arch>.img in ~/.lima/docker/lima.yaml:
$ $(basename "${BASH_SOURCE[0]}") --minimal --version 24.04 ~/.lima/docker/lima.yaml
$ limactl factory-reset docker
Flags:
--flavor <flavor> Use the specified flavor image
--server Shortcut for --flavor server
--minimal Shortcut for --flavor minimal
--version <version> Use the specified version
The version can be an alias: latest, latest_lts, or lts.
-h, --help Print this help message
HELP
}
readonly -A ubuntu_base_urls=(
[minimal]=https://cloud-images.ubuntu.com/minimal/releases/
[server]=https://cloud-images.ubuntu.com/releases/
)
function ubuntu_base_url() {
[[ -v ubuntu_base_urls[$1] ]] || error_exit "Unsupported flavor: $1"
echo "${ubuntu_base_urls[$1]}"
}
function ubuntu_downloaded_json() {
local flavor=$1 base_url json_url
json_url=$(ubuntu_base_url "${flavor}")streams/v1/com.ubuntu.cloud:released:download.json
download_to_cache "${json_url}"
}
function ubuntu_image_url_try_replace_release_with_version() {
local location=$1 release=$2 version=$3 location_using_version
set +e
location_using_version=$(
set -e
validate_url "${location/\/${release}\//\/${version}\/}" 2>/dev/null
)
if [[ $? -eq 0 ]]; then
echo "${location_using_version}"
else
echo "${location}"
fi
set -e
}
function ubuntu_image_url_latest() {
local flavor=$1 version=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json jq_filter location_digest_release
base_url=$(ubuntu_base_url "${flavor}")
ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}")
jq_filter="
[
.products[\"com.ubuntu.cloud:${flavor}:${version}:${arch}\"] |
.release as \$release |
.versions[]?.items[] | select(.path | endswith(\"${path_suffix}\")) |
[\"${base_url}\"+.path, \"sha256:\"+.sha256, \$release] | @tsv
] | last
"
location_digest_release=$(jq -r "${jq_filter}" "${ubuntu_downloaded_json}")
[[ ${location_digest_release} != "null" ]] ||
error_exit "The URL for ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}."
local location digest release location_using_version
read -r location digest release <<<"${location_digest_release}"
location=$(validate_url "${location}")
location=$(ubuntu_image_url_try_replace_release_with_version "${location}" "${release}" "${version}")
arch=$(limayaml_arch "${arch}")
json_vars location arch digest
}
function ubuntu_image_url_release() {
local flavor=$1 version=$2 arch=$3 path_suffix=$4 base_url
base_url=$(ubuntu_base_url "${flavor}")
ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}")
local jq_filter release location
jq_filter="
[
.products | to_entries[] as \$product_entry |
\$product_entry.value| select(.version == \"${version}\") |
.release
] | first
"
release=$(jq -r "${jq_filter}" "${ubuntu_downloaded_json}")
[[ ${release} != "null" ]] ||
error_exit "The URL for ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}."
location=$(validate_url "${base_url}${release}/release/ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix}")
location=$(ubuntu_image_url_try_replace_release_with_version "${location}" "${release}" "${version}")
arch=$(limayaml_arch "${arch}")
json_vars location arch
}
function ubuntu_file_info() {
local location=$1 location_dirname sha256sums location_basename digest
location=$(validate_url "${location}")
location_dirname=$(dirname "${location}")
sha256sums=$(download_to_cache "${location_dirname}/SHA256SUMS")
location_basename=$(basename "${location}")
digest=${location+$(awk "/${location_basename}/{print \"sha256:\"\$1}" "${sha256sums}")}
json_vars location digest
}
function ubuntu_image_entry_with_kernel_info() {
local image_entry=$1 location
location=$(jq -e -r '.location' <<<"${image_entry}")
local location_dirname location_basename location_prefix
location_dirname=$(dirname "${location}")/unpacked
location_basename="$(basename "${location}" | cut -d- -f1-5 | cut -d. -f1-2)"
location_prefix="${location_dirname}/${location_basename}"
local kernel initrd
set +e
kernel=$(
set -e
ubuntu_file_info "${location_prefix}-vmlinuz-generic"
)
[[ $? -eq 0 ]] || error_exit "kernel image not found at ${location_prefix}-vmlinuz-generic"
initrd=$(
set -e
ubuntu_file_info "${location_prefix}-initrd-generic" 2>/dev/null
)
set -e
json_vars kernel initrd <<<"${image_entry}"
}
function ubuntu_flavor_from_location_basename() {
local location=$1 location_basename flavor
location_basename=$(basename "${location}")
flavor=$(echo "${location_basename}" | cut -d- -f3)
[[ -n ${flavor} ]] || error_exit "Failed to get flavor from ${location}"
echo "${flavor}"
}
function ubuntu_version_from_location_basename() {
local location=$1 location_basename version
location_basename=$(basename "${location}")
version=$(echo "${location_basename}" | cut -d- -f2)
[[ -n ${version} ]] || error_exit "Failed to get version from ${location}"
echo "${version}"
}
function ubuntu_version_latest_lts() {
local flavor=${1:-server}
ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}")
jq -e -r '[.products[]|.version|select(endswith(".04"))]|last // empty' "${ubuntu_downloaded_json}"
}
function ubuntu_version_latest() {
local flavor=${1:-server}
ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}")
jq -e -r '[.products[]|.version]|last // empty' "${ubuntu_downloaded_json}"
}
function ubuntu_version_resolve_aliases() {
local location=$1 flavor version
flavor=${2:-$(ubuntu_flavor_from_location_basename "${location}")}
version=${3:-}
case "${version}" in
latest_lts | lts) ubuntu_version_latest_lts "${flavor}" ;;
latest) ubuntu_version_latest "${flavor}" ;;
*) echo "${version}" ;;
esac
}
function ubuntu_arch_from_location_basename() {
local location=$1 location_basename arch
location_basename=$(basename "${location}")
arch=$(echo "${location_basename}" | cut -d- -f5 | cut -d. -f1)
[[ -n ${arch} ]] || error_exit "Failed to get arch from ${location}"
echo "${arch}"
}
function ubuntu_path_suffix_from_location_basename() {
local location=$1 arch path_suffix
arch=$(ubuntu_arch_from_location_basename "${location}")
path_suffix="${location##*"${arch}"}"
[[ -n ${path_suffix} ]] || error_exit "Failed to get path suffix from ${location}"
echo "${path_suffix}"
}
function ubuntu_location_url_spec() {
local location=$1 url_spec
case "${location}" in
https://cloud-images.ubuntu.com/minimal/releases/*/release/*) url_spec=release ;;
https://cloud-images.ubuntu.com/minimal/releases/*/release-*/*) url_spec=latest ;;
https://cloud-images.ubuntu.com/releases/*/release/*) url_spec=release ;;
https://cloud-images.ubuntu.com/releases/*/release-*/*) url_spec=latest ;;
*)
return 1
;;
esac
echo "${url_spec}"
}
function ubuntu_cache_key_for_image_kernel_flavor_version() {
local location=$1 kernel_location=${2:-null} url_spec with_kernel='' flavor version arch path_suffix
url_spec=$(ubuntu_location_url_spec "${location}")
[[ ${kernel_location} != "null" ]] && with_kernel=_with_kernel
flavor=$(ubuntu_flavor_from_location_basename "${location}")
version=$(ubuntu_version_from_location_basename "${location}")
arch=$(ubuntu_arch_from_location_basename "${location}")
path_suffix=$(ubuntu_path_suffix_from_location_basename "${location}")
echo "ubuntu_${url_spec}${with_kernel}_${version}-${flavor}-${arch}-${path_suffix}"
}
function ubuntu_image_entry_for_image_kernel_flavor_version() {
local location=$1 kernel_location=$2 url_spec
url_spec=$(ubuntu_location_url_spec "${location}")
local flavor version arch path_suffix
flavor=${3:-$(ubuntu_flavor_from_location_basename "${location}")}
version=${4:-$(ubuntu_version_from_location_basename "${location}")}
arch=$(ubuntu_arch_from_location_basename "${location}")
path_suffix=$(ubuntu_path_suffix_from_location_basename "${location}")
local image_entry
image_entry=$(ubuntu_image_url_"${url_spec}" "${flavor}" "${version}" "${arch}" "${path_suffix}")
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
elif [[ ${kernel_location} != "null" ]]; then
ubuntu_image_entry_with_kernel_info "${image_entry}"
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+=("ubuntu")
else
declare -a SUPPORTED_DISTRIBUTIONS=("ubuntu")
fi
function ubuntu_cache_key_for_image_kernel() { ubuntu_cache_key_for_image_kernel_flavor_version "$@"; }
function ubuntu_image_entry_for_image_kernel() { ubuntu_image_entry_for_image_kernel_flavor_version "$@"; }
return 0
fi
declare -a templates=()
declare overriding_flavor=
declare overriding_version=
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
ubuntu_print_help
exit 0
;;
-d | --debug) set -x ;;
--flavor)
if [[ -n $2 && $2 != -* ]]; then
overriding_flavor="$2"
shift
else
error_exit "--flavor requires a value"
fi
;;
--flavor=*) overriding_flavor="${1#*=}" ;;
--minimal) overriding_flavor="minimal" ;;
--server) overriding_flavor="server" ;;
--version)
if [[ -n $2 && $2 != -* ]]; then
overriding_version="$2"
shift
else
error_exit "--version requires a value"
fi
;;
--version=*) overriding_version="${1#*=}" ;;
*.yaml) templates+=("$1") ;;
*)
error_exit "Unknown argument: $1"
;;
esac
shift
done
if [[ ${#templates[@]} -eq 0 ]]; then
ubuntu_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
overriding_version=$(
set -e # Enable 'set -e' for the next command.
ubuntu_version_resolve_aliases "${location}" "${overriding_flavor}" "${overriding_version}"
)
[[ $? -eq 0 ]] || continue
cache_key=$(
set -e # Enable 'set -e' for the next command.
ubuntu_cache_key_for_image_kernel_flavor_version "${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
ubuntu_image_entry_for_image_kernel_flavor_version "${location}" "${kernel_location}" "${overriding_flavor}" "${overriding_version}"
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