diff --git a/doc/toolbox-create.1.md b/doc/toolbox-create.1.md index 19cf305b3..efd604579 100644 --- a/doc/toolbox-create.1.md +++ b/doc/toolbox-create.1.md @@ -4,7 +4,8 @@ toolbox\-create - Create a new toolbox container ## SYNOPSIS -**toolbox create** [*--distro DISTRO* | *-d DISTRO*] +**toolbox create** [*--authfile AUTHFILE*] + [*--distro DISTRO* | *-d DISTRO*] [*--image NAME* | *-i NAME*] [*--release RELEASE* | *-r RELEASE*] [*CONTAINER*] @@ -79,6 +80,14 @@ confusion. ## OPTIONS ## +**--authfile** AUTHFILE + +Path to a file used for registry authentication. Exposes option `--authfile` of +command `podman pull` that is used internally. + +The default is `${XDG_RUNTIME_DIR}/containers/auth.json`, which is set using +`podman login`. + **--distro** DISTRO, **-d** DISTRO Create a toolbox container for a different operating system DISTRO than the @@ -119,6 +128,12 @@ $ toolbox create --distro fedora --release f30 $ toolbox create --image bar foo ``` +### Create a toolbox container using an image from a registry with authentication + +``` +$ toolbox create --authfile $HOME/auth.json --image quay.io/foo/bar +``` + ## SEE ALSO -`toolbox(1)`, `toolbox-init-container(1)`, `podman(1)`, `podman-create(1)` +`toolbox(1)`, `toolbox-init-container(1)`, `podman(1)`, `podman-create(1)`, `podman-login(1)`, `podman-pull(1)` diff --git a/playbooks/setup-env.yaml b/playbooks/setup-env.yaml index 28d7695c9..20a33f843 100644 --- a/playbooks/setup-env.yaml +++ b/playbooks/setup-env.yaml @@ -12,8 +12,10 @@ - flatpak-session-helper - golang - golang-github-cpuguy83-md2man + - httpd-tools - meson - ninja-build + - openssl - patchelf - podman - skopeo diff --git a/src/cmd/create.go b/src/cmd/create.go index bac340296..4815778c3 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -42,6 +42,7 @@ const ( var ( createFlags struct { + authFile string container string distro string image string @@ -66,6 +67,11 @@ var createCmd = &cobra.Command{ func init() { flags := createCmd.Flags() + flags.StringVar(&createFlags.authFile, + "authfile", + "", + "Path to a file used for registry authentication") + flags.StringVarP(&createFlags.container, "container", "c", @@ -115,6 +121,12 @@ func create(cmd *cobra.Command, args []string) error { return errors.New("options --image and --release cannot be used together") } + if cmd.Flag("authfile").Changed { + if !utils.PathExists(createFlags.authFile) { + return fmt.Errorf("file %s does not exist", createFlags.authFile) + } + } + var container string var containerArg string @@ -727,7 +739,7 @@ func pullImage(image, release string) (bool, error) { defer s.Stop() } - if err := podman.Pull(imageFull); err != nil { + if err := podman.Pull(imageFull, createFlags.authFile); err != nil { var builder strings.Builder fmt.Fprintf(&builder, "failed to pull image %s\n", imageFull) fmt.Fprintf(&builder, "If it was a private image, log in with: podman login %s\n", domain) diff --git a/src/pkg/podman/podman.go b/src/pkg/podman/podman.go index 9099df1ea..e783837e0 100644 --- a/src/pkg/podman/podman.go +++ b/src/pkg/podman/podman.go @@ -227,9 +227,16 @@ func IsToolboxImage(image string) (bool, error) { } // Pull pulls an image -func Pull(imageName string) error { +// +// authfile is a path to a JSON authentication file and is internally used only +// if it is not an empty string. The file at the path should exist. +func Pull(imageName string, authfile string) error { logLevelString := LogLevel.String() - args := []string{"--log-level", logLevelString, "pull", imageName} + args := []string{"--log-level", logLevelString, "pull"} + if authfile != "" { + args = append(args, []string{"--authfile", authfile}...) + } + args = append(args, imageName) if err := shell.Run("podman", nil, nil, nil, args...); err != nil { return err diff --git a/test/system/000-setup.bats b/test/system/000-setup.bats index 8259c32de..f0fcce831 100644 --- a/test/system/000-setup.bats +++ b/test/system/000-setup.bats @@ -9,4 +9,6 @@ load 'libs/helpers' # Cache all images that will be needed during the tests _pull_and_cache_distro_image fedora 32 || die _pull_and_cache_distro_image busybox || die + + _setup_docker_registry } diff --git a/test/system/101-create.bats b/test/system/101-create.bats index 0e06e3a65..525b30c99 100644 --- a/test/system/101-create.bats +++ b/test/system/101-create.bats @@ -79,3 +79,35 @@ teardown() { assert_line --index 1 "If it was a private image, log in with: podman login foo.org" assert_line --index 2 "Use 'toolbox --verbose ...' for further details." } + +@test "create: Try to create a container and pass a non-existent file to the --authfile option" { + local file="$BATS_RUN_TMPDIR/non-existent-file" + + run $TOOLBOX create --authfile "$file" + + assert_failure + assert_output "Error: file $file does not exist" +} + +@test "create: Create a container based on an image from locked registry using an authentication file" { + local authfile="$BATS_RUN_TMPDIR/authfile" + local image="fedora-toolbox:32" + + run $PODMAN login --authfile "$authfile" --username user --password user "$DOCKER_REG_URI" + assert_success + + run $TOOLBOX -y create -i "$DOCKER_REG_URI/$image" + + assert_failure + assert_line --index 0 "Error: failed to pull image $DOCKER_REG_URI/$image" + assert_line --index 1 "If it was a private image, log in with: podman login $DOCKER_REG_URI" + assert_line --index 2 "Use 'toolbox --verbose ...' for further details." + + run $TOOLBOX -y create -i "$DOCKER_REG_URI/$image" --authfile "$authfile" + + assert_success + assert_output "Created container: fedora-toolbox-32" + assert_output "Enter with: toolbox enter fedora-toolbox-32" + + rm "$authfile" +} diff --git a/test/system/999-teardown.bats b/test/system/999-teardown.bats index 5e2aaef11..c4f2742e7 100644 --- a/test/system/999-teardown.bats +++ b/test/system/999-teardown.bats @@ -4,5 +4,6 @@ load 'libs/helpers' @test "test suite: Teardown" { _clean_cached_images + _clean_docker_registry _clean_temporary_storage } diff --git a/test/system/README.md b/test/system/README.md index f7398b089..734648ada 100644 --- a/test/system/README.md +++ b/test/system/README.md @@ -13,6 +13,8 @@ Running them won't remove any existing containers or images. - `awk` - `bats` - `GNU coreutils` +- `httpd-tools` +- `openssl` - `podman` - `skopeo` - `toolbox` diff --git a/test/system/libs/helpers.bash b/test/system/libs/helpers.bash index 92dfe5b20..526ea7fcb 100644 --- a/test/system/libs/helpers.bash +++ b/test/system/libs/helpers.bash @@ -10,6 +10,13 @@ readonly IMAGE_CACHE_DIR="${BATS_RUN_TMPDIR}/image-cache" readonly ROOTLESS_PODMAN_STORE_DIR="${TEMP_STORAGE_DIR}/storage" readonly PODMAN_STORE_CONFIG_FILE="${TEMP_STORAGE_DIR}/store.conf" +readonly DOCKER_REG_ROOT="${TEMP_STORAGE_DIR}/podman-registry-root" +readonly DOCKER_REG_DIR="${TEMP_STORAGE_DIR}/docker-registry" +readonly DOCKER_REG_CERTS_DIR="${BATS_RUN_TMPDIR}/certs" +readonly DOCKER_REG_AUTH_DIR="${BATS_RUN_TMPDIR}/auth" +readonly DOCKER_REG_URI="localhost:50000" +readonly DOCKER_REG_NAME="docker-registry" + # Podman and Toolbox commands to run readonly PODMAN=${PODMAN:-podman} readonly TOOLBOX=${TOOLBOX:-toolbox} @@ -17,6 +24,7 @@ readonly SKOPEO=$(command -v skopeo) # Images declare -Ag IMAGES=([busybox]="quay.io/toolbox_tests/busybox" \ + [docker-reg]="quay.io/toolbox_tests/registry" \ [fedora]="registry.fedoraproject.org/fedora-toolbox" \ [rhel]="registry.access.redhat.com/ubi8") @@ -36,6 +44,7 @@ function _setup_environment() { check_xdg_runtime_dir } + function _setup_containers_store() { mkdir -p ${TEMP_STORAGE_DIR} # Setup a storage config file for PODMAN @@ -119,6 +128,86 @@ function _clean_cached_images() { } +# Prepares a localy hosted image registry +# +# The registry is set up with Podman set to an alternative root. It won't +# affect other containers or images in the default root. +# +# Instructions taken from https://docs.docker.com/registry/deploying/ +function _setup_docker_registry() { + # Create certificates for HTTPS + # This is needed so that Podman does not have to be configured to work with + # HTTP-only registries + run mkdir -p "${DOCKER_REG_CERTS_DIR}" + assert_success + run openssl req \ + -newkey rsa:4096 + -nodes -sha256 + -keyout "${DOCKER_REG_CERTS_DIR}/domain.key" \ + -addext "subjectAltName= DNS:localhost" \ + -x509 + -days 365 + -subj '/' \ + -out "${DOCKER_REG_CERTS_DIR}/domain.crt" + assert_success + + # Add certificate to Podman's trusted certificates (rootless) + run mkdir -p "~/.config/containers/certs.d/${DOCKER_REG_URI}" + assert_success + run cp "${DOCKER_REG_CERTS_DIR}/domain.crt" "~/.config/containers/certs.d/${DOCKER_REG_URI}" + assert_success + + # Create a registry user + # username: user; password: user + run mkdir -p "${DOCKER_REG_AUTH_DIR}" + assert_success + run htpasswd -Bbc "${DOCKER_REG_AUTH_DIR}/htpasswd" user user + assert_success + + # Create separate Podman root + run mkdir -p "${DOCKER_REG_ROOT}" + assert_success + + # Pull Docker registry image + run $PODMAN --root "${DOCKER_REG_ROOT}" pull IMAGES[docker-reg] + assert_success + + # Create a Docker registry + run $PODMAN --root "${DOCKER_REG_ROOT}" run -d \ + --rm \ + --name "${DOCKER_REG_NAME}" \ + --privileged \ + -v "${DOCKER_REG_AUTH_DIR}:/auth" \ + -e REGISTRY_AUTH=htpasswd \ + -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \ + -e REGISTRY_AUTH_HTPASSWD_PATH="/auth/htpasswd" \ + -v "${DOCKER_REG_CERTS_DIR}":/certs \ + -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \ + -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \ + -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \ + -p 50000:443 \ + IMAGES[docker-reg] + assert_success + + # Add fedora-toolbox:32 image to the registry + run $SKOPEO copy "dir:${IMAGE_CACHE_DIR}/fedora-toolbox-32" "docker://${DOCKER_REG_URI}/fedora-toolbox-32" + assert_success +} + + +# Stop, removes and cleans after a locally hosted Docker registry +function _clean_docker_registry() { + # Stop Docker registry container + podman --root "${DOCKER_REG_ROOT}" stop "${DOCKER_REG_NAME}" + # Remove Docker registry dirs + rm -rf "${DOCKER_REG_ROOT}" + rm -rf "${DOCKER_REG_DIR}" + # Remove dir with created registry certificates + rm -rf "~/.config/containers/certs.d/${DOCKER_REG_URI}" + +} + + # Copies an image from local storage to Podman's image store # # Call before creating any container. Network failures are not nice.