Sfoglia il codice sorgente

Add alpine-make-rootfs script

Jakub Jirutka 7 anni fa
parent
commit
ba40e7158e
1 ha cambiato i file con 383 aggiunte e 0 eliminazioni
  1. 383 0
      alpine-make-rootfs

+ 383 - 0
alpine-make-rootfs

@@ -0,0 +1,383 @@
+#!/bin/sh
+# vim: set ts=4:
+#---help---
+# Usage: alpine-make-rootfs [options] [--] <dest> [<script> [<script-opts...>]]
+#
+# This script creates Alpine Linux rootfs for containers. It must be run as
+# root - to create files with correct permissions and use chroot (optional).
+# If $APK is not available on the host system, then static apk-tools
+# specified by $APK_TOOLS_URI is downloaded and used.
+#
+# Arguments:
+#   <dest>                                Path where to write the rootfs. It may be:
+#                                         - path with suffix .tar, .tar.bz2, .tbz, .tar.gz, .tgz,
+#                                           or tar.xz to create a TAR archive;
+#                                         - other path to keep rootfs as a directory;
+#                                         - or "-" to dump TAR archive (w/o compression) to STDOUT.
+#
+#   <script>                              Path of script to execute after installing base system in
+#                                         the prepared rootfs and before clean-up.
+#
+#   <script-opts>                         Arguments to pass to the script.
+#
+# Options and Environment Variables:
+#   -b --branch ALPINE_BRANCH             Alpine branch to install; used only when
+#                                         --repositories-file is not specified. Default is v3.7.
+#
+#      --keys-dir KEYS_DIR                Path of directory with Alpine keys to copy into
+#                                         the rootfs. Default is /etc/apk/keys. If does not exist,
+#                                         keys for x86_64 embedded in this script will be used.
+#
+#   -m --mirror-uri ALPINE_MIRROR         URI of the Aports mirror to fetch packages; used only
+#                                         when --repositories-file is not specified. Default is
+#                                         https://nl.alpinelinux.org/alpine.
+
+#   -C --no-cleanup (CLEANUP)             Don't umount and remove temporary directories when done.
+#
+#   -p --packages PACKAGES                Additional packages to install into the rootfs.
+#
+#   -r --repositories-file REPOS_FILE     Path of repositories file to copy into the rootfs.
+#                                         Default is /etc/apk/repositories. If does not exist,
+#                                         repositories file with Alpine's main and community
+#                                         repositories on --mirror-uri is created.
+#
+#   -c --script-chroot (SCRIPT_CHROOT)    Bind <script>'s directory at /mnt inside the rootfs dir
+#                                         and chroot into the rootfs before executing <script>.
+#
+#   -d --temp-dir TEMP_DIR                Path where to create a temporary directory; used for
+#                                         downloading apk-tools when not available on the host
+#                                         sytem or for rootfs when <dest> is "-" (i.e. STDOUT).
+#                                         This path must not exist! Defaults to using `mkdir -d`.
+#
+#   -t --timezone TIMEZONE                Timezone to set (e.g. Europe/Prague). Default is to leave
+#                                         timezone UTC.
+#
+#   -h --help                             Show this help message and exit.
+#
+#   -v --version                          Print version and exit.
+#
+#   APK                                   APK command to use. Default is "apk".
+#
+#   APK_OPTS                              Options to pass into apk on each execution.
+#                                         Default is "--no-progress".
+#
+#   APK_TOOLS_URI                         URL of static apk-tools tarball to download if $APK is
+#                                         not found on the host system. Default is x86_64 apk-tools
+#                                         from https://github.com/alpinelinux/apk-tools/releases.
+#
+#   APK_TOOLS_SHA256                      SHA-256 checksum of $APK_TOOLS_URI.
+#
+# Each option can be also provided by environment variable. If both option and
+# variable is specified and the option accepts only one argument, then the
+# option takes precedence.
+#
+# https://github.com/jirutka/alpine-make-rootfs
+#---help---
+set -eu
+
+readonly PROGNAME='alpine-make-rootfs'
+readonly VERSION='0.0.0'
+
+# Base Alpine packages to install in rootfs.
+readonly ALPINE_BASE_PKGS='alpine-baselayout busybox busybox-suid musl-utils'
+
+# Alpine APK keys for verification of packages for x86_64.
+readonly ALPINE_KEYS='
+alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe\nqxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O\nQ0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA\njixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R\nL5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo\nGuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B\nywIDAQAB
+alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlzMkl7b5PBdfMzGdCT0\ncGloRr5xGgVmsdq5EtJvFkFAiN8Ac9MCFy/vAFmS8/7ZaGOXoCDWbYVLTLOO2qtX\nyHRl+7fJVh2N6qrDDFPmdgCi8NaE+3rITWXGrrQ1spJ0B6HIzTDNEjRKnD4xyg4j\ng01FMcJTU6E+V2JBY45CKN9dWr1JDM/nei/Pf0byBJlMp/mSSfjodykmz4Oe13xB\nCa1WTwgFykKYthoLGYrmo+LKIGpMoeEbY1kuUe04UiDe47l6Oggwnl+8XD1MeRWY\nsWgj8sF4dTcSfCMavK4zHRFFQbGp/YFJ/Ww6U9lA3Vq0wyEI6MCMQnoSMFwrbgZw\nwwIDAQAB
+'
+# List of directories to remove when empty.
+readonly FUTILE_DIRS='
+	/home /media/cdrom /media/floppy /media/usb /mnt /srv /usr/local/bin
+	/usr/local/lib /usr/local/share
+'
+# Name used as a "virtual package" for temporarily installed packages.
+readonly VIRTUAL_PKG=".make-$PROGNAME"
+
+: ${APK:="apk"}
+: ${APK_OPTS:="--no-progress"}
+: ${APK_TOOLS_URI:="https://github.com/alpinelinux/apk-tools/releases/download/v2.9.1/apk-tools-2.9.1-x86_64-linux.tar.gz"}
+: ${APK_TOOLS_SHA256:="a0546d814a85fcc94a6e560360c4f1997119e40d79d9bc818f1571b2cf2ea5e9"}
+
+
+# Set pipefail if supported.
+if ( set -o pipefail 2>/dev/null ); then
+	set -o pipefail
+fi
+
+# For compatibility with systems that does not have "realpath" command.
+if ! command -v realpath >/dev/null; then
+	alias realpath='readlink -f'
+fi
+
+die() {
+	printf '\033[1;31mERROR:\033[0m %s\n' "$@" >&2  # bold red
+	exit 1
+}
+
+einfo() {
+	printf '\n\033[1;36m> %s\033[0m\n' "$@" >&2  # bold cyan
+}
+
+# Prints help and exists with the specified status.
+help() {
+	sed -En '/^#---help---/,/^#---help---/p' "$0" | sed -E 's/^# ?//; 1d;$d;'
+	exit ${1:-0}
+}
+
+# Cleans the host system. This function is executed before exiting the script.
+cleanup() {
+	set +eu
+	trap '' EXIT HUP INT TERM  # unset trap to avoid loop
+
+	if [ "$HOST_DISTRO" = alpine ]; then
+		_apk del $VIRTUAL_PKG >&2
+	fi
+	if [ -d "$TEMP_DIR" ]; then
+		rm -Rf "$TEMP_DIR"
+	fi
+	if [ -d "$rootfs" ]; then
+		umount_recursively "$rootfs" \
+			|| die "Failed to unmount mounts inside $rootfs!"
+		[ "$rootfs" = "$ROOTFS_DEST" ] || rm -Rf "$rootfs"
+	fi
+}
+
+_apk() {
+	"$APK" $APK_OPTS "$@"
+}
+
+# Writes Alpine APK keys embedded in this script into directory $1.
+dump_alpine_keys() {
+	local dest_dir="$1"
+	local content file line
+
+	mkdir -p "$dest_dir"
+	for line in $ALPINE_KEYS; do
+		file=${line%%:*}
+		content=${line#*:}
+
+		printf "-----BEGIN PUBLIC KEY-----\n$content\n-----END PUBLIC KEY-----\n" \
+			> "$dest_dir/$file"
+	done
+}
+
+# Prepares chroot at the specified path.
+prepare_chroot() {
+	local dest="$1"
+
+	mkdir -p "$dest"/proc "$dest"/dev "$dest"/sys
+	mount -t proc none "$dest"/proc
+	mount --bind /dev "$dest"/dev
+	mount --bind /sys "$dest"/sys
+
+	install -D -m 644 /etc/resolv.conf "$dest"/etc/resolv.conf
+}
+
+# Sets up timezone $1 in Alpine rootfs.
+setup_timezone() {
+	local timezone="$1"
+	local rootfs="${2:-}"
+
+	_apk add --root "$rootfs" tzdata
+
+	install -D "$rootfs"/usr/share/zoneinfo/$timezone \
+		"$rootfs"/etc/zoneinfo/$timezone
+	ln -sf zoneinfo/$timezone "$rootfs"/etc/localtime
+
+	_apk del --root "$rootfs" tzdata
+}
+
+# Unmounts all filesystems under the directory tree $1 (must be absolute path).
+umount_recursively() {
+	local mount_point=$(realpath "$1")
+
+	cat /proc/mounts \
+		| cut -d ' ' -f 2 \
+		| { grep "^$mount_point" || true; } \
+		| sort -r \
+		| xargs -r -n 1 umount
+}
+
+# Downloads the specified file using wget and checks checksum.
+wgets() (
+	local url="$1"
+	local sha256="$2"
+	local dest="${3:-.}"
+
+	cd "$dest" \
+		&& wget -T 10 --no-verbose "$url" \
+		&& echo "$sha256  ${url##*/}" | sha256sum -c
+)
+
+
+#=============================  M a i n  ==============================#
+
+opts=$(getopt -n $PROGNAME -o b:m:Cp:r:cd:t:hV \
+	-l branch:,keys-dir:,mirror-uri:,no-cleanup,packages:,repositories-file:,script-chroot,temp-dir:,timezone:,help,version \
+	-- "$@") || help 1 >&2
+
+eval set -- "$opts"
+while [ $# -gt 0 ]; do
+	n=2
+	case "$1" in
+		-b | --branch) ALPINE_BRANCH="$2";;
+		     --keys-dir) KEYS_DIR="$2";;
+		-m | --mirror-uri) ALPINE_MIRROR="$2";;
+		-C | --no-cleanup) CLEANUP='no'; n=1;;
+		-p | --packages) PACKAGES="${PACKAGES:-} $2";;
+		-r | --repositories-file) REPOS_FILE="$2";;
+		-c | --script-chroot) SCRIPT_CHROOT='yes'; n=1;;
+		-d | --temp-dir) TEMP_DIR="$2";;
+		-t | --timezone) TIMEZONE="$2";;
+		-h | --help) help 0;;
+		-V | --version) echo "$PROGNAME $VERSION"; exit 0;;
+		--) shift; break;;
+	esac
+	shift $n
+done
+
+[ $# -ne 0 ] || help 1 >&2
+
+ROOTFS_DEST="$1"; shift
+SCRIPT=
+[ $# -eq 0 ] || { SCRIPT=$(realpath "$1"); shift; }
+
+[ "$(id -u)" -eq 0 ] || die 'This script must be run as root!'
+
+[ ! -e "${TEMP_DIR:-}" ] || die "Temp path $TEMP_DIR must not exist!"
+
+: ${ALPINE_BRANCH:="v3.7"}
+: ${ALPINE_MIRROR:="https://nl.alpinelinux.org/alpine"}
+: ${CLEANUP:="yes"}
+: ${KEYS_DIR:="/etc/apk/keys"}
+: ${PACKAGES:=}
+: ${REPOS_FILE:="/etc/apk/repositories"}
+: ${SCRIPT_CHROOT:="no"}
+: ${TEMP_DIR:="$(mktemp -d /tmp/$PROGNAME.XXXXXX)"}
+: ${TIMEZONE:=}
+
+host_pkgs=''
+case "$ROOTFS_DEST" in
+	*.tar.bz2 | *.tbz) tar_opts='-cj';;
+	*.tar.gz | *.tgz) tar_opts='-cz';;
+	*.tar.xz) tar_opts='-cJ'; host_pkgs="$host_pkgs xz";;
+	*.tar | -) tar_opts='-c';;
+	*) tar_opts='';;
+esac
+
+rootfs="$ROOTFS_DEST"
+if [ "$ROOTFS_DEST" = '-' ]; then
+	rootfs="$TEMP_DIR/rootfs"
+elif [ "$tar_opts" ]; then
+	rootfs="${rootfs%.*}"
+	rootfs="${rootfs%.tar}"
+fi
+
+if [ -f /etc/os-release ]; then
+	: ${HOST_DISTRO:="$(. /etc/os-release && echo "$ID")"}
+else
+	: ${HOST_DISTRO:="unknown"}
+fi
+
+[ "$CLEANUP" = no ] || trap cleanup EXIT HUP INT TERM
+
+#-----------------------------------------------------------------------
+if [ "$HOST_DISTRO" = alpine ] && [ "$host_pkgs" ]; then
+	einfo "Installing $host_pkgs on host system"
+	_apk add -t $VIRTUAL_PKG $host_pkgs >&2
+fi
+
+#-----------------------------------------------------------------------
+if ! command -v "$APK" >/dev/null; then
+	einfo "$APK not found, downloading static apk-tools"
+
+	wgets "$APK_TOOLS_URI" "$APK_TOOLS_SHA256" "$TEMP_DIR"
+	tar -C "$TEMP_DIR" -xzf "$TEMP_DIR/${APK_TOOLS_URI##*/}"
+	APK="$(ls "$TEMP_DIR"/apk-tools-*/apk)"
+fi
+
+#-----------------------------------------------------------------------
+einfo 'Installing base system'
+
+mkdir -p "$rootfs"/etc/apk/keys
+
+if [ -f "$REPOS_FILE" ]; then
+	install -m 644 "$REPOS_FILE" "$rootfs"/etc/apk/repositories
+else
+	cat > "$rootfs"/etc/apk/repositories <<-EOF
+		$ALPINE_MIRROR/$ALPINE_BRANCH/main
+		$ALPINE_MIRROR/$ALPINE_BRANCH/community
+	EOF
+fi
+
+if [ -d "$KEYS_DIR" ]; then
+	cp "$KEYS_DIR"/* "$rootfs"/etc/apk/keys/
+else
+	dump_alpine_keys "$rootfs"/etc/apk/keys/
+fi
+
+_apk add --root "$rootfs" --update-cache --initdb $ALPINE_BASE_PKGS >&2
+
+# This package contains /etc/os-release, /etc/alpine-release and /etc/issue,
+# but we don't wanna install all its dependencies (e.g. openrc).
+_apk fetch --root "$rootfs" --stdout alpine-base \
+	| tar -xz -C "$rootfs" etc >&2
+ln -sf /run "$rootfs"/var/run
+
+_apk add --root "$rootfs" -t "$VIRTUAL_PKG" apk-tools >&2
+
+#-----------------------------------------------------------------------
+if [ "$TIMEZONE" ]; then
+	einfo "Setting timezone $TIMEZONE"
+	setup_timezone "$TIMEZONE" "$rootfs" >&2
+fi
+
+#-----------------------------------------------------------------------
+if [ "$PACKAGES" ]; then
+	einfo 'Installing additional packages'
+	_apk add --root "$rootfs" $PACKAGES >&2
+fi
+
+#-----------------------------------------------------------------------
+if [ "$SCRIPT" ]; then
+	script_name="${SCRIPT##*/}"
+
+	if [ "$SCRIPT_CHROOT" = 'no' ]; then
+		einfo "Executing script: $script_name $*"
+
+		( cd "$rootfs" && "$SCRIPT" "$@" >&2 ) || die 'Script failed'
+	else
+		einfo "Executing script in chroot: $script_name $*"
+
+		prepare_chroot "$rootfs"
+		mount --bind "${SCRIPT%/*}" "$rootfs"/mnt
+		chroot "$rootfs" \
+			sh -c "cd /mnt && ./$script_name \"\$@\"" -- "$@" >&2 \
+			|| die 'Script failed'
+		umount_recursively "$rootfs"
+	fi
+fi
+
+#-----------------------------------------------------------------------
+einfo 'Cleaning-up rootfs'
+
+_apk del --root "$rootfs" --purge "$VIRTUAL_PKG" >&2
+
+rm -Rf "$rootfs"/var/cache/apk "$rootfs"/etc/resolv.conf
+rm -Rf "$rootfs"/dev/*
+
+[ -f "$rootfs"/sbin/apk ] \
+	|| rm -Rf "$rootfs"/etc/apk "$rootfs"/lib/apk
+
+for dir in $FUTILE_DIRS; do
+	rmdir -p "$rootfs$dir" 2>/dev/null || true
+done
+
+#-----------------------------------------------------------------------
+if [ "$tar_opts" ]; then
+	einfo 'Creating rootfs archive'
+
+	tar -C "$rootfs" $tar_opts --numeric-owner -f "$ROOTFS_DEST" .
+	ls -la "$ROOTFS_DEST" >&2
+fi