From ee9fdc93a5e1295ba956002f06cfdca22af73823 Mon Sep 17 00:00:00 2001 From: Kiara Grouwstra Date: Thu, 1 Aug 2024 21:14:59 +0000 Subject: [PATCH] add support for docker compose secrets enables using [docker compose secrets](https://docs.docker.com/compose/use-secrets/) from arion, which includes: - [top-level `secrets` element](https://docs.docker.com/compose/compose-file/09-secrets/) defining the secrets to be used for the below two use-cases, exposing them at `/run/secrets/`. comes in flavors `file` vs `environment`. - run-time: [`services` top-level `secrets` element](https://docs.docker.com/compose/compose-file/05-services/#secrets) - build time: [build secrets](https://docs.docker.com/build/building/secrets/) (to be [mounted](https://docs.docker.com/build/building/secrets/#secret-mounts) in the `Dockerfile` like `RUN --mount=type=secret,id= ...`) unlike #52, i did not so far add support for their [long syntax](https://docs.docker.com/compose/compose-file/05-services/#long-syntax-4), which despite the confusing documentation appears [limited to Docker Swarm](https://github.com/docker/compose/issues/9648#issuecomment-1380290233), in my understanding limiting its use in Arion. --- .../testdata/Arion/NixSpec/arion-compose.json | 1 + .../Arion/NixSpec/arion-context-compose.json | 10 ++- .../Arion/NixSpec/arion-context-compose.nix | 2 + src/nix/lib.nix | 4 + src/nix/modules.nix | 3 +- .../modules/composition/docker-compose.nix | 9 +++ src/nix/modules/composition/secrets.nix | 33 +++++++++ src/nix/modules/secrets/secret.nix | 55 ++++++++++++++ .../service/docker-compose-service.nix | 73 +++++++++++++------ 9 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 src/nix/modules/composition/secrets.nix create mode 100644 src/nix/modules/secrets/secret.nix diff --git a/src/haskell/testdata/Arion/NixSpec/arion-compose.json b/src/haskell/testdata/Arion/NixSpec/arion-compose.json index 64b8169..57c19de 100644 --- a/src/haskell/testdata/Arion/NixSpec/arion-compose.json +++ b/src/haskell/testdata/Arion/NixSpec/arion-compose.json @@ -4,6 +4,7 @@ "name": "unit-test-data" } }, + "secrets": {}, "services": { "webserver": { "command": [ diff --git a/src/haskell/testdata/Arion/NixSpec/arion-context-compose.json b/src/haskell/testdata/Arion/NixSpec/arion-context-compose.json index ae67f53..b26ccf7 100644 --- a/src/haskell/testdata/Arion/NixSpec/arion-context-compose.json +++ b/src/haskell/testdata/Arion/NixSpec/arion-context-compose.json @@ -4,10 +4,18 @@ "name": "unit-test-data" } }, + "secrets": { + "foo": { + "environment": "FOO" + } + }, "services": { "webserver": { "build": { - "context": "" + "context": "", + "secrets": [ + "foo" + ] }, "environment": {}, "ports": [ diff --git a/src/haskell/testdata/Arion/NixSpec/arion-context-compose.nix b/src/haskell/testdata/Arion/NixSpec/arion-context-compose.nix index a4c5b7c..2f85c6d 100644 --- a/src/haskell/testdata/Arion/NixSpec/arion-context-compose.nix +++ b/src/haskell/testdata/Arion/NixSpec/arion-context-compose.nix @@ -2,8 +2,10 @@ project.name = "unit-test-data"; services.webserver.service = { build.context = "${./build-context}"; + build.secrets = ["foo"]; ports = [ "8080:80" ]; }; + secrets.foo.environment = "FOO"; } diff --git a/src/nix/lib.nix b/src/nix/lib.nix index b7aab59..4266a04 100644 --- a/src/nix/lib.nix +++ b/src/nix/lib.nix @@ -11,11 +11,15 @@ let networkRef = fragment: ''See ${link "https://github.com/compose-spec/compose-spec/blob/${composeSpecRev}/06-networks.md#${fragment}" "Compose Spec Networks #${fragment}"}''; + secretRef = fragment: + ''See ${link "https://github.com/compose-spec/compose-spec/blob/${composeSpecRev}/09-secrets.md#${fragment}" "Compose Spec Secrets #${fragment}"}''; + in { inherit link networkRef serviceRef + secretRef ; } diff --git a/src/nix/modules.nix b/src/nix/modules.nix index 8c3251a..de2ad40 100644 --- a/src/nix/modules.nix +++ b/src/nix/modules.nix @@ -3,6 +3,7 @@ ./modules/composition/host-environment.nix ./modules/composition/images.nix ./modules/composition/networks.nix + ./modules/composition/secrets.nix ./modules/composition/service-info.nix ./modules/composition/composition.nix -] \ No newline at end of file +] diff --git a/src/nix/modules/composition/docker-compose.nix b/src/nix/modules/composition/docker-compose.nix index 8ff3f62..ce33b1f 100644 --- a/src/nix/modules/composition/docker-compose.nix +++ b/src/nix/modules/composition/docker-compose.nix @@ -68,6 +68,14 @@ in description = "A attribute set of volume configurations."; default = {}; }; + docker-compose.secrets = lib.mkOption { + type = lib.types.attrsOf lib.types.unspecified; + description = '' + An attribute set of secret configurations. For more info, see: + https://docs.docker.com/compose/compose-file/09-secrets/ + ''; + default = {}; + }; }; config = { out.dockerComposeYaml = pkgs.writeText "docker-compose.yaml" config.out.dockerComposeYamlText; @@ -79,6 +87,7 @@ in services = lib.mapAttrs (k: c: c.out.service) config.services; x-arion = config.docker-compose.extended; volumes = config.docker-compose.volumes; + secrets = config.docker-compose.secrets; }; }; } diff --git a/src/nix/modules/composition/secrets.nix b/src/nix/modules/composition/secrets.nix new file mode 100644 index 0000000..3cf1260 --- /dev/null +++ b/src/nix/modules/composition/secrets.nix @@ -0,0 +1,33 @@ +{ config, lib, ... }: + +let + inherit (lib) + mkOption + types + ; + inherit (import ../../lib.nix { inherit lib; }) + link + ; +in +{ + + options = { + secrets = mkOption { + type = types.lazyAttrsOf (types.submoduleWith { + modules = [ + ../secrets/secret.nix + ]; + }); + description = '' + See ${link "https://docs.docker.com/compose/compose-file/09-secrets/" "Docker Compose Secrets"} + ''; + }; + }; + + config = { + + secrets = {}; + docker-compose.secrets = lib.mapAttrs (k: v: v.out) config.secrets; + + }; +} diff --git a/src/nix/modules/secrets/secret.nix b/src/nix/modules/secrets/secret.nix new file mode 100644 index 0000000..687f453 --- /dev/null +++ b/src/nix/modules/secrets/secret.nix @@ -0,0 +1,55 @@ +{ config, lib, options, ... }: + +let + inherit (lib) + mkOption + optionalAttrs + types + ; + inherit (import ../../lib.nix { inherit lib; }) + secretRef + ; +in +{ + options = { + file = mkOption { + description = '' + The secret is created with the contents of the file at the specified path. + ${secretRef "file"} + ''; + type = types.nullOr types.str; + }; + + environment = mkOption { + description = '' + The secret is created with the value of an environment variable. + ${secretRef "environment"} + ''; + type = types.nullOr types.str; + }; + + out = mkOption { + internal = true; + description = '' + Defines sensitive data that is granted to the services in your Compose application. + The source of the secret is either `file` or `environment`. + ''; + type = lib.types.attrsOf lib.types.raw or lib.types.unspecified; + }; + }; + + config = { + out = + lib.mapAttrs + (k: opt: opt.value) + (lib.filterAttrs + (k: opt: opt.isDefined) + { + inherit (options) + file + environment + ; + } + ); + }; +} diff --git a/src/nix/modules/service/docker-compose-service.nix b/src/nix/modules/service/docker-compose-service.nix index c55b971..3b3b4df 100644 --- a/src/nix/modules/service/docker-compose-service.nix +++ b/src/nix/modules/service/docker-compose-service.nix @@ -57,29 +57,56 @@ in default = []; description = serviceRef "tmpfs"; }; - service.build.context = mkOption { - type = nullOr str; - default = null; - description = '' - Locates a Dockerfile to use for creating an image to use in this service. + service.build = mkOption { + default = {}; + description = serviceRef "build"; + type = submodule ({ options, ...}: { + options = { + _out = mkOption { + internal = true; + readOnly = true; + default = lib.mapAttrs (k: opt: opt.value) (lib.filterAttrs (_: opt: opt.value != null) { inherit (options) context dockerfile target secrets; }); + }; + context = mkOption { + type = nullOr str; + default = null; + description = '' + Locates a Dockerfile to use for creating an image to use in this service. - https://docs.docker.com/compose/compose-file/build/#context - ''; - }; - service.build.dockerfile = mkOption { - type = nullOr str; - default = null; - description = '' - Sets an alternate Dockerfile. A relative path is resolved from the build context. - https://docs.docker.com/compose/compose-file/build/#dockerfile - ''; + https://docs.docker.com/compose/compose-file/build/#context + ''; + }; + dockerfile = mkOption { + type = nullOr str; + default = null; + description = '' + Sets an alternate Dockerfile. A relative path is resolved from the build context. + https://docs.docker.com/compose/compose-file/build/#dockerfile + ''; + }; + target = mkOption { + type = nullOr str; + default = null; + description = '' + Defines the stage to build as defined inside a multi-stage Dockerfile. + https://docs.docker.com/compose/compose-file/build/#target + ''; + }; + secrets = mkOption { + type = nullOr (listOf str); + default = null; + description = '' + Build-time secrets exposed to the service. + ''; + }; + }; + }); }; - service.build.target = mkOption { - type = nullOr str; - default = null; + service.secrets = mkOption { + type = listOf str; + default = []; description = '' - Defines the stage to build as defined inside a multi-stage Dockerfile. - https://docs.docker.com/compose/compose-file/build/#target + Run-time secrets exposed to the service. ''; }; service.hostname = mkOption { @@ -353,8 +380,8 @@ in ; } // lib.optionalAttrs (config.service.image != null) { inherit (config.service) image; - } // lib.optionalAttrs (config.service.build.context != null ) { - build = lib.filterAttrs (n: v: v != null) config.service.build; + } // lib.optionalAttrs (config.service.build._out != {}) { + build = config.service.build._out; } // lib.optionalAttrs (cap_add != []) { inherit cap_add; } // lib.optionalAttrs (cap_drop != []) { @@ -379,6 +406,8 @@ in inherit (config.service) external_links; } // lib.optionalAttrs (config.service.extra_hosts != []) { inherit (config.service) extra_hosts; + } // lib.optionalAttrs (config.service.secrets != []) { + inherit (config.service) secrets; } // lib.optionalAttrs (config.service.hostname != null) { inherit (config.service) hostname; } // lib.optionalAttrs (config.service.dns != []) {