Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional embedding of container images #239

Merged
merged 22 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6235ab6
Update linuxkit to 1.5.0:
jacobweinstock Aug 21, 2024
ff15b8a
Fix Docker build warnings:
jacobweinstock Aug 21, 2024
9fc15e8
Add optional embedding of container images into DinD:
jacobweinstock Aug 21, 2024
9318be6
Add note on embedded images being optional:
jacobweinstock Aug 21, 2024
f68dc4b
Add platform option to pull-images.sh:
jacobweinstock Aug 21, 2024
d481896
Don't git track the images.txt file:
jacobweinstock Aug 21, 2024
4165fe2
Update the embed process:
jacobweinstock Aug 22, 2024
72fde59
Change the way the images are presented to hook-docker:
jacobweinstock Aug 23, 2024
385941e
Add newlines to all files without them:
jacobweinstock Aug 23, 2024
d0d5e0f
Remove .dockerignore:
jacobweinstock Aug 23, 2024
04dc1bc
Make the docker:dind image configurable:
jacobweinstock Aug 23, 2024
4bd7ee6
Refactor script to pull images:
jacobweinstock Aug 26, 2024
882aa21
Fix multi arch:
jacobweinstock Aug 26, 2024
b490bac
Load images from inside DinD:
jacobweinstock Aug 26, 2024
4a8f7ce
Add note on Docker storage driver:
jacobweinstock Aug 26, 2024
58eb394
Remove note on host Docker storage driver:
jacobweinstock Aug 26, 2024
de8a316
Update images.txt format:
jacobweinstock Aug 26, 2024
67a2d6c
Add some code comments
jacobweinstock Aug 26, 2024
845d6b2
Check that DinD uses the overlay2 storage driver:
jacobweinstock Aug 27, 2024
6be6b09
Add ssl certs to SSH container:
jacobweinstock Aug 27, 2024
50cdc1d
Fix build process:
jacobweinstock Aug 27, 2024
78f2850
Update hook-bootkit:
jacobweinstock Aug 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ cache/
*.swp
.idea
kernel/Dockerfile.autogen.*
images/hook-embedded/images/*
!images/hook-embedded/images/.keep
images/hook-embedded/images.txt
images/hook-embedded/docker/*
!images/hook-embedded/docker/.keep
images/hook-embedded/images_tar/*
!images/hook-embedded/images_tar/.keep
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,21 @@ The `gha-matrix` CLI command prepares a set of JSON outputs for GitHub Actions m
- `DOCKER_ARCH` is used by the `linuxkit-containers` command to build the containers for the specified architecture.
- `DO_PUSH`: `yes` or `no`, will push the built containers to the OCI registry; defaults to `no`.

### Embedding container images into the DinD (docker-in-docker), also known as [hook-docker](images/hook-docker/), container

For use cases where having container images already available in Docker is needed, the following steps can be taken to embed container images into hook-docker (DinD):

> Note: This is optional and no container images will be embedded by default.

> Note: This will increase the overall size of HookOS. As HookOS is an in memory OS, make sure that the size increase works for the machines you are provisioning.

1. Create a file named `images.txt` in the [images/hook-embedded/](images/hook-embedded/) directory.
1. Populate this `images.txt` file with the list of images to be embedded. See [images/hook-embedded/images.txt.example](images/hook-embedded/images.txt.example) for details on the required file format.
1. Change directories to [images/hook-embedded/](images/hook-embedded/) and run [`pull-images.sh`](images/hook-embedded/pull-images.sh) script when building amd64 images and run [`pull-images.sh arm64`](images/hook-embedded/pull-images.sh) when building arm64 images. Read the comments at the top of the script for more details.
1. Change directories to the root of the HookOS repository and run `sudo ./build.sh build ...` to build the HookOS kernel and ramdisk. FYI, `sudo` is needed as DIND changes file ownerships to root.

### Build system TO-DO list

- [ ] Update to Linuxkit 1.2.0 and new linuxkit pkgs; this might lead into the containerd vs dind;
- [ ] `make debug` functionality (sshd enabled) was lost in the Makefile -> bash transition;

[formats]: https://github.com/linuxkit/linuxkit/blob/master/README.md#booting-and-testing
Expand Down
1 change: 1 addition & 0 deletions bash/hook-lk-containers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function build_all_hook_linuxkit_containers() {
build_hook_linuxkit_container hook-mdev HOOK_CONTAINER_MDEV_IMAGE
build_hook_linuxkit_container hook-containerd HOOK_CONTAINER_CONTAINERD_IMAGE
build_hook_linuxkit_container hook-runc HOOK_CONTAINER_RUNC_IMAGE
build_hook_linuxkit_container hook-embedded HOOK_CONTAINER_EMBEDDED_IMAGE
}

function build_hook_linuxkit_container() {
Expand Down
3 changes: 2 additions & 1 deletion bash/linuxkit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ function linuxkit_build() {
HOOK_CONTAINER_MDEV_IMAGE="${HOOK_CONTAINER_MDEV_IMAGE}" \
HOOK_CONTAINER_CONTAINERD_IMAGE="${HOOK_CONTAINER_CONTAINERD_IMAGE}" \
HOOK_CONTAINER_RUNC_IMAGE="${HOOK_CONTAINER_RUNC_IMAGE}" \
envsubst '$HOOK_VERSION $HOOK_KERNEL_IMAGE $HOOK_KERNEL_ID $HOOK_KERNEL_VERSION $HOOK_CONTAINER_IP_IMAGE $HOOK_CONTAINER_BOOTKIT_IMAGE $HOOK_CONTAINER_DOCKER_IMAGE $HOOK_CONTAINER_MDEV_IMAGE $HOOK_CONTAINER_CONTAINERD_IMAGE $HOOK_CONTAINER_RUNC_IMAGE' \
HOOK_CONTAINER_EMBEDDED_IMAGE="${HOOK_CONTAINER_EMBEDDED_IMAGE}" \
envsubst '$HOOK_VERSION $HOOK_KERNEL_IMAGE $HOOK_KERNEL_ID $HOOK_KERNEL_VERSION $HOOK_CONTAINER_IP_IMAGE $HOOK_CONTAINER_BOOTKIT_IMAGE $HOOK_CONTAINER_DOCKER_IMAGE $HOOK_CONTAINER_MDEV_IMAGE $HOOK_CONTAINER_CONTAINERD_IMAGE $HOOK_CONTAINER_RUNC_IMAGE $HOOK_CONTAINER_EMBEDDED_IMAGE' \
> "hook.${inventory_id}.yaml"

declare -g linuxkit_bin=""
Expand Down
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ declare -g HOOK_LK_CONTAINERS_OCI_BASE="${HOOK_LK_CONTAINERS_OCI_BASE:-"quay.io/
declare -g SKOPEO_IMAGE="${SKOPEO_IMAGE:-"quay.io/skopeo/stable:latest"}"

# See https://github.com/linuxkit/linuxkit/releases
declare -g -r LINUXKIT_VERSION_DEFAULT="1.2.0" # LinuxKit version to use by default; each flavor can set its own too
declare -g -r LINUXKIT_VERSION_DEFAULT="1.5.0" # LinuxKit version to use by default; each flavor can set its own too

# Directory to use for storing downloaded artifacts: LinuxKit binary, shellcheck binary, etc.
declare -g -r CACHE_DIR="${CACHE_DIR:-"cache"}"
Expand Down
2 changes: 1 addition & 1 deletion images/hook-bootkit/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.21-alpine as dev
FROM golang:1.22.6-alpine AS dev
COPY . /src/
WORKDIR /src
RUN go mod download
Expand Down
4 changes: 3 additions & 1 deletion images/hook-bootkit/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/tinkerbell/hook/hook-bootkit

go 1.17
go 1.22

toolchain go1.22.6

require (
github.com/cenkalti/backoff/v4 v4.3.0
Expand Down
39 changes: 25 additions & 14 deletions images/hook-bootkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
"time"

"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
Expand Down Expand Up @@ -127,13 +127,22 @@ func run(ctx context.Context, log logr.Logger) error {

authStr := base64.URLEncoding.EncodeToString(encodedJSON)

pullOpts := types.ImagePullOptions{
pullOpts := image.PullOptions{
RegistryAuth: authStr,
}
var out io.ReadCloser
imagePullOperation := func() error {
// with embedded images, the tink worker could potentially already exist
// in the local Docker image cache. And the image name could be something
// unreachable via the network (for example: 127.0.0.1/embedded/tink-worker).
// Because of this we check if the image already exists and don't return an
// error if the image does not exist and the pull fails.
var imageExists bool
if _, _, err := cli.ImageInspectWithRaw(ctx, imageName); err == nil {
imageExists = true
}
out, err = cli.ImagePull(ctx, imageName, pullOpts)
if err != nil {
if err != nil && !imageExists {
log.Error(err, "image pull failure", "imageName", imageName)
return err
}
Expand All @@ -143,18 +152,20 @@ func run(ctx context.Context, log logr.Logger) error {
return err
}

buf := bufio.NewScanner(out)
for buf.Scan() {
structured := make(map[string]interface{})
if err := json.Unmarshal(buf.Bytes(), &structured); err != nil {
log.Info("image pull logs", "output", buf.Text())
} else {
log.Info("image pull logs", "logs", structured)
}
if out != nil {
buf := bufio.NewScanner(out)
for buf.Scan() {
structured := make(map[string]interface{})
if err := json.Unmarshal(buf.Bytes(), &structured); err != nil {
log.Info("image pull logs", "output", buf.Text())
} else {
log.Info("image pull logs", "logs", structured)
}

}
if err := out.Close(); err != nil {
log.Error(err, "closing image pull logs failed")
}
if err := out.Close(); err != nil {
log.Error(err, "closing image pull logs failed")
}
}

log.Info("Removing any existing tink-worker container")
Expand Down
3 changes: 2 additions & 1 deletion images/hook-docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.20-alpine as dev
FROM golang:1.20-alpine AS dev
COPY . /src/
WORKDIR /src
RUN CGO_ENABLED=0 go build -a -ldflags '-s -w -extldflags "-static"' -o /hook-docker
Expand All @@ -13,4 +13,5 @@ RUN strip /usr/local/bin/docker /usr/local/bin/dockerd /usr/local/bin/docker-pro
# Purge binutils package after stripping
RUN apk del binutils
COPY --from=dev /hook-docker .

ENTRYPOINT ["/hook-docker"]
9 changes: 9 additions & 0 deletions images/hook-embedded/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM scratch
ENTRYPOINT []
WORKDIR /
COPY ./images/ /etc/embedded-images/
# the name 001 is important as that is the order in which the scripts are executed
# we need this mounting to happen before the other init.d scripts run so that
# the mount points are available to them.
COPY ./images-mount.sh /etc/init.d/001-images-mount.sh
CMD []
Empty file.
17 changes: 17 additions & 0 deletions images/hook-embedded/images-mount.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh

exec 3>&1 4>&2
trap 'exec 2>&4 1>&3' 0 1 2 3
exec 1>/var/log/embedded-images.log 2>&1

set -xeuo pipefail

# We can't have a Linuxkit "init" container that dumps its file contents to /var and be writable
# because the init process overwrites it and the contents are lost.
# Instead, we have the init container, with all the Docker images, dump its contents to /etc/embedded-images.
# Then we bind mount /etc/embedded-images to /run/images (/var/run is symlinked to /run) and make sure it's
# read/write. This allows the DinD container to bind mount /var/run/images to /var/lib/docker and the Docker
# images are available right away and /var/lib/docker is writable.
mkdir -p /run/images
mount -o bind,rw /etc/embedded-images/ /run/images
mount -o remount,rw /run/images
9 changes: 9 additions & 0 deletions images/hook-embedded/images.txt.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This is an example file. It explains the required format.
# For the actual file, you must remove all the comments.
# The format is source image, a single space, optional additional tag of the source image, a single space, true or false to remove the original tag.
#<source image> <optional additional tag of the source image> <remove original tag>
# for example:
quay.io/tinkerbell/tink-worker:v0.10.0
quay.io/tinkerbell/tink-worker:v0.10.0 tink-worker:v0.10.0 true
quay.io/tinkerbell/actions/image2disk embedded/actions/image2disk
quay.io/tinkerbell/actions/cexec 127.0.0.1/embedded/actions/cexec true
Empty file.
Empty file.
124 changes: 124 additions & 0 deletions images/hook-embedded/pull-images.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/bin/bash

# This script is used to build container images that are embedded in HookOS.
# When HookOS boots up, the DinD container will have all the images in its cache.

set -euo pipefail

function docker_save_image() {
local image="$1"
local output_dir="$2"
local output_file="${output_dir}/$(echo "${image}" | tr '/' '-')"

docker save -o "${output_file}".tar "${image}"
}

function docker_pull_image() {
local image="$1"
local arch="${2-amd64}"

docker pull --platform=linux/"${arch}" "${image}"
}

function docker_remove_image() {
local image="$1"

docker rmi "${image}" || true
}

function trap_handler() {
local dind_container="$1"

if [[ "${remove_dind_container}" == "true" ]]; then
docker rm -f "${dind_container}" &> /dev/null
else
echo "DinD container NOT removed, please remove it manually"
fi
}

function main() {
local dind_container="$1"
local images_file="$2"
local arch="$3"
local dind_container_image="$4"

# Pull the images
while IFS=" " read -r first_image image_tag || [ -n "${first_image}" ] ; do
echo -e "----------------------- $first_image -----------------------"
# Remove the image if it exists so that the image pulls the correct architecture
docker_remove_image "${first_image}"
docker_pull_image "${first_image}" "${arch}"
done < "${images_file}"

# Save the images
local output_dir="${PWD}/images_tar"
mkdir -p "${output_dir}"
while IFS=" " read -r first_image image_tag || [ -n "${first_image}" ] ; do
docker_save_image "${first_image}" "${output_dir}"
done < "${images_file}"

export remove_dind_container="true"
# as this function maybe called multiple times, we need to ensure the container is removed
trap "trap_handler ${dind_container}" RETURN
# we're using set -e so the trap on RETURN will not be executed when a command fails
trap "trap_handler ${dind_container}" EXIT

# start DinD container
# In order to avoid the src bind mount directory (./images/) ownership from changing to root
# we don't bind mount to /var/lib/docker in the container because the DinD container is running as root and
# will change the permissions of the bind mount directory (images/) to root.
echo -e "Starting DinD container"
echo -e "-----------------------"
docker run -d --privileged --name "${dind_container}" -v "${PWD}/images_tar":/images_tar -v "${PWD}"/images/:/var/lib/docker-embedded/ -d "${dind_container_image}"

# wait until the docker daemon is ready
until docker exec "${dind_container}" docker info &> /dev/null; do
sleep 1
if [[ $(docker inspect -f '{{.State.Status}}' "${dind_container}") == "exited" ]]; then
echo "DinD container exited unexpectedly"
docker logs "${dind_container}"
exit 1
fi
done

# As hook-docker uses the overlay2 storage driver the DinD must use the overlay2 storage driver too.
# make sure the overlay2 storage driver is used by the DinD container.
# The VFS storage driver might get used if /var/lib/docker in the DinD container cannot be used by overlay2.
storage_driver=$(docker exec "${dind_container}" docker info --format '{{.Driver}}')
if [[ "${storage_driver}" != "overlay2" ]]; then
export remove_dind_container="false"
echo "DinD container is not using overlay2 storage driver, storage driver detected: ${storage_driver}"
exit 1
fi

# remove the contents of /var/lib/docker-embedded so that any previous images are removed. Without this it seems to cause boot issues.
docker exec "${dind_container}" sh -c "rm -rf /var/lib/docker-embedded/*"

# Load the images
for image_file in "${output_dir}"/*; do
echo -e "Loading image: ${image_file}"
docker exec "${dind_container}" docker load -i "/images_tar/$(basename ${image_file})"
done

# clean up tar files
rm -rf "${output_dir}"/*

# Create any tags for the images and remove any original tags
while IFS=" " read -r first_image image_tag remove_original || [ -n "${first_image}" ] ; do
if [[ "${image_tag}" != "" ]]; then
docker exec "${dind_container}" docker tag "${first_image}" "${image_tag}"
if [[ "${remove_original}" == "true" ]]; then
docker exec "${dind_container}" docker rmi "${first_image}"
fi
fi
done < "${images_file}"

# We need to copy /var/lib/docker to /var/lib/docker-embedded in order for HookOS to use the Docker images in its build.
docker exec "${dind_container}" sh -c "cp -a /var/lib/docker/* /var/lib/docker-embedded/"
}

arch="${1-amd64}"
dind_container_name="hookos-dind"
images_file="images.txt"
dind_container_image="${2-docker:dind}"
main "${dind_container_name}" "${images_file}" "${arch}" "${dind_container_image}"
31 changes: 22 additions & 9 deletions linuxkit-templates/hook.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@
# - HOOK_CONTAINER_MDEV_IMAGE: ${HOOK_CONTAINER_MDEV_IMAGE}
# - HOOK_CONTAINER_CONTAINERD_IMAGE: ${HOOK_CONTAINER_CONTAINERD_IMAGE}
# - HOOK_CONTAINER_RUNC_IMAGE: ${HOOK_CONTAINER_RUNC_IMAGE}
# - HOOK_CONTAINER_EMBEDDED_IMAGE: ${HOOK_CONTAINER_EMBEDDED_IMAGE}
# - Other variables are not replaced: for example this is a literal dollarsign-SOMETHING: $SOMETHING and with braces: ${SOMETHING}

kernel:
image: "${HOOK_KERNEL_IMAGE}"
cmdline: "this_is_not_used=at_at_all_in_hook command_line_is_determined_by=ipxe"
cmdline: "this_is_not_used=at_all_in_hook command_line_is_determined_by=ipxe"

init:
# this sha is the first with cgroups v2 as the default
- linuxkit/init:8a7b6cdb89197dc94eb6db69ef9dc90b750db598
# this init container sha has support for volumes
- linuxkit/init:872d2e1be745f1acb948762562cf31c367303a3b
- "${HOOK_CONTAINER_RUNC_IMAGE}"
- "${HOOK_CONTAINER_CONTAINERD_IMAGE}"
- linuxkit/ca-certificates:v1.0.0
- linuxkit/firmware:24402a25359c7bc290f7fc3cd23b6b5f0feb32a5 # "Some" firmware from Linuxkit pkg; see https://github.com/linuxkit/linuxkit/blob/master/pkg/firmware/Dockerfile
- "${HOOK_CONTAINER_EMBEDDED_IMAGE}"

onboot:
- name: rngd1
Expand Down Expand Up @@ -100,6 +102,8 @@ services:
devices:
- path: all
type: b
- path: all
type: c
- path: "/dev/console"
type: c
major: 5
Expand Down Expand Up @@ -187,6 +191,8 @@ services:
devices:
- path: all
type: b
- path: all
type: c

- name: hook-bootkit
image: "${HOOK_CONTAINER_BOOTKIT_IMAGE}"
Expand Down Expand Up @@ -219,8 +225,15 @@ services:
mkdir:
- /var/lib/dhcpcd

#dbg - name: sshd
#dbg image: linuxkit/sshd:v1.0.0
#SSH_SERVER - name: sshd
#SSH_SERVER image: linuxkit/sshd:v1.0.0
#SSH_SERVER binds.add:
#SSH_SERVER - /etc/profile.d/local.sh:/etc/profile.d/local.sh
#SSH_SERVER - /root/.ssh/authorized_keys:/root/.ssh/authorized_keys
#SSH_SERVER - /usr/bin/nerdctl:/usr/bin/nerdctl
#SSH_SERVER - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt
#SSH_SERVER - /:/host_root


files:
- path: etc/profile.d/local.sh
Expand Down Expand Up @@ -299,10 +312,10 @@ files:
ttyUSB1
ttyUSB2

#dbg - path: root/.ssh/authorized_keys
#dbg source: ~/.ssh/id_rsa.pub
#dbg mode: "0600"
#dbg optional: true
#SSH_SERVER - path: root/.ssh/authorized_keys
#SSH_SERVER source: ~/.ssh/id_rsa.pub
#SSH_SERVER mode: "0600"
#SSH_SERVER optional: true

trust:
org:
Expand Down
Loading