summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--consumers/users.nix11
-rw-r--r--contracts/secrets.nix99
-rw-r--r--default.nix41
-rw-r--r--providers/asecret.nix127
5 files changed, 277 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b2be92b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+result
diff --git a/consumers/users.nix b/consumers/users.nix
new file mode 100644
index 0000000..9e4f401
--- /dev/null
+++ b/consumers/users.nix
@@ -0,0 +1,11 @@
+{ lib, config, ... }:
+{
+ options = {
+ userPasswords.secrets.consumer = lib.mkOption {
+ type = config.contracts.secrets.provider;
+ };
+ };
+ config = {
+ users.users.root.passwordFile = config.userPasswords.secrets.consumer.output.path;
+ };
+}
diff --git a/contracts/secrets.nix b/contracts/secrets.nix
new file mode 100644
index 0000000..89b6466
--- /dev/null
+++ b/contracts/secrets.nix
@@ -0,0 +1,99 @@
+{ lib, ... }:
+{
+ contracts.secrets = {
+ meta = {
+ maintainer = [ "cindi" ];
+ description = "provide 'secrets'";
+ };
+
+ input = {
+ options.type = lib.mkOption {
+ type = lib.types.str;
+ default = "random-password";
+ };
+ };
+
+ output = {
+ options.path = lib.mkOption {
+ type = lib.types.path;
+ description = ''
+ Path to the file containing the secret generated out of band.
+ This path will exist after deploying to a target host,
+ it is not available through the nix store.
+ '';
+ };
+ };
+
+ behaviorTest = { providerRoot, extraModules ? [] }: {
+ nodes.machine = { config, ... }: {
+ imports = extraModules;
+
+ options.test = {
+ owner = lib.mkOption {
+ type = lib.types.str;
+ default = "root";
+ };
+
+ group = lib.mkOption {
+ type = lib.types.str;
+ default = "root";
+ };
+
+ mode = lib.mkOption {
+ type = lib.types.str;
+ default = "0400";
+ };
+
+ content = lib.mkOption {
+ type = lib.types.str;
+ default = "a super secret secret!";
+ };
+ };
+
+ config = lib.mkMerge [
+ (lib.setAttrByPath providerRoot {
+ # We set consumer.input and not input directly because the latter is readOnly.
+ consumer.input = {
+ inherit (config.test) owner group mode;
+ };
+ })
+ (lib.mkIf (config.test.owner != "root") {
+ users.users.${config.test.owner}.isNormalUser = true;
+ })
+ (lib.mkIf (config.test.group != "root") {
+ users.groups.${config.test.group} = {};
+ })
+ ];
+ };
+
+ testScript = { nodes, ... }:
+ let
+ cfg = nodes.machine;
+ inherit (lib.getAttrFromPath providerRoot nodes.machine) output;
+ in
+ ''
+ owner = machine.succeed("stat -c '%U' ${output.path}").strip()
+ print(f"Got owner {owner}")
+ if owner != "${cfg.test.owner}":
+ raise Exception(f"Owner should be '${cfg.test.owner}' but got '{owner}'")
+
+ group = machine.succeed("stat -c '%G' ${output.path}").strip()
+ print(f"Got group {group}")
+ if group != "${cfg.test.group}":
+ raise Exception(f"Group should be '${cfg.test.group}' but got '{group}'")
+
+ mode = str(int(machine.succeed("stat -c '%a' ${output.path}").strip()))
+ print(f"Got mode {mode}")
+ wantedMode = str(int("${cfg.test.mode}"))
+ if mode != wantedMode:
+ raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'")
+
+ content = machine.succeed("cat ${output.path}").strip()
+ print(f"Got content {content}")
+ if content != "${cfg.test.content}":
+ raise Exception(f"Content should be '${cfg.test.content}' but got '{content}'")
+ '';
+ };
+ };
+
+}
diff --git a/default.nix b/default.nix
index 744d2f0..5fe0306 100644
--- a/default.nix
+++ b/default.nix
@@ -3,10 +3,47 @@
}:
with (import ./lib {});
eval {
- machines.bob = { self, ... }: {
+ machines.bob = { self, config, ... }: {
+ imports = [
+ ./consumers/users.nix
+ ./contracts/secrets.nix
+ ./providers/asecret.nix
+ ];
networking.hostName = "bob";
+ asecret.secrets.provider = config.userPasswords.secrets;
+ userPasswords.secrets.consumer = config.asecret.secrets;
};
machines.alice = {
networking.hostName = "alice";
};
-}
+} //
+(let
+ lib = pkgs.lib;
+ config = {};
+in
+{
+ test =
+ let
+ inherit ((import ./contracts/secrets.nix {
+ inherit lib;
+ }).contracts.secrets) behaviorTest;
+ in
+ pkgs.testers.runNixOSTest ({
+ name = "contracts-filebackup-restic";
+ meta.maintainers = [ lib.maintainers.ibizaman ];
+ # I tried using the following line but it leads to infinite recursion.
+ # Instead, I made a hacky import. pkgs.callPackage was also giving an
+ # infinite recursion.
+ #
+ # } // config.contracts.secret.behaviorTest {
+ #
+ } // behaviorTest {
+ providerRoot = [ "testing" "asecret" "mysecret" "secret" ];
+ extraModules = [
+ ./providers/asecret.nix
+ ({ config, ... }: {
+ testing.asecret.mysecret.content = config.test.content;
+ })
+ ];
+ });
+})
diff --git a/providers/asecret.nix b/providers/asecret.nix
new file mode 100644
index 0000000..df7fe20
--- /dev/null
+++ b/providers/asecret.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+let
+ cfg = config.asecret;
+
+ inherit (lib) mapAttrs' mkOption nameValuePair;
+ inherit (lib.types) attrsOf nullOr str submodule;
+ inherit (pkgs) writeText;
+in
+{
+ options.asecret = mkOption {
+ default = {};
+ description = ''
+ A secret. These should only be used in tests.
+ '';
+
+ example = lib.literalExpression ''
+ {
+ mySecret = {
+ secret.input = {
+ user = "me";
+ mode = "0400";
+ restartUnits = [ "myservice.service" ];
+ };
+ settings.content = "My Secret";
+ };
+ }
+ '';
+ type = attrsOf (submodule (mod@{ name, options, ... }: {
+ options = {
+ mode = mkOption {
+ description = ''
+ Mode of the secret file.
+ '';
+ type = str;
+ default = "0400";
+ };
+
+ owner = mkOption {
+ description = ''
+ Linux user owning the secret file.
+ '';
+ type = str;
+ };
+
+ group = mkOption {
+ description = ''
+ Linux group owning the secret file.
+ '';
+ type = str;
+ default = options.user.default;
+ defaultText = "user";
+ };
+
+ content = mkOption {
+ type = nullOr str;
+ description = ''
+ Content of the secret as a string.
+
+ This will be stored in the nix store and should only be used for testing or maybe in dev.
+ '';
+ default = null;
+ };
+
+ source = mkOption {
+ type = nullOr str;
+ description = ''
+ Source of the content of the secret as a path in the nix store.
+ '';
+ default = null;
+ };
+
+ path = mkOption {
+ type = str;
+ description = ''
+ Path where the secret should be located.
+ '';
+ default = "/run/hardcodedSecrets/hardcodedSecret_${name}";
+ };
+
+ secrets = mkOption {
+ type = config.contracts.secrets.provider;
+ };
+ };
+
+ config = {
+ inherit (mod.config.secret.input) mode owner group;
+ secret.output.path = mod.config.path;
+ };
+ }));
+ };
+
+ config = {
+ system.activationScripts = mapAttrs' (n: cfg':
+ let
+ source = if cfg'.source != null
+ then cfg'.source
+ else writeText "hardcodedSecret_${n}_content" cfg'.content;
+ in
+ nameValuePair "hardcodedSecret_${n}" ''
+ (
+ set -e
+ mkdir -p "$(dirname "${cfg'.path}")"
+ touch "${cfg'.path}"
+ chmod ${cfg'.mode} "${cfg'.path}"
+ chown ${cfg'.owner}:${cfg'.group} "${cfg'.path}"
+ cp ${source} "${cfg'.path}"
+ ) || echo "Failed to create hardcoded secret at ${cfg'.path}"
+ ''
+ ) cfg;
+ };
+
+ # Without `meta.buildDocsInSandbox = false;`, I get:
+ #
+ # > error: attribute 'contracts' missing
+ # > at /nix/store/2gd9yzcfpqqp00vskxlqq4ds48mpgdzv-nixos/modules/testing/hardcodedSecret.nix:81:18:
+ # > 80| secret = mkOption {
+ # > 81| type = config.contracts.secret.provider;
+ # > | ^
+ # > 82| };
+ # > Cacheable portion of option doc build failed.
+ # > Usually this means that an option attribute that ends up in documentation (eg `default` or `description`) depends on the restricted module arguments `config` or `pkgs`.
+ # >
+ # > Rebuild your configuration with `--show-trace` to find the offending location. Remove the references to restricted arguments (eg by escaping their antiquotations or adding a `defaultText`) or disable the sandboxed build for the failing module by setting `meta.buildDocsInSandbox = false`.
+ #
+ # With the line, I don't get the warning but still get the missing 'contracts' attribute error.
+ meta.buildDocsInSandbox = false;
+}