From 62aa337f17cf6d011d83adc583f77613193c45fe Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 21 Jul 2021 09:22:07 -0700 Subject: [PATCH] feat(secrets): :sparkles: Add support for secrets Adds support for - - (1) Declared secrets with the file location. - (2) Declared secrets with file location, mounted as a different named secret. - (3) Declared secrets with file location, mounted at arbitrary location. - (4) External secrets (type=mount), mounted as original secret name. - (5) External secrets (type=mount), mounted as original secret name, with specified uid, gid and mode. --- podman_compose.py | 73 ++++++++++++++++++- .../bad_external_name/docker-compose.yaml | 18 +++++ .../bad_external_target/docker-compose.yaml | 18 +++++ tests/secrets/docker-compose.yaml | 42 +++++++++++ tests/secrets/my_secret | 1 + tests/secrets/print_secrets.sh | 6 ++ 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 tests/secrets/bad_external_name/docker-compose.yaml create mode 100644 tests/secrets/bad_external_target/docker-compose.yaml create mode 100644 tests/secrets/docker-compose.yaml create mode 100644 tests/secrets/my_secret create mode 100755 tests/secrets/print_secrets.sh diff --git a/podman_compose.py b/podman_compose.py index 68f47b2..e365369 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -513,6 +513,74 @@ def get_mount_args(compose, cnt, volume): args = mount_desc_to_mount_args(compose, volume, srv_name, cnt['name']) return ['--mount', args] + +def get_secret_args(compose, cnt, secret): + 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( + 'ERROR: undeclared secret: "{}", service: "{}"' + .format(secret, cnt['_service']) + ) + declared_secret = compose.declared_secrets[secret_name] + + source_file = declared_secret.get('file', None) + dest_file = '' + secret_opts = '' + + target = None if is_str(secret) else secret.get('target', None) + uid = None if is_str(secret) else secret.get('uid', None) + gid = None if is_str(secret) else secret.get('gid', None) + mode = None if is_str(secret) else secret.get('mode', None) + + if source_file: + if not target: + dest_file = '/run/secrets/{}'.format(secret_name) + elif not target.startswith("/"): + dest_file = '/run/secrets/{}'.format(target if target else secret_name) + else: + dest_file = target + volume_ref = [ + '--volume', '{}:{}:ro,rprivate,rbind'.format(source_file, dest_file) + ] + if uid or gid or mode: + print( + 'WARNING: Service "{}" uses secret "{}" with uid, gid, or mode.' + .format(cnt['_service'], target if target else secret_name) + + ' These fields are not supported by this implementation of the Compose file' + ) + return volume_ref + # v3.5 and up added external flag, earlier the spec + # only required a name to be specified. + # docker-compose does not support external secrets outside of swarm mode. + # However accessing these via podman is trivial + # since these commands are directly translated to + # podman-create commands, albiet we can only support a 1:1 mapping + # at the moment + if declared_secret.get('external', False) or declared_secret.get('name', None): + secret_opts += ',uid={}'.format(uid) if uid else '' + secret_opts += ',gid={}'.format(gid) if gid else '' + secret_opts += ',mode={}'.format(mode) if mode else '' + # The target option is only valid for type=env, + # which in an ideal world would work + # for type=mount as well. + # having a custom name for the external secret + # has the same problem as well + ext_name = declared_secret.get('name', None) + err_str = 'ERROR: Custom name/target reference "{}" for mounted external secret "{}" is not supported' + if ext_name and ext_name != secret_name: + raise ValueError(err_str.format(secret_name, ext_name)) + elif target and target != secret_name: + raise ValueError(err_str.format(target, secret_name)) + elif target: + print('WARNING: Service "{}" uses target: "{}" for secret: "{}".' + .format(cnt['_service'], target, secret_name) + + ' That is un-supported and a no-op and is ignored.') + return [ '--secret', '{}{}'.format(secret_name, secret_opts) ] + + raise ValueError('ERROR: unparseable secret: "{}", service: "{}"' + .format(secret_name, cnt['_service'])) + + def container_to_res_args(cnt, podman_args): # v2 < https://docs.docker.com/compose/compose-file/compose-file-v2/#cpu-and-other-resources cpus_limit_v2 = try_float(cnt.get('cpus', None), None) @@ -587,6 +655,8 @@ def container_to_args(compose, cnt, detached=True): for volume in cnt.get('volumes', []): # TODO: should we make it os.path.realpath(os.path.join(, i))? podman_args.extend(get_mount_args(compose, cnt, volume)) + for secret in cnt.get('secrets', []): + podman_args.extend(get_secret_args(compose, cnt, secret)) for i in cnt.get('extra_hosts', []): podman_args.extend(['--add-host', i]) for i in cnt.get('expose', []): @@ -850,6 +920,7 @@ class PodmanCompose: self.pods = None self.containers = None self.shared_vols = None + self.declared_secrets = None self.container_names_by_service = None self.container_by_name = None self._prefer_volume_over_mount = True @@ -1006,7 +1077,7 @@ class PodmanCompose: # other top-levels: # networks: {driver: ...} # configs: {...} - # secrets: {...} + self.declared_secrets = compose.get('secrets', {}) given_containers = [] container_names_by_service = {} for service_name, service_desc in services.items(): diff --git a/tests/secrets/bad_external_name/docker-compose.yaml b/tests/secrets/bad_external_name/docker-compose.yaml new file mode 100644 index 0000000..3e64a7b --- /dev/null +++ b/tests/secrets/bad_external_name/docker-compose.yaml @@ -0,0 +1,18 @@ +version: "3.8" +services: + test: + image: busybox + command: + - cat + - /run/secrets/new_secret + tmpfs: + - /run + - /tmp + secrets: + - new_secret + +secrets: + new_secret: + external: true + name: my_secret + diff --git a/tests/secrets/bad_external_target/docker-compose.yaml b/tests/secrets/bad_external_target/docker-compose.yaml new file mode 100644 index 0000000..80e5cef --- /dev/null +++ b/tests/secrets/bad_external_target/docker-compose.yaml @@ -0,0 +1,18 @@ +version: "3.8" +services: + test: + image: busybox + command: + - cat + - /run/secrets/my_secret_2 + tmpfs: + - /run + - /tmp + secrets: + - source: my_secret + target: new_secret + +secrets: + my_secret: + external: true + diff --git a/tests/secrets/docker-compose.yaml b/tests/secrets/docker-compose.yaml new file mode 100644 index 0000000..4423dcb --- /dev/null +++ b/tests/secrets/docker-compose.yaml @@ -0,0 +1,42 @@ +version: "3.8" +services: + test: + image: busybox + command: + - /tmp/print_secrets.sh + tmpfs: + - /run + - /tmp + volumes: + - ./print_secrets.sh:/tmp/print_secrets.sh + secrets: + - my_secret + - my_secret_2 + - source: my_secret_3 + target: my_secret_3 + uid: '103' + gid: '103' + mode: 400 + - file_secret + - source: file_secret + target: custom_name + - source: file_secret + target: /etc/custom_location + - source: file_secret + target: unused_params_warning + uid: '103' + gid: '103' + mode: 400 + +secrets: + my_secret: + external: true + my_secret_2: + external: true + name: my_secret_2 + my_secret_3: + external: true + name: my_secret_3 + file_secret: + file: ./my_secret + diff --git a/tests/secrets/my_secret b/tests/secrets/my_secret new file mode 100644 index 0000000..235fe34 --- /dev/null +++ b/tests/secrets/my_secret @@ -0,0 +1 @@ +important-secret-is-important diff --git a/tests/secrets/print_secrets.sh b/tests/secrets/print_secrets.sh new file mode 100755 index 0000000..7115716 --- /dev/null +++ b/tests/secrets/print_secrets.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +ls -la /run/secrets/* +ls -la /etc/custom_location +cat /run/secrets/* +cat /etc/custom_location