diff --git a/src/nix/modules/composition/docker-compose.nix b/src/nix/modules/composition/docker-compose.nix index 95855ff..999ff40 100644 --- a/src/nix/modules/composition/docker-compose.nix +++ b/src/nix/modules/composition/docker-compose.nix @@ -11,8 +11,36 @@ */ { pkgs, lib, config, ... }: let + cfg = config.docker-compose; + inherit (lib) mkOption optionalAttrs mapAttrs; + inherit (lib.types) submodule attrsOf nullOr either str path bool; evalService = name: modules: pkgs.callPackage ../../eval-service.nix {} { inherit name modules; inherit (config) host; }; + dockerComposeRef = fragment: + ''See Docker Compose#${fragment}''; + + secretType = submodule { + options = { + file = mkOption { + type = either path str; + description = '' + Sets the secret's value to this file. + + ${dockerComposeRef "secrets"} + ''; + }; + external = mkOption { + type = bool; + default = false; + description = '' + Whether the value of this secret is set via other means. + + ${dockerComposeRef "secrets"} + ''; + }; + }; + }; + in { options = { @@ -42,6 +70,11 @@ in description = "Attribute set of evaluated service configurations."; readOnly = true; }; + docker-compose.secrets = lib.mkOption { + type = attrsOf secretType; + description = dockerComposeRef "secrets"; + default = {}; + }; }; config = { build.dockerComposeYaml = pkgs.writeText "docker-compose.yaml" config.build.dockerComposeYamlText; @@ -52,6 +85,13 @@ in version = "3.4"; services = lib.mapAttrs (k: c: c.config.build.service) config.docker-compose.evaluatedServices; x-arion = config.docker-compose.extended; + } // optionalAttrs (cfg.secrets != {}) { + secrets = mapAttrs (_k: s: optionalAttrs (s.external != false) { + inherit (s) external; + } // optionalAttrs (s.file != null) { + file = toString s.file; + } + ) cfg.secrets; }; }; } diff --git a/src/nix/modules/service/docker-compose-service.nix b/src/nix/modules/service/docker-compose-service.nix index 54dae9f..d40c6f7 100644 --- a/src/nix/modules/service/docker-compose-service.nix +++ b/src/nix/modules/service/docker-compose-service.nix @@ -7,9 +7,11 @@ { pkgs, lib, config, ... }: let - inherit (lib) mkOption types; + inherit (lib) mkOption types mapAttrs mapAttrsToList; inherit (types) listOf nullOr attrsOf str either int bool; + cfg = config.service; + link = url: text: ''${text}''; dockerComposeRef = fragment: @@ -23,6 +25,48 @@ let cap_add = lib.attrNames (lib.filterAttrs (name: value: value == true) config.service.capabilities); cap_drop = lib.attrNames (lib.filterAttrs (name: value: value == false) config.service.capabilities); + serviceSecretType = types.submodule { + options = { + source = mkOption { + type = nullOr str; + default = null; + description = dockerComposeRef "secrets"; + }; + uid = mkOption { + type = nullOr (either str int); + default = null; + description = dockerComposeRef "secrets"; + }; + gid = mkOption { + type = nullOr (either str int); + default = null; + description = dockerComposeRef "secrets"; + }; + mode = mkOption { + type = nullOr str; + # default = "0444"; + default = null; + description = '' + The default value of is usually 0444. This option may not be supported + when not deploying to a Swarm. + + ${dockerComposeRef "secrets"} + ''; + }; + }; + }; + secrets = mapAttrsToList (k: s: { + target = k; + } //lib.optionalAttrs (s.source != null) { + inherit (s) source; + } // lib.optionalAttrs (s.uid != null) { + inherit (s) uid; + } // lib.optionalAttrs (s.gid != null) { + inherit (s) gid; + } // lib.optionalAttrs (s.mode != null) { + inherit (s) mode; + }) cfg.secrets; + in { options = { @@ -197,6 +241,19 @@ in } ''; }; + service.secrets = mkOption { + type = attrsOf serviceSecretType; + default = {}; + description = dockerComposeRef "secrets"; + example = { + redis_secret = { + source = "web_cache_redis_secret"; + uid = 123; + gid = 123; + mode = "0440"; + }; + }; + }; }; config.build.service = { @@ -242,6 +299,8 @@ in inherit (config.service) restart; } // lib.optionalAttrs (config.service.stop_signal != null) { inherit (config.service) stop_signal; + } // lib.optionalAttrs (secrets != null) { + inherit secrets; } // lib.optionalAttrs (config.service.tmpfs != []) { inherit (config.service) tmpfs; } // lib.optionalAttrs (config.service.tty != null) { diff --git a/tests/arion-test/default.nix b/tests/arion-test/default.nix index 1020a1b..40f2ccd 100644 --- a/tests/arion-test/default.nix +++ b/tests/arion-test/default.nix @@ -34,6 +34,7 @@ in (preEval [ ../../examples/minimal/arion-compose.nix ]).config.build.dockerComposeYaml (preEval [ ../../examples/full-nixos/arion-compose.nix ]).config.build.dockerComposeYaml (preEval [ ../../examples/nixos-unit/arion-compose.nix ]).config.build.dockerComposeYaml + (preEval [ ../testcases/secrets/arion-compose.nix ]).config.build.dockerComposeYaml pkgs.stdenv ]; }; @@ -63,5 +64,13 @@ in $machine->succeed("cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion down && rm -rf work"); $machine->waitUntilFails("curl localhost:8000"); }; + + subtest "secrets", sub { + $machine->succeed("cp -r ${../testcases/secrets} work && cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion up -d"); + $machine->waitUntilSucceeds("curl localhost:8000"); + $machine->succeed("test qux = \"\$(curl localhost:8000/foo.txt)\""); + $machine->succeed("(cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion down) && rm -rf work"); + $machine->waitUntilFails("curl localhost:8000"); + }; ''; } diff --git a/tests/testcases/secrets/arion-compose.nix b/tests/testcases/secrets/arion-compose.nix new file mode 100644 index 0000000..dc530eb --- /dev/null +++ b/tests/testcases/secrets/arion-compose.nix @@ -0,0 +1,17 @@ +{ + docker-compose.services.webserver = { pkgs, ... }: { + nixos.useSystemd = true; + nixos.configuration.boot.tmpOnTmpfs = true; + nixos.configuration.services.nginx.enable = true; + + # Please don't do this + nixos.configuration.services.nginx.virtualHosts.localhost.root = "/run/secrets"; + + service.useHostStore = true; + service.ports = [ + "8000:80" # host:container + ]; + service.secrets."foo.txt".source = "foo"; + }; + docker-compose.secrets.foo.file = ./foo.key; +} diff --git a/tests/testcases/secrets/arion-pkgs.nix b/tests/testcases/secrets/arion-pkgs.nix new file mode 100644 index 0000000..d13a605 --- /dev/null +++ b/tests/testcases/secrets/arion-pkgs.nix @@ -0,0 +1,2 @@ +# Instead of pinning Nixpkgs, we can opt to use the one in NIX_PATH +import {} diff --git a/tests/testcases/secrets/foo.key b/tests/testcases/secrets/foo.key new file mode 100644 index 0000000..78df5b0 --- /dev/null +++ b/tests/testcases/secrets/foo.key @@ -0,0 +1 @@ +qux \ No newline at end of file