Compare commits
2 commits
main
...
secrets-te
Author | SHA1 | Date | |
---|---|---|---|
|
d42da1a9fb | ||
|
a6878c0391 |
10 changed files with 197 additions and 3 deletions
24
src/arion
24
src/arion
|
@ -118,7 +118,9 @@ fi
|
|||
debug docker_compose_args: "${docker_compose_args[@]}"
|
||||
debug files: "${files[@]}"
|
||||
|
||||
docker_compose_yaml=.tmp-nix-docker-compose-$$-$RANDOM.yaml
|
||||
docker_system_id="$(docker info --format '{{.Name}}-{{.ID}}')"
|
||||
|
||||
docker_compose_yaml=.tmp-arion-$$-$RANDOM.yaml
|
||||
cleanup() {
|
||||
rm -f $docker_compose_yaml
|
||||
}
|
||||
|
@ -203,6 +205,26 @@ do_build() {
|
|||
}
|
||||
EOF
|
||||
)"
|
||||
|
||||
# FIXME: Do something else for swarm
|
||||
# FIXME: Include project name
|
||||
export ARION_SECRETS_DIR="arion-secrets/$docker_system_id"
|
||||
|
||||
if [[ true = "$(jq <"$docker_compose_yaml" '.["x-arion"].hasTextSecrets')" ]]; then
|
||||
echo 1>&2 "Evaluating configuration read-only for secrets..."
|
||||
eval "$(nix-instantiate \
|
||||
"$nix_dir/eval-composition.nix" \
|
||||
--eval \
|
||||
--readonly-mode \
|
||||
--json \
|
||||
--argstr uid "$UID" \
|
||||
--arg modules "$modules" \
|
||||
--arg pkgs "$pkgs_argument" \
|
||||
--arg writableStore false \
|
||||
--show-trace \
|
||||
--attr 'config.build.writeSecretsScript' \
|
||||
| jq -r)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
{ modules ? [], uid ? "0", pkgs, hostNixStorePrefix ? "", }:
|
||||
{ modules ? []
|
||||
, uid ? "0"
|
||||
, pkgs
|
||||
, hostNixStorePrefix ? ""
|
||||
, writableStore ? true
|
||||
}:
|
||||
|
||||
let _pkgs = pkgs;
|
||||
in
|
||||
|
@ -22,6 +27,7 @@ let
|
|||
./modules/composition/host-environment.nix
|
||||
./modules/composition/images.nix
|
||||
./modules/composition/service-info.nix
|
||||
./modules/composition/text-secrets.nix
|
||||
];
|
||||
|
||||
argsModule = {
|
||||
|
@ -30,6 +36,7 @@ let
|
|||
config._module.args.pkgs = lib.mkIf (pkgs != null) (lib.mkForce pkgs);
|
||||
config.host.nixStorePrefix = hostNixStorePrefix;
|
||||
config.host.uid = lib.toInt uid;
|
||||
config.host.writableStore = writableStore;
|
||||
};
|
||||
|
||||
in
|
||||
|
|
|
@ -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 <link xlink:href="https://docs.docker.com/compose/compose-file/#${fragment}">Docker Compose#${fragment}</link>'';
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,5 +29,14 @@
|
|||
'';
|
||||
};
|
||||
|
||||
host.writableStore = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = ''
|
||||
Whether the Nix store is writable. Normally it is, but when extracting
|
||||
secrets, it must not be writable in order to prevent secrets from
|
||||
accidentally leaking into the Nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
|
29
src/nix/modules/composition/text-secrets.nix
Normal file
29
src/nix/modules/composition/text-secrets.nix
Normal file
|
@ -0,0 +1,29 @@
|
|||
{ config, lib, ... }:
|
||||
let
|
||||
inherit (lib) mkOption mapAttrsToList concatStrings escapeShellArg;
|
||||
inherit (lib.types) attrsOf unspecified;
|
||||
|
||||
in
|
||||
{
|
||||
options = {
|
||||
textSecrets = mkOption {
|
||||
type = attrsOf unspecified; # unspecified for laziness
|
||||
default = {};
|
||||
description = "Secrets to write to files.";
|
||||
};
|
||||
build.writeSecretsScript = mkOption {
|
||||
type = unspecified; # unspecified for laziness
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
description = "Generated script that writes the textSecrets.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
docker-compose.extended.hasTextSecrets = config.textSecrets != {};
|
||||
build.writeSecretsScript = concatStrings (mapAttrsToList (k: v: ''
|
||||
mkdir -p "$ARION_SECRETS_DIR"
|
||||
echo ${escapeShellArg v} >$ARION_SECRETS_DIR/${escapeShellArg k}
|
||||
'') config.textSecrets);
|
||||
};
|
||||
}
|
|
@ -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:
|
||||
''<link xlink:href="${url}">${text}</link>'';
|
||||
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) {
|
||||
|
|
|
@ -63,5 +63,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)\"");
|
||||
$machine->succeed("cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion down && rm -rf work");
|
||||
$machine->waitUntilFails("curl localhost:8000");
|
||||
};
|
||||
'';
|
||||
}
|
||||
|
|
17
tests/testcases/secrets/arion-compose.nix
Normal file
17
tests/testcases/secrets/arion-compose.nix
Normal file
|
@ -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;
|
||||
}
|
2
tests/testcases/secrets/arion-pkgs.nix
Normal file
2
tests/testcases/secrets/arion-pkgs.nix
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Instead of pinning Nixpkgs, we can opt to use the one in NIX_PATH
|
||||
import <nixpkgs> {}
|
1
tests/testcases/secrets/foo.key
Normal file
1
tests/testcases/secrets/foo.key
Normal file
|
@ -0,0 +1 @@
|
|||
qux
|
Loading…
Reference in a new issue