Skip to content

Commit

Permalink
Pass file secrets to "podman build" via parameter "--secret"
Browse files Browse the repository at this point in the history
to make them available for "RUN --mount=type=secret" statements inside the
Dockerfile.

Keep using --volume to pass file secrets to "podman run".

Signed-off-by: wiehe <[email protected]>
  • Loading branch information
wiehe committed Mar 11, 2024
1 parent abb0cb1 commit aa48928
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 10 deletions.
41 changes: 31 additions & 10 deletions podman_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,11 @@ async def get_mount_args(compose, cnt, volume):
return ["--mount", args]


def get_secret_args(compose, cnt, secret):
def get_secret_args(compose, cnt, secret, podman_is_building=False):
"""
podman_is_building: True if we are preparing arguments for an invocation of "podman build"
False if we are preparing for something else like "podman run"
"""
secret_name = secret if is_str(secret) else secret.get("source", None)
if not secret_name or secret_name not in compose.declared_secrets.keys():
raise ValueError(f'ERROR: undeclared secret: "{secret}", service: {cnt["_service"]}')
Expand All @@ -543,16 +547,33 @@ def get_secret_args(compose, cnt, secret):
mode = None if is_str(secret) else secret.get("mode", None)

if source_file:
if not target:
dest_file = f"/run/secrets/{secret_name}"
elif not target.startswith("/"):
sec = target if target else secret_name
dest_file = f"/run/secrets/{sec}"
else:
dest_file = target
# assemble path for source file first, because we need it for all cases
basedir = compose.dirname
source_file = os.path.realpath(os.path.join(basedir, os.path.expanduser(source_file)))
volume_ref = ["--volume", f"{source_file}:{dest_file}:ro,rprivate,rbind"]

if podman_is_building:
# pass file secrets to "podman build" with param --secret
if not target:
secret_id = secret_name
elif "/" in target:
raise ValueError(
f'ERROR: Build secret "{secret_name}" has invalid target "{target}". '
+ "(Expected plain filename without directory as target.)"
)
else:
secret_id = target
volume_ref = ["--secret", f"id={secret_id},src={source_file}"]
else:
# pass file secrets to "podman run" as volumes
if not target:
dest_file = f"/run/secrets/{secret_name}"
elif not target.startswith("/"):
sec = target if target else secret_name
dest_file = f"/run/secrets/{sec}"
else:
dest_file = target
volume_ref = ["--volume", f"{source_file}:{dest_file}:ro,rprivate,rbind"]

if uid or gid or mode:
sec = target if target else secret_name
log.warning(
Expand Down Expand Up @@ -2140,7 +2161,7 @@ async def build_one(compose, args, cnt):
raise OSError("Dockerfile not found in " + ctx)
build_args = ["-f", dockerfile, "-t", cnt["image"]]
for secret in build_desc.get("secrets", []):
build_args.extend(get_secret_args(compose, cnt, secret))
build_args.extend(get_secret_args(compose, cnt, secret, podman_is_building=True))
for tag in build_desc.get("tags", []):
build_args.extend(["-t", tag])
if "target" in build_desc:
Expand Down
9 changes: 9 additions & 0 deletions tests/build_secrets/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM busybox

RUN --mount=type=secret,required=true,id=build_secret \
ls -l /run/secrets/ && cat /run/secrets/build_secret

RUN --mount=type=secret,required=true,id=build_secret,target=/tmp/secret \
ls -l /run/secrets/ /tmp/ && cat /tmp/secret

CMD [ 'echo', 'nothing here' ]
22 changes: 22 additions & 0 deletions tests/build_secrets/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: "3.8"

services:
test:
image: test
secrets:
- run_secret # implicitly mount to /run/secrets/run_secret
- source: run_secret
target: /tmp/run_secret2 # explicit mount point

build:
context: .
secrets:
- build_secret # can be mounted in Dockerfile with "RUN --mount=type=secret,id=build_secret"
- source: build_secret
target: build_secret2 # rename to build_secret2

secrets:
build_secret:
file: ./my_secret
run_secret:
file: ./my_secret
18 changes: 18 additions & 0 deletions tests/build_secrets/docker-compose.yaml.invalid
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: "3.8"

services:
test:
image: test
build:
context: .
secrets:
# invalid target argument
#
# According to https://github.com/compose-spec/compose-spec/blob/master/build.md, target is
# supposed to be the "name of a *file* to be mounted in /run/secrets/". Not a path.
- source: build_secret
target: /build_secret

secrets:
build_secret:
file: ./my_secret
1 change: 1 addition & 0 deletions tests/build_secrets/my_secret
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
important-secret-is-important
90 changes: 90 additions & 0 deletions tests/test_podman_compose_build_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SPDX-License-Identifier: GPL-2.0


"""Test how secrets in files are passed to podman."""

import os
import subprocess
import unittest

from .test_podman_compose import podman_compose_path
from .test_podman_compose import test_path


def compose_yaml_path():
""" "Returns the path to the compose file used for this test module"""
return os.path.join(test_path(), "build_secrets")


class TestComposeBuildSecrets(unittest.TestCase):
def test_run_secret(self):
"""podman run should receive file secrets as --volume
See build_secrets/docker-compose.yaml for secret names and mount points (aka targets)
"""
cmd = (
"coverage",
"run",
podman_compose_path(),
"--dry-run",
"--verbose",
"-f",
os.path.join(compose_yaml_path(), "docker-compose.yaml"),
"run",
"test",
)
p = subprocess.run(
cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True
)
self.assertEqual(p.returncode, 0)
secret_path = os.path.join(compose_yaml_path(), "my_secret")
self.assertIn(f"--volume {secret_path}:/run/secrets/run_secret:ro,rprivate,rbind", p.stdout)
self.assertIn(f"--volume {secret_path}:/tmp/run_secret2:ro,rprivate,rbind", p.stdout)

def test_build_secret(self):
"""podman build should receive secrets as --secret, so that they can be used inside the
Dockerfile in "RUN --mount=type=secret ..." commands.
"""
cmd = (
"coverage",
"run",
podman_compose_path(),
"--dry-run",
"--verbose",
"-f",
os.path.join(compose_yaml_path(), "docker-compose.yaml"),
"build",
)
p = subprocess.run(
cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True
)
self.assertEqual(p.returncode, 0)
secret_path = os.path.join(compose_yaml_path(), "my_secret")
self.assertIn(f"--secret id=build_secret,src={secret_path}", p.stdout)
self.assertIn(f"--secret id=build_secret2,src={secret_path}", p.stdout)

def test_invalid_build_secret(self):
"""build secrets in docker-compose file can only have a target argument without directory
component
"""
cmd = (
"coverage",
"run",
podman_compose_path(),
"--dry-run",
"--verbose",
"-f",
os.path.join(compose_yaml_path(), "docker-compose.yaml.invalid"),
"build",
)
p = subprocess.run(
cmd, stdout=subprocess.PIPE, check=False, stderr=subprocess.STDOUT, text=True
)
self.assertNotEqual(p.returncode, 0)
self.assertIn(
'ValueError: ERROR: Build secret "build_secret" has invalid target "/build_secret"',
p.stdout,
)

0 comments on commit aa48928

Please sign in to comment.