diff --git a/gh-podman-in-podman/Containerfile b/gh-podman-in-podman/Containerfile new file mode 100644 index 0000000..8ef0d64 --- /dev/null +++ b/gh-podman-in-podman/Containerfile @@ -0,0 +1,66 @@ +FROM registry.access.redhat.com/ubi8/python-311 + +USER root + +RUN dnf -y module enable container-tools:rhel8; dnf -y update; rpm --restore --quiet shadow-utils; \ + dnf -y install crun podman netavark fuse-overlayfs openssh-clients /etc/containers/storage.conf \ + curl git jq hostname procps findutils which openssl --exclude container-selinux; \ + rm -rf /var/cache/* /var/log/dnf* /var/log/yum.* + +ENV UID=1000 +ENV GID=0 +ENV USERNAME="runner" +ARG ANSIBLE_VERSION="2.15.0" + +RUN useradd -m $USERNAME -u $UID +# This is to mimic the OpenShift behaviour of adding the dynamic user to group 0. +RUN usermod -G 0 $USERNAME +ENV HOME /home/${USERNAME} +WORKDIR /home/${USERNAME} + +RUN chmod g+w /etc/passwd && \ + touch /etc/sub{g,u}id && \ + chmod -v ug+rw /etc/sub{g,u}id && \ + echo -e "runner:1:999\nrunner:1001:64535" > /etc/subuid && \ + echo -e "runner:1:999\nrunner:1001:64535" > /etc/subgid + +COPY --chown=${USERNAME}:0 entrypoint.sh uid.sh register.sh get_github_app_token.sh ./ + +ADD containers.conf /etc/containers/containers.conf +ADD podman-containers.conf /home/runner/.config/containers/containers.conf + +RUN mkdir -p /home/runner/.local/share/containers && \ + chown runner:runner -R /home/runner && \ + chmod 644 /etc/containers/containers.conf + +ADD storage.conf /usr/share/containers/storage.conf +RUN sed -e 's|^#mount_program|mount_program|g' \ + -e '/additionalimage.*/a "/var/lib/shared",' \ + -e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' \ + /usr/share/containers/storage.conf \ + > /etc/containers/storage.conf + +RUN printf '/run/secrets/etc-pki-entitlement:/run/secrets/etc-pki-entitlement\n/run/secrets/rhsm:/run/secrets/rhsm\n' > /etc/containers/mounts.conf + +VOLUME /var/lib/containers +VOLUME /home/runner/.local/share/containers + +RUN mkdir -p /var/lib/shared/overlay-images \ + /var/lib/shared/overlay-layers \ + /var/lib/shared/vfs-images \ + /var/lib/shared/vfs-layers && \ + touch /var/lib/shared/overlay-images/images.lock && \ + touch /var/lib/shared/overlay-layers/layers.lock && \ + touch /var/lib/shared/vfs-images/images.lock && \ + touch /var/lib/shared/vfs-layers/layers.lock + +ENV _CONTAINERS_USERNS_CONFIGURED="" + +COPY --chown=${USERNAME}:0 get-runner-release.sh ./ +RUN chmod ug+x ./get-runner-release.sh +RUN ./get-runner-release.sh +RUN ./bin/installdependencies.sh +RUN pip install --progress-bar off 'molecule>=24.2.0' 'molecule-plugins[docker]>=23.0.0' 'molecule-plugins[podman]' "ansible-core~=${ANSIBLE_VERSION}" + +ENTRYPOINT ./entrypoint.sh + diff --git a/gh-podman-in-podman/containers.conf b/gh-podman-in-podman/containers.conf new file mode 100644 index 0000000..c04303b --- /dev/null +++ b/gh-podman-in-podman/containers.conf @@ -0,0 +1,13 @@ +[containers] +netns="host" +userns="host" +ipcns="host" +utsns="host" +cgroupns="host" +cgroups="disabled" +log_driver = "k8s-file" +[engine] +cgroup_manager = "cgroupfs" +events_logger="file" +runtime="crun" + diff --git a/gh-podman-in-podman/entrypoint.sh b/gh-podman-in-podman/entrypoint.sh new file mode 100644 index 0000000..adfed64 --- /dev/null +++ b/gh-podman-in-podman/entrypoint.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Based on https://github.com/bbrowning/github-runner/blob/master/entrypoint.sh + +./uid.sh > /tmp/uid.sh.log + +set -eE + +CREDS_FILE="${PWD}/.credentials" + +# Assume registration artifacts have been persisted from a previous start +# if no PAT or TOKEN is provided, and simply attempt to start. +if [ -n "${GITHUB_PAT:-}" ] || [ -n "${RUNNER_TOKEN:-}" ] || [ -n "${GITHUB_APP_ID:-}" ]; then + source ./register.sh +elif [ -e "${CREDS_FILE}" ]; then + echo "No GITHUB_PAT or RUNNER_TOKEN provided. Using existing credentials file ${CREDS_FILE}." +else + echo "No saved credentials found in ${CREDS_FILE}." + echo "Fatal: GITHUB_PAT or RUNNER_TOKEN must be set in the environment." + exit 1 +fi + +if [ -n "${GITHUB_PAT:-}" ]; then + trap 'remove; exit 130' INT + trap 'remove; exit 143' TERM +elif [ -n "${GITHUB_APP_ID:-}" ]; then + trap 'remove_github_app; exit 130' INT + trap 'remove_github_app; exit 143' TERM +else + trap 'exit 130' INT + trap 'exit 143' TERM +fi + +set -x +./bin/runsvc.sh --once & +svc_pid=$! + +wait $svc_pid + diff --git a/gh-podman-in-podman/get-runner-release.sh b/gh-podman-in-podman/get-runner-release.sh new file mode 100644 index 0000000..8def54c --- /dev/null +++ b/gh-podman-in-podman/get-runner-release.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Use the GitHub API to find the latest release of the GitHub Action runner, +# then download and extract the tarball for that release. + +set -eE -o pipefail + +release_file=/tmp/latest-runner-release.json +releases_api=https://api.github.com/repos/actions/runner/releases/latest + +echo "Fetching latest release from $releases_api" + +if [ ! $GITHUB_PAT = '' ]; then + # Set this to work around rate-limiting issues + echo "GITHUB_PAT is set; using for GitHub API" + auth_header="Authorization: token $GITHUB_PAT" +fi + +curl -sSLf -H "$auth_header" -H 'Accept: application/json' -o $release_file $releases_api + +latest_tag=$(jq -r '.tag_name' $release_file) +echo "Latest runner is ${latest_tag}" +echo $latest_tag >> ".RUNNER_VERSION" +rm $release_file + +tag_without_v=$(echo $latest_tag | cut -c 2-) + +os="linux" # could be "win" or "osx" +arch="x64" # for linux os, could be "arm" or "arm64" + +runner_tar="actions-runner-${os}-${arch}-${tag_without_v}.tar.gz" +runner_url="https://github.com/actions/runner/releases/download/${latest_tag}/${runner_tar}" + +set -x +curl -sSLf -O ${runner_url} +tar fxzp ${runner_tar} +rm ${runner_tar} + diff --git a/gh-podman-in-podman/get_github_app_token.sh b/gh-podman-in-podman/get_github_app_token.sh new file mode 100644 index 0000000..1d651ac --- /dev/null +++ b/gh-podman-in-podman/get_github_app_token.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Adapted from https://stackoverflow.com/a/62646786 and +# Github's docs: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app + +get_github_app_token() { + NOW=$( date +%s ) + IAT=$((${NOW} - 60)) + EXP=$((${NOW} + 540)) + HEADER_RAW='{"alg":"RS256"}' + HEADER=$( echo -n "${HEADER_RAW}" | openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n' ) + PAYLOAD_RAW='{"iat":'"${IAT}"',"exp":'"${EXP}"',"iss":'"${GITHUB_APP_ID}"'}' + PAYLOAD=$( echo -n "${PAYLOAD_RAW}" | openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n' ) + HEADER_PAYLOAD="${HEADER}"."${PAYLOAD}" + + # Making a tmp directory here because /bin/sh doesn't support process redirection <() + tmp_dir=/tmp/github_app_tmp + mkdir "${tmp_dir}" + echo -n "${GITHUB_APP_PEM}" > "${tmp_dir}/github.pem" + echo -n "${HEADER_PAYLOAD}" > "${tmp_dir}/header" + SIGNATURE=$( openssl dgst -sha256 -sign "${tmp_dir}/github.pem" "${tmp_dir}/header" | openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n' ) + rm -rf "${tmp_dir}" + + JWT="${HEADER_PAYLOAD}"."${SIGNATURE}" + INSTALL_URL="https://${GITHUB_API_SERVER}/app/installations/${GITHUB_APP_INSTALL_ID}/access_tokens" + INSTALL_TOKEN_PAYLOAD=$(curl -sSfLX POST -H "Authorization: Bearer ${JWT}" -H "Accept: application/vnd.github.v3+json" "${INSTALL_URL}") + INSTALL_TOKEN=$(echo ${INSTALL_TOKEN_PAYLOAD} | jq .token --raw-output) + + echo "${INSTALL_TOKEN}" +} diff --git a/gh-podman-in-podman/podman-containers.conf b/gh-podman-in-podman/podman-containers.conf new file mode 100644 index 0000000..53c7e79 --- /dev/null +++ b/gh-podman-in-podman/podman-containers.conf @@ -0,0 +1,6 @@ +[containers] +volumes = [ + "/proc:/proc", +] +default_sysctls = [] + diff --git a/gh-podman-in-podman/register.sh b/gh-podman-in-podman/register.sh new file mode 100644 index 0000000..c1c87e0 --- /dev/null +++ b/gh-podman-in-podman/register.sh @@ -0,0 +1,121 @@ +#!/bin/sh +# Based on https://github.com/bbrowning/github-runner/blob/master/entrypoint.sh + +set -eE + +# Load Github app authentication helper function +source ./get_github_app_token.sh + +if [ -z "${GITHUB_OWNER:-}" ]; then + echo "Fatal: \$GITHUB_OWNER must be set in the environment" + exit 1 +fi + +if [ -z "${GITHUB_DOMAIN:-}" ]; then + echo "Connecting to public GitHub" + GITHUB_DOMAIN="github.com" + GITHUB_API_SERVER="api.github.com" +else + echo "Connecting to GitHub server at '$GITHUB_DOMAIN'" + GITHUB_API_SERVER="${GITHUB_DOMAIN}/api/v3" +fi + +echo "GitHub API server is '$GITHUB_API_SERVER'" + +if [ -z "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_REPO:-}" ]; then + GITHUB_REPOSITORY=$GITHUB_REPO +fi + +# https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#create-a-registration-token-for-an-organization + +registration_url="https://${GITHUB_DOMAIN}/${GITHUB_OWNER}${GITHUB_REPOSITORY:+/$GITHUB_REPOSITORY}" + +if [ -z "${GITHUB_PAT:-}" ] && [ -z "${GITHUB_APP_ID:-}" ]; then + echo "Neither GITHUB_PAT nor the GITHUB_APP variables are set in the environment. Automatic runner removal will be disabled." + echo "Visit ${registration_url}/settings/actions/runners to manually force removal of runner." +fi + +if [ -z "${RUNNER_TOKEN:-}" ]; then + if [ -z "${GITHUB_REPOSITORY:-}" ]; then + echo "Runner is scoped to organization '${GITHUB_OWNER}'" + echo "View runner status at https://${GITHUB_DOMAIN}/organizations/${GITHUB_OWNER}/settings/actions" + + token_url="https://${GITHUB_API_SERVER}/orgs/${GITHUB_OWNER}/actions/runners/registration-token" + else + echo "Runner is scoped to repository '${GITHUB_OWNER}/${GITHUB_REPOSITORY}'" + echo "View runner status at https://${GITHUB_DOMAIN}/${GITHUB_OWNER}/${GITHUB_REPOSITORY}/settings/actions" + + token_url="https://${GITHUB_API_SERVER}/repos/${GITHUB_OWNER}/${GITHUB_REPOSITORY}/actions/runners/registration-token" + fi + echo "Obtaining runner token from ${token_url}" + + if [ -n "${GITHUB_APP_ID:-}" ] && [ -n "${GITHUB_APP_INSTALL_ID:-}" ] && [ -n "${GITHUB_APP_PEM:-}" ]; then + echo "GITHUB_APP environment variables are set. Using GitHub App authentication." + app_token=$(get_github_app_token) + payload=$(curl -sSfLX POST -H "Authorization: token ${app_token}" ${token_url}) + else + echo "Using GITHUB_PAT for authentication." + payload=$(curl -sSfLX POST -H "Authorization: token ${GITHUB_PAT}" ${token_url}) + fi + + export RUNNER_TOKEN=$(echo $payload | jq .token --raw-output) + echo "Obtained registration token" +else + echo "Using RUNNER_TOKEN from environment" +fi + +labels_arg="" +if [ -n "${RUNNER_LABELS:-}" ]; then + labels_arg="--labels $RUNNER_LABELS" +else + echo "No labels provided" +fi + +runner_group_arg="" +# Runner groups are only valid for organization-wide runners +if [ -n "${RUNNER_GROUP:-}" ]; then + if [ -z "${GITHUB_REPOSITORY:-}" ]; then + runner_group_arg="--runnergroup $RUNNER_GROUP" + else + echo "Not applying runner group '${RUNNER_GROUP}' - Runner groups are not valid for repository-scoped runners." + fi +else + echo "No runner group provided" +fi + +ephemeral_arg="" +if [ -n "${EPHEMERAL:-}" ]; then + ephemeral_arg="--ephemeral" +fi + +if [ -n "${RUNNER_TOKEN:-}" ]; then + set -x + ./config.sh \ + --name $(hostname) \ + --token ${RUNNER_TOKEN} \ + --url ${registration_url} \ + --work ${RUNNER_WORKDIR} \ + ${labels_arg} \ + ${runner_group_arg} \ + ${ephemeral_arg} \ + --unattended \ + --replace + set +x +fi + +remove() { + payload=$(curl -sSfLX POST -H "Authorization: token ${GITHUB_PAT}" ${token_url%/registration-token}/remove-token) + export REMOVE_TOKEN=$(echo $payload | jq .token --raw-output) + + ./config.sh remove --unattended --token "${REMOVE_TOKEN}" +} + +remove_github_app() { + app_token=$(get_github_app_token) + payload=$(curl -sSfLX POST -H "Authorization: token ${app_token}" ${token_url%/registration-token}/remove-token) + export REMOVE_TOKEN=$(echo $payload | jq .token --raw-output) + + ./config.sh remove --unattended --token "${REMOVE_TOKEN}" +} + + diff --git a/gh-podman-in-podman/runnner-2.16@.service b/gh-podman-in-podman/runnner-2.16@.service new file mode 100644 index 0000000..0dd999c --- /dev/null +++ b/gh-podman-in-podman/runnner-2.16@.service @@ -0,0 +1,12 @@ +[Unit] +Description=GitHub self-hosted runner %i +After=network.target + +[Service] +ExecStart=podman run --name=runner-2.16-%i --replace --secret github_token --security-opt label=disable --security-opt seccomp=unconfined --device /dev/fuse:rw -v /var/lib/runner-2.16-%i:/var/lib/containers:Z --privileged -ti --rm -e GITHUB_OWNER=ansible-middleware -e RUNNER_LABELS='molecule-2.16,runner-1' -e RUNNER_ALLOW_RUNASROOT=1 -e EPHEMERAL=1 localhost/runner-3.11-2.16 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target + diff --git a/gh-podman-in-podman/storage.conf b/gh-podman-in-podman/storage.conf new file mode 100644 index 0000000..5c0b66f --- /dev/null +++ b/gh-podman-in-podman/storage.conf @@ -0,0 +1,158 @@ +# This file is the configuration file for all tools +# that use the containers/storage library. The storage.conf file +# overrides all other storage.conf files. Container engines using the +# container/storage library do not inherit fields from other storage.conf +# files. +# +# Note: The storage.conf file overrides other storage.conf files based on this precedence: +# /usr/containers/storage.conf +# /etc/containers/storage.conf +# $HOME/.config/containers/storage.conf +# $XDG_CONFIG_HOME/containers/storage.conf (If XDG_CONFIG_HOME is set) +# See man 5 containers-storage.conf for more information +# The "container storage" table contains all of the server options. +[storage] + +# Default Storage Driver, Must be set for proper operation. +driver = "overlay" + +# Temporary storage location +runroot = "/run/containers/storage" + +# Priority list for the storage drivers that will be tested one +# after the other to pick the storage driver if it is not defined. +# driver_priority = ["overlay", "btrfs"] + +# Primary Read/Write location of container storage +# When changing the graphroot location on an SELINUX system, you must +# ensure the labeling matches the default locations labels with the +# following commands: +# semanage fcontext -a -e /var/lib/containers/storage /NEWSTORAGEPATH +# restorecon -R -v /NEWSTORAGEPATH +graphroot = "/var/lib/containers/storage" + +# Optional alternate location of image store if a location separate from the +# container store is required. If set, it must be different than graphroot. +# imagestore = "" + + +# Storage path for rootless users +# +# rootless_storage_path = "$HOME/.local/share/containers/storage" + +# Transient store mode makes all container metadata be saved in temporary storage +# (i.e. runroot above). This is faster, but doesn't persist across reboots. +# Additional garbage collection must also be performed at boot-time, so this +# option should remain disabled in most configurations. +# transient_store = true + +[storage.options] +# Storage options to be passed to underlying storage drivers + +# AdditionalImageStores is used to pass paths to additional Read/Only image stores +# Must be comma separated list. +additionalimagestores = [ +] + +# Allows specification of how storage is populated when pulling images. This +# option can speed the pulling process of images compressed with format +# zstd:chunked. Containers/storage looks for files within images that are being +# pulled from a container registry that were previously pulled to the host. It +# can copy or create a hard link to the existing file when it finds them, +# eliminating the need to pull them from the container registry. These options +# can deduplicate pulling of content, disk storage of content and can allow the +# kernel to use less memory when running containers. + +# containers/storage supports four keys +# * enable_partial_images="true" | "false" +# Tells containers/storage to look for files previously pulled in storage +# rather then always pulling them from the container registry. +# * use_hard_links = "false" | "true" +# Tells containers/storage to use hard links rather then create new files in +# the image, if an identical file already existed in storage. +# * ostree_repos = "" +# Tells containers/storage where an ostree repository exists that might have +# previously pulled content which can be used when attempting to avoid +# pulling content from the container registry +# * convert_images = "false" | "true" +# If set to true, containers/storage will convert images to a +# format compatible with partial pulls in order to take advantage +# of local deduplication and hard linking. It is an expensive +# operation so it is not enabled by default. +pull_options = {enable_partial_images = "true", use_hard_links = "false", ostree_repos=""} + +# Root-auto-userns-user is a user name which can be used to look up one or more UID/GID +# ranges in the /etc/subuid and /etc/subgid file. These ranges will be partitioned +# to containers configured to create automatically a user namespace. Containers +# configured to automatically create a user namespace can still overlap with containers +# having an explicit mapping set. +# This setting is ignored when running as rootless. +# root-auto-userns-user = "storage" +# +# Auto-userns-min-size is the minimum size for a user namespace created automatically. +# auto-userns-min-size=1024 +# +# Auto-userns-max-size is the maximum size for a user namespace created automatically. +# auto-userns-max-size=65536 + +[storage.options.overlay] +# ignore_chown_errors can be set to allow a non privileged user running with +# a single UID within a user namespace to run containers. The user can pull +# and use any image even those with multiple uids. Note multiple UIDs will be +# squashed down to the default uid in the container. These images will have no +# separation between the users in the container. Only supported for the overlay +# and vfs drivers. +#ignore_chown_errors = "false" + +# Inodes is used to set a maximum inodes of the container image. +# inodes = "" + +# Path to an helper program to use for mounting the file system instead of mounting it +# directly. +#mount_program = "/usr/bin/fuse-overlayfs" + +# mountopt specifies comma separated list of extra mount options +mountopt = "nodev,metacopy=on" + +# Set to skip a PRIVATE bind mount on the storage home directory. +# skip_mount_home = "false" + +# Set to use composefs to mount data layers with overlay. +# use_composefs = "false" + +# Size is used to set a maximum size of the container image. +# size = "" + +# ForceMask specifies the permissions mask that is used for new files and +# directories. +# +# The values "shared" and "private" are accepted. +# Octal permission masks are also accepted. +# +# "": No value specified. +# All files/directories, get set with the permissions identified within the +# image. +# "private": it is equivalent to 0700. +# All files/directories get set with 0700 permissions. The owner has rwx +# access to the files. No other users on the system can access the files. +# This setting could be used with networked based homedirs. +# "shared": it is equivalent to 0755. +# The owner has rwx access to the files and everyone else can read, access +# and execute them. This setting is useful for sharing containers storage +# with other users. For instance have a storage owned by root but shared +# to rootless users as an additional store. +# NOTE: All files within the image are made readable and executable by any +# user on the system. Even /etc/shadow within your image is now readable by +# any user. +# +# OCTAL: Users can experiment with other OCTAL Permissions. +# +# Note: The force_mask Flag is an experimental feature, it could change in the +# future. When "force_mask" is set the original permission mask is stored in +# the "user.containers.override_stat" xattr and the "mount_program" option must +# be specified. Mount programs like "/usr/bin/fuse-overlayfs" present the +# extended attribute permissions to processes within containers rather than the +# "force_mask" permissions. +# +# force_mask = "" + diff --git a/gh-podman-in-podman/uid.sh b/gh-podman-in-podman/uid.sh new file mode 100644 index 0000000..3c970c2 --- /dev/null +++ b/gh-podman-in-podman/uid.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# Ensure that dynamically-assigned uid has an entry in /etc/passwd on container startup. + +# https://docs.openshift.com/container-platform/3.3/creating_images/guidelines.html#openshift-container-platform-specific-guidelines +# https://www.openshift.com/blog/jupyter-on-openshift-part-6-running-as-an-assigned-user-id + +set -eEu + +uid=$(id -u) +gid=$(id -g) +username=${USERNAME:-default} + +echo "Current user has ID ${uid} and GID ${gid}" + +if ! whoami &> /dev/null; then + if [ -w /etc/passwd ]; then + echo "Creating passwd entry for $username" + echo "${username}:x:${uid}:0:${username} user:${HOME}:/sbin/nologin" >> /etc/passwd + echo -n "New passwd entry: " + tail -n 1 /etc/passwd + else + echo "No write permission to /etc/passwd!" 1>&2 + fi +else + echo "User already has passwd entry" +fi + +echo "whoami=$(whoami)" +echo "groups=$(groups 2>/dev/null)" + +set +x +echo "Creating sub{u,g}id entries for $username" +subuids_start=$(expr $uid + 1000) +subgids_start=$(expr $gid + 1000) + +# Do not allocate too many. +# https://github.com/containers/buildah/issues/3053 +no_subids=50000 + +echo "${username}:${subuids_start}:${no_subids}" | tee /etc/subuid +echo "${username}:${subgids_start}:${no_subids}" | tee /etc/subgid + +# set -x +# tail -n +1 /etc/sub{u,g}id + diff --git a/molecule-podman-in-podman/Containerfile b/molecule-podman-in-podman/Containerfile new file mode 100644 index 0000000..edb45bd --- /dev/null +++ b/molecule-podman-in-podman/Containerfile @@ -0,0 +1,58 @@ +FROM registry.access.redhat.com/ubi8/python-311 + +USER root + +RUN dnf -y module enable container-tools:rhel8; dnf -y update; rpm --restore --quiet shadow-utils; \ + dnf -y install crun podman netavark fuse-overlayfs openssh-clients /etc/containers/storage.conf \ + curl git jq hostname procps findutils which openssl --exclude container-selinux; \ + rm -rf /var/cache/* /var/log/dnf* /var/log/yum.* + +ENV UID=1000 +ENV GID=0 +ENV USERNAME="runner" +ARG ANSIBLE_VERSION="2.15.0" + +RUN useradd -m $USERNAME -u $UID +# This is to mimic the OpenShift behaviour of adding the dynamic user to group 0. +RUN usermod -G 0 $USERNAME +ENV HOME /home/${USERNAME} +WORKDIR /home/${USERNAME} + +RUN chmod g+w /etc/passwd && \ + touch /etc/sub{g,u}id && \ + chmod -v ug+rw /etc/sub{g,u}id && \ + echo -e "runner:1:999\nrunner:1001:64535" > /etc/subuid && \ + echo -e "runner:1:999\nrunner:1001:64535" > /etc/subgid + +ADD containers.conf /etc/containers/containers.conf +ADD podman-containers.conf /home/runner/.config/containers/containers.conf + +RUN mkdir -p /home/runner/.local/share/containers && \ + chown runner:runner -R /home/runner && \ + chmod 644 /etc/containers/containers.conf + +ADD storage.conf /usr/share/containers/storage.conf +RUN sed -e 's|^#mount_program|mount_program|g' \ + -e '/additionalimage.*/a "/var/lib/shared",' \ + -e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' \ + /usr/share/containers/storage.conf \ + > /etc/containers/storage.conf + +RUN printf '/run/secrets/etc-pki-entitlement:/run/secrets/etc-pki-entitlement\n/run/secrets/rhsm:/run/secrets/rhsm\n' > /etc/containers/mounts.conf + +VOLUME /var/lib/containers +VOLUME /home/runner/.local/share/containers + +RUN mkdir -p /var/lib/shared/overlay-images \ + /var/lib/shared/overlay-layers \ + /var/lib/shared/vfs-images \ + /var/lib/shared/vfs-layers && \ + touch /var/lib/shared/overlay-images/images.lock && \ + touch /var/lib/shared/overlay-layers/layers.lock && \ + touch /var/lib/shared/vfs-images/images.lock && \ + touch /var/lib/shared/vfs-layers/layers.lock + +ENV _CONTAINERS_USERNS_CONFIGURED="" + +RUN pip install --progress-bar off 'molecule>=24.2.0' 'molecule-plugins[docker]>=23.0.0' 'molecule-plugins[podman]' "ansible-core~=${ANSIBLE_VERSION}" + diff --git a/molecule-podman-in-podman/README.md b/molecule-podman-in-podman/README.md new file mode 100644 index 0000000..957fc9c --- /dev/null +++ b/molecule-podman-in-podman/README.md @@ -0,0 +1,20 @@ +## podman on podman on rhel + +This container is based on the python 3.11 avriant of ubi8 and id meant to run other podman containers inside it (specifically, molecule containers). + +To build: + + podman build -f Containerfile --build-arg ANSIBLE_VERSION=2.15.0 -t + +The outer container is proviliged and needs to run as root with: + + podman run --name=runner --replace --security-opt label=disable --security-opt seccomp=unconfined --device /dev/fuse:rw -v /var/lib/runner:/var/lib/containers:Z --privileged -ti --rm + +Each outer container needs its own `/var/lib/runner` (world-writable) directory on the host to contain overlays and images. + +In the outer container `/var/lib/shared` contains the files for the inner containers. + +When inside the outer container, as the user `runner`, you can directly run `molecule` + +Inner containers will only be visible in the outer container that created them. +Outer containers are visible to the host, while inner containers are not. diff --git a/molecule-podman-in-podman/containers.conf b/molecule-podman-in-podman/containers.conf new file mode 100644 index 0000000..c04303b --- /dev/null +++ b/molecule-podman-in-podman/containers.conf @@ -0,0 +1,13 @@ +[containers] +netns="host" +userns="host" +ipcns="host" +utsns="host" +cgroupns="host" +cgroups="disabled" +log_driver = "k8s-file" +[engine] +cgroup_manager = "cgroupfs" +events_logger="file" +runtime="crun" + diff --git a/molecule-podman-in-podman/podman-containers.conf b/molecule-podman-in-podman/podman-containers.conf new file mode 100644 index 0000000..53c7e79 --- /dev/null +++ b/molecule-podman-in-podman/podman-containers.conf @@ -0,0 +1,6 @@ +[containers] +volumes = [ + "/proc:/proc", +] +default_sysctls = [] + diff --git a/molecule-podman-in-podman/storage.conf b/molecule-podman-in-podman/storage.conf new file mode 100644 index 0000000..5c0b66f --- /dev/null +++ b/molecule-podman-in-podman/storage.conf @@ -0,0 +1,158 @@ +# This file is the configuration file for all tools +# that use the containers/storage library. The storage.conf file +# overrides all other storage.conf files. Container engines using the +# container/storage library do not inherit fields from other storage.conf +# files. +# +# Note: The storage.conf file overrides other storage.conf files based on this precedence: +# /usr/containers/storage.conf +# /etc/containers/storage.conf +# $HOME/.config/containers/storage.conf +# $XDG_CONFIG_HOME/containers/storage.conf (If XDG_CONFIG_HOME is set) +# See man 5 containers-storage.conf for more information +# The "container storage" table contains all of the server options. +[storage] + +# Default Storage Driver, Must be set for proper operation. +driver = "overlay" + +# Temporary storage location +runroot = "/run/containers/storage" + +# Priority list for the storage drivers that will be tested one +# after the other to pick the storage driver if it is not defined. +# driver_priority = ["overlay", "btrfs"] + +# Primary Read/Write location of container storage +# When changing the graphroot location on an SELINUX system, you must +# ensure the labeling matches the default locations labels with the +# following commands: +# semanage fcontext -a -e /var/lib/containers/storage /NEWSTORAGEPATH +# restorecon -R -v /NEWSTORAGEPATH +graphroot = "/var/lib/containers/storage" + +# Optional alternate location of image store if a location separate from the +# container store is required. If set, it must be different than graphroot. +# imagestore = "" + + +# Storage path for rootless users +# +# rootless_storage_path = "$HOME/.local/share/containers/storage" + +# Transient store mode makes all container metadata be saved in temporary storage +# (i.e. runroot above). This is faster, but doesn't persist across reboots. +# Additional garbage collection must also be performed at boot-time, so this +# option should remain disabled in most configurations. +# transient_store = true + +[storage.options] +# Storage options to be passed to underlying storage drivers + +# AdditionalImageStores is used to pass paths to additional Read/Only image stores +# Must be comma separated list. +additionalimagestores = [ +] + +# Allows specification of how storage is populated when pulling images. This +# option can speed the pulling process of images compressed with format +# zstd:chunked. Containers/storage looks for files within images that are being +# pulled from a container registry that were previously pulled to the host. It +# can copy or create a hard link to the existing file when it finds them, +# eliminating the need to pull them from the container registry. These options +# can deduplicate pulling of content, disk storage of content and can allow the +# kernel to use less memory when running containers. + +# containers/storage supports four keys +# * enable_partial_images="true" | "false" +# Tells containers/storage to look for files previously pulled in storage +# rather then always pulling them from the container registry. +# * use_hard_links = "false" | "true" +# Tells containers/storage to use hard links rather then create new files in +# the image, if an identical file already existed in storage. +# * ostree_repos = "" +# Tells containers/storage where an ostree repository exists that might have +# previously pulled content which can be used when attempting to avoid +# pulling content from the container registry +# * convert_images = "false" | "true" +# If set to true, containers/storage will convert images to a +# format compatible with partial pulls in order to take advantage +# of local deduplication and hard linking. It is an expensive +# operation so it is not enabled by default. +pull_options = {enable_partial_images = "true", use_hard_links = "false", ostree_repos=""} + +# Root-auto-userns-user is a user name which can be used to look up one or more UID/GID +# ranges in the /etc/subuid and /etc/subgid file. These ranges will be partitioned +# to containers configured to create automatically a user namespace. Containers +# configured to automatically create a user namespace can still overlap with containers +# having an explicit mapping set. +# This setting is ignored when running as rootless. +# root-auto-userns-user = "storage" +# +# Auto-userns-min-size is the minimum size for a user namespace created automatically. +# auto-userns-min-size=1024 +# +# Auto-userns-max-size is the maximum size for a user namespace created automatically. +# auto-userns-max-size=65536 + +[storage.options.overlay] +# ignore_chown_errors can be set to allow a non privileged user running with +# a single UID within a user namespace to run containers. The user can pull +# and use any image even those with multiple uids. Note multiple UIDs will be +# squashed down to the default uid in the container. These images will have no +# separation between the users in the container. Only supported for the overlay +# and vfs drivers. +#ignore_chown_errors = "false" + +# Inodes is used to set a maximum inodes of the container image. +# inodes = "" + +# Path to an helper program to use for mounting the file system instead of mounting it +# directly. +#mount_program = "/usr/bin/fuse-overlayfs" + +# mountopt specifies comma separated list of extra mount options +mountopt = "nodev,metacopy=on" + +# Set to skip a PRIVATE bind mount on the storage home directory. +# skip_mount_home = "false" + +# Set to use composefs to mount data layers with overlay. +# use_composefs = "false" + +# Size is used to set a maximum size of the container image. +# size = "" + +# ForceMask specifies the permissions mask that is used for new files and +# directories. +# +# The values "shared" and "private" are accepted. +# Octal permission masks are also accepted. +# +# "": No value specified. +# All files/directories, get set with the permissions identified within the +# image. +# "private": it is equivalent to 0700. +# All files/directories get set with 0700 permissions. The owner has rwx +# access to the files. No other users on the system can access the files. +# This setting could be used with networked based homedirs. +# "shared": it is equivalent to 0755. +# The owner has rwx access to the files and everyone else can read, access +# and execute them. This setting is useful for sharing containers storage +# with other users. For instance have a storage owned by root but shared +# to rootless users as an additional store. +# NOTE: All files within the image are made readable and executable by any +# user on the system. Even /etc/shadow within your image is now readable by +# any user. +# +# OCTAL: Users can experiment with other OCTAL Permissions. +# +# Note: The force_mask Flag is an experimental feature, it could change in the +# future. When "force_mask" is set the original permission mask is stored in +# the "user.containers.override_stat" xattr and the "mount_program" option must +# be specified. Mount programs like "/usr/bin/fuse-overlayfs" present the +# extended attribute permissions to processes within containers rather than the +# "force_mask" permissions. +# +# force_mask = "" +