diff --git a/lib/contracts/default.nix b/lib/contracts/default.nix new file mode 100644 index 0000000000000..a8ce0a4d5409a --- /dev/null +++ b/lib/contracts/default.nix @@ -0,0 +1,62 @@ +{ lib, ... }: +let + inherit (lib) mkOption modules types; + inherit (types) + attrsOf + listOf + option + submodule + str + ; + contractModule = mkOption { + type = submodule { + options = { + meta = mkOption { + description = '' + Useful information about the contract and its maintenance. + ''; + type = submodule { + options = { + description = mkOption { + description = '' + Description of the contract. + ''; + type = str; + }; + maintainers = mkOption { + description = '' + Maintainers of the contract. + ''; + type = listOf str; + }; + }; + }; + }; + input = mkOption { + description = '' + Input type of a contract. + ''; + type = attrsOf option; + apply = modules.mkContract; + }; + output = mkOption { + description = '' + Output type of a contract. + ''; + type = attrsOf option; + apply = modules.mkContract; + }; + behaviorTest = mkOption { + # The type should be more precise of course. + # There should actually be a NixOSTest type. + # And we can probably do something fancy with the `input` and `output` modules. + type = types.functionTo types.attrs; + }; + }; + }; + }; +in +# yields: attrsOf contractModule +lib.mapAttrs (_: path: modules.evalOption contractModule (import path { inherit lib; })) { + fileSecrets = ./file-secrets.nix; +} diff --git a/lib/contracts/file-secrets.nix b/lib/contracts/file-secrets.nix new file mode 100644 index 0000000000000..7155f2ccfb071 --- /dev/null +++ b/lib/contracts/file-secrets.nix @@ -0,0 +1,134 @@ +{ + lib, + ... +}: +let + inherit (lib) mkOption types; + inherit (types) str; +in +{ + meta = { + description = '' + Contract for secrets handling where a consumer requests a secret + and a provider provides it at runtime at a given file path. + ''; + maintainers = with lib.maintainers; [ + ibizaman + kiara + ]; + }; + + input = { + mode = mkOption { + description = '' + Mode the secret file must have. + ''; + type = str; + default = "0400"; + }; + + owner = mkOption { + description = '' + Linux user that must own the secret file. + ''; + type = str; + }; + + group = mkOption { + description = '' + Linux group that must own the secret file. + ''; + type = str; + }; + }; + output = { + path = mkOption { + type = str; + 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 = + { + name, + providerRoot, + extraModules ? [ ], + }: + { + name = "contracts_filesecrets_${name}"; + nodes.machine = + { config, ... }: + { + imports = extraModules; + + options.test = { + owner = mkOption { + type = str; + default = "root"; + }; + + group = mkOption { + type = str; + default = "root"; + }; + + mode = mkOption { + type = str; + default = "0400"; + }; + + content = mkOption { + type = str; + default = "a super secret secret!"; + }; + }; + + config = lib.mkMerge [ + (lib.setAttrByPath providerRoot { + 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/lib/default.nix b/lib/default.nix index 15949d1cdf367..5b942dbe1e873 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -72,6 +72,9 @@ let options = callLibs ./options.nix; types = callLibs ./types.nix; + # contracts + contracts = callLibs ./contracts; + # constants licenses = callLibs ./licenses.nix; sourceTypes = callLibs ./source-types.nix; diff --git a/lib/modules.nix b/lib/modules.nix index 6d430effcc895..40fe7597e109d 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -2077,6 +2077,208 @@ let config = lib.importTOML file; }; + /** + Evaluate a configuration in the context of a corresponding module system option. + + modules.evalOption :: option -> attrs -> attrs + + # Inputs + + `option` + + : 1\. Module system option in which to evaluate the configuration + + `conf` + + : 2\. Configuration to evaluate within the option + */ + evalOption = + option: conf: + (lib.evalModules { + modules = [ + { + options.opt = option; + config.opt = conf; + } + ]; + }).config.opt; + + /** + Extend a (sub-)module option with a set of overrides. + + modules.extendOption :: attrs -> option -> option + + # Inputs + + `overrides` + + : 1\. A (recursive) attrset of fields to add to the option + + `opt` + + : 2\. Option to extend with `overrides` + + Example: + + ```nix + { config, lib, ... }: + let + inherit (lib) mkOption modules types; + in + { + options.foo = modules.extendOption + { + bar = { + default = 10; + defaultText = "10"; + }; + } + (mkOption { + type = types.submodule { + options.bar = mkOption { + type = types.int; + }; + }; + }) + } + ``` + */ + extendOption = + overrides: opt: + let + inherit (opt) type; + isSubmodule = lib.isOptionType type && type.name == "submodule"; + # (deduplicated) keys from submodules to iterate over + # we don't need the values, but attrset offers O(1) containment checks + subOptions = lib.optionalAttrs isSubmodule ( + lib.attrsets.mergeAttrsList (lib.lists.map (mod: mod.options or { }) type.getSubModules) + ); + subOverrides = lib.filterAttrs (k: _: subOptions ? ${k}) overrides; + directOverrides = lib.filterAttrs (k: _: !(subOptions ? ${k})) overrides; + in + lib.mkOption ( + # re-wrap existing option attributes + (builtins.removeAttrs opt [ "_type" ]) + # if we are annotating something that isn't a sub-module, + # just override relevant attributes on the option + // directOverrides + # to annotate a sub-module, presume we should just annotate its sub-options, + # which we iterate over to reconstruct with relevant annotations. + // lib.optionalAttrs isSubmodule { + type = lib.types.submoduleWith ( + # meta we will not change + type.functor.payload + # modules are the parameter that will change as per our overrides + // { + modules = [ + { + options = lib.mapAttrs ( + k: _: + let + # option for the attribute in question: for now presume just one sub-module had it + attrOpt = (lib.head (lib.filter (m: (m.options or { }) ? ${k}) type.getSubModules)).options.${k}; + # any overrides for the attribute in question + attrOverrides = subOverrides.${k} or { }; + in + # recurse until we have no more overrides to annotate the option with + if attrOverrides != { } then extendOption attrOverrides attrOpt else attrOpt + ) subOptions; + } + ]; + } + ); + } + ); + + /** + Extend a (sub-)module type with a set of overrides. + + modules.extendSubmodule :: attrs -> optionType -> optionType + + # Inputs + + `overrides` + + : 1\. A (recursive) attrset of fields to add to the option + + `mod` + + : 2\. A (sub-)module type to extend with `overrides` + + Example: + + ```nix + { config, lib, ... }: + let + inherit (lib) mkOption modules types; + in + { + options.foo = mkOption { + default = { }; + type = modules.extendSubmodule + bar = { + default = 10; + defaultText = "10"; + }; + (types.submodule { + options.bar = mkOption { + type = types.int; + }; + }); + }; + } + ``` + */ + extendSubmodule = + overrides: mod: + (extendOption overrides ( + lib.mkOption { + type = mod; + } + )).type; + + /** + Construct a (sub-)module type from options and a set of overrides. + + modules.mkContract :: attrsOf option -> attrs -> optionType + + # Inputs + + `options` + + : 1\. An attrset of module options from which to construct the submodule type + + `overrides` + + : 2\. A (recursive) attrset of fields to add to the submodule type + + Example: + + ```nix + { config, lib, ... }: + let + inherit (lib) mkOption modules types; + in + { + options.foo = mkOption { + default = { }; + type = modules.mkContract + { + bar = mkOption { + type = types.int; + }; + } + bar = { + default = 10; + defaultText = "10"; + }; + }; + } + ``` + */ + mkContract = + options: overrides: extendSubmodule overrides (lib.types.submodule { inherit options; }); + private = mapAttrs ( @@ -2226,6 +2428,9 @@ private # are just needed by types.nix, but are not meant to be consumed # externally. inherit + evalOption + extendOption + extendSubmodule defaultOrderPriority defaultOverridePriority doRename @@ -2251,6 +2456,7 @@ private mkAssert mkBefore mkChangedOptionModule + mkContract mkDefault mkDefinition mkDerivedConfig diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index aa9d3443fb6a4..22c9a7a21dd48 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -704,6 +704,9 @@ checkConfigError 'In module .*/options-type-error-configuration.nix: expected an # Check that that merging of option collisions doesn't depend on type being set checkConfigError 'The option .group..*would be a parent of the following options, but its type .. does not support nested options.\n\s*- option.s. with prefix .group.enable..*' config.group.enable ./merge-typeless-option.nix +# types.option +checkConfigOutput '^10$' config.anOption ./option.nix + # Test that types.optionType merges types correctly checkConfigOutput '^10$' config.theOption.int ./optionTypeMerging.nix checkConfigOutput '^"hello"$' config.theOption.str ./optionTypeMerging.nix @@ -819,6 +822,11 @@ checkConfigOutput '^true$' config.viaConfig ./mkDefinition.nix checkConfigOutput '^true$' config.mkMerge ./mkDefinition.nix checkConfigOutput '^true$' config.mkForce ./mkDefinition.nix +checkConfigOutput '2' config.bar.baz ./evalOption.nix +checkConfigError 'not of type' config.foo.bar ./extendOption.nix +checkConfigError 'not of type' config.foo.bar ./extendSubmodule.nix +checkConfigError 'not of type' config.foo.bar ./mkContract.nix + # specialArgs._class checkConfigOutput '"nixos"' config.nixos.config.foo ./specialArgs-class.nix checkConfigOutput '"bar"' config.conditionalImportAsNixos.config.foo ./specialArgs-class.nix diff --git a/lib/tests/modules/evalOption.nix b/lib/tests/modules/evalOption.nix new file mode 100644 index 0000000000000..1c043cdcf3987 --- /dev/null +++ b/lib/tests/modules/evalOption.nix @@ -0,0 +1,29 @@ +{ lib, ... }: +let + inherit (lib) modules mkOption types; +in +{ + options.bar = mkOption { + default = + modules.evalOption + (mkOption { + default = { }; + type = types.submodule ( + { config, ... }: + { + options = { + foo = mkOption { + type = types.int; + }; + baz = mkOption { + default = config.foo + 1; + }; + }; + } + ); + }) + { + foo = 1; + }; + }; +} diff --git a/lib/tests/modules/extendOption.nix b/lib/tests/modules/extendOption.nix new file mode 100644 index 0000000000000..a03fe728e2991 --- /dev/null +++ b/lib/tests/modules/extendOption.nix @@ -0,0 +1,16 @@ +{ lib, ... }: +{ + options.foo = + lib.modules.extendOption + { + bar.default = "baz"; + } + ( + lib.mkOption { + default = { }; + type = lib.types.submodule { + options.bar = lib.mkOption { type = lib.types.int; }; + }; + } + ); +} diff --git a/lib/tests/modules/extendSubmodule.nix b/lib/tests/modules/extendSubmodule.nix new file mode 100644 index 0000000000000..50f99bf778292 --- /dev/null +++ b/lib/tests/modules/extendSubmodule.nix @@ -0,0 +1,16 @@ +{ lib, ... }: +{ + options.foo = lib.mkOption { + default = { }; + type = + lib.modules.extendSubmodule + { + bar.default = "baz"; + } + ( + lib.types.submodule { + options.bar = lib.mkOption { type = lib.types.int; }; + } + ); + }; +} diff --git a/lib/tests/modules/extendedOptions.nix b/lib/tests/modules/extendedOptions.nix new file mode 100644 index 0000000000000..fcf5a9df58379 --- /dev/null +++ b/lib/tests/modules/extendedOptions.nix @@ -0,0 +1,14 @@ +{ lib, ... }: +{ + options.foo = lib.mkOption { + default = { }; + type = + lib.modules.extendedOptions + { + bar = lib.mkOption { type = lib.types.int; }; + } + { + bar.default = "baz"; + }; + }; +} diff --git a/lib/tests/modules/mkContract.nix b/lib/tests/modules/mkContract.nix new file mode 100644 index 0000000000000..c8abe8779a6d6 --- /dev/null +++ b/lib/tests/modules/mkContract.nix @@ -0,0 +1,14 @@ +{ lib, ... }: +{ + options.foo = lib.mkOption { + default = { }; + type = + lib.modules.mkContract + { + bar = lib.mkOption { type = lib.types.int; }; + } + { + bar.default = "baz"; + }; + }; +} diff --git a/lib/tests/modules/option.nix b/lib/tests/modules/option.nix new file mode 100644 index 0000000000000..bf48822d1d224 --- /dev/null +++ b/lib/tests/modules/option.nix @@ -0,0 +1,15 @@ +{ config, lib, ... }: +{ + options = { + theOption = lib.mkOption { + type = lib.types.option; + }; + anOption = config.theOption; + }; + config = { + theOption = lib.mkOption { + type = lib.types.int; + }; + anOption = 10; + }; +} diff --git a/lib/types.nix b/lib/types.nix index 4a859694a6e82..cef33debaba32 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -1180,6 +1180,13 @@ rec { }; }; + option = mkOptionType { + name = "option"; + description = "option"; + descriptionClass = "noun"; + check = isType "option"; + }; + # The type of a type! optionType = mkOptionType { name = "optionType"; diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index fde4180a04a61..90366d78006de 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1964,6 +1964,7 @@ ./tasks/stratis.nix ./tasks/swraid.nix ./tasks/trackpoint.nix + ./testing/hardcoded-secret.nix ./testing/service-runner.nix ./virtualisation/amazon-options.nix ./virtualisation/appvm.nix diff --git a/nixos/modules/services/web-apps/stash.nix b/nixos/modules/services/web-apps/stash.nix index e4e1c65d09327..a3455740ec235 100644 --- a/nixos/modules/services/web-apps/stash.nix +++ b/nixos/modules/services/web-apps/stash.nix @@ -6,6 +6,7 @@ }: let inherit (lib) + contracts getExe literalExpression mkEnableOption @@ -16,6 +17,7 @@ let toUpper types ; + inherit (contracts) fileSecrets; cfg = config.services.stash; @@ -366,6 +368,31 @@ let done ''; }; + + secretOptionType = + let + contractSecretsType = types.submodule { + options = { + input = mkOption { + description = "Input of the contract for file secrets."; + default = { }; + type = fileSecrets.input { + owner.default = cfg.user; + group.default = cfg.group; + }; + }; + + output = mkOption { + description = "Output of the contract for file secrets."; + type = fileSecrets.output { }; + }; + }; + }; + in + types.oneOf [ + types.path + contractSecretsType + ]; in { meta = { @@ -418,7 +445,7 @@ in }; passwordFile = mkOption { - type = types.nullOr types.path; + type = types.nullOr secretOptionType; default = null; example = "/path/to/password/file"; description = '' @@ -431,12 +458,12 @@ in ''; }; - jwtSecretKeyFile = mkOption { - type = types.path; + jwtSecretKey = mkOption { + type = secretOptionType; description = "Path to file containing a secret used to sign JWT tokens."; }; - sessionStoreKeyFile = mkOption { - type = types.path; + sessionStoreKey = mkOption { + type = secretOptionType; description = "Path to file containing a secret for session store."; }; @@ -514,9 +541,15 @@ in install -d ${cfg.settings.generated} if [[ -z "${toString cfg.mutableSettings}" || ! -f ${cfg.dataDir}/config.yml ]]; then env \ - password=$(< ${cfg.passwordFile}) \ - jwtSecretKeyFile=$(< ${cfg.jwtSecretKeyFile}) \ - sessionStoreKeyFile=$(< ${cfg.sessionStoreKeyFile}) \ + password=$(< ${ + if lib.isPath cfg.passwordFile then cfg.passwordFile else cfg.passwordFile.output.path + }) \ + jwtSecretKeyFile=$(< ${ + if lib.isPath cfg.jwtSecretKey then cfg.jwtSecretKey else cfg.jwtSecretKey.output.path + }) \ + sessionStoreKeyFile=$(< ${ + if lib.isPath cfg.sessionStoreKey then cfg.sessionStoreKey else cfg.sessionStoreKey.output.path + }) \ ${lib.getExe pkgs.yq-go} ' .jwt_secret_key = strenv(jwtSecretKeyFile) | .session_store_key = strenv(sessionStoreKeyFile) | diff --git a/nixos/modules/testing/hardcoded-secret.nix b/nixos/modules/testing/hardcoded-secret.nix new file mode 100644 index 0000000000000..73453698fce54 --- /dev/null +++ b/nixos/modules/testing/hardcoded-secret.nix @@ -0,0 +1,98 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.testing.hardcoded-secret; + + inherit (lib) + contracts + mapAttrs' + mkOption + nameValuePair + ; + inherit (lib.types) + attrsOf + str + submodule + ; + inherit (pkgs) writeText; + inherit (contracts) fileSecrets; +in +{ + options.testing.hardcoded-secret = mkOption { + default = { }; + description = '' + Hardcoded file secrets. These should only be used in tests. + + They aim to replace the usage of pkgs.writeText in NixOS VM tests + as those make the file world readable + while this module set runtime permissions on the file. + This makes the tests more accurate, ensuring the permissions + set by the contract consumer are correct. + ''; + example = lib.literalExpression '' + { + mySecret = { + input = { + user = "me"; + mode = "0400"; + }; + content = "My Secret"; + }; + } + ''; + type = attrsOf ( + submodule ( + { name, ... }: + { + options = { + input = mkOption { + description = "Input of the contract for file secrets."; + type = fileSecrets.input { + owner.default = "root"; + group.default = "root"; + }; + }; + output = mkOption { + description = "Output of the contract for file secrets."; + default = { }; + type = fileSecrets.output { + path.default = "/run/hardcodedsecrets/${name}"; + }; + }; + + content = mkOption { + type = 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. + ''; + }; + }; + } + ) + ); + }; + + config = { + system.activationScripts = mapAttrs' ( + n: cfg': + let + source = writeText "hardcodedsecret_${n}_content" cfg'.content; + + inherit (cfg') input output; + in + nameValuePair "hardcodedsecret_${n}" '' + mkdir -p "$(dirname "${output.path}")" + touch "${output.path}" + chmod ${input.mode} "${output.path}" + chown ${input.owner}:${input.group} "${output.path}" + cp ${source} "${output.path}" + '' + ) cfg; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 109a146767349..0b2268c8088f1 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -410,6 +410,9 @@ in containers-restart_networking = runTest ./containers-restart_networking.nix; containers-tmpfs = runTest ./containers-tmpfs.nix; containers-unified-hierarchy = runTest ./containers-unified-hierarchy.nix; + contracts = import ./contracts { + inherit runTest; + }; convos = runTest ./convos.nix; coredns = runTest ./coredns.nix; corerad = runTest ./corerad.nix; diff --git a/nixos/tests/contracts/default.nix b/nixos/tests/contracts/default.nix new file mode 100644 index 0000000000000..548829012fc85 --- /dev/null +++ b/nixos/tests/contracts/default.nix @@ -0,0 +1,4 @@ +{ runTest }: +{ + filesecrets-hardcoded-secret = runTest ./filesecrets/hardcoded-secret.nix; +} diff --git a/nixos/tests/contracts/filesecrets/hardcoded-secret.nix b/nixos/tests/contracts/filesecrets/hardcoded-secret.nix new file mode 100644 index 0000000000000..ec137074f1f85 --- /dev/null +++ b/nixos/tests/contracts/filesecrets/hardcoded-secret.nix @@ -0,0 +1,24 @@ +{ + lib, + ... +}: +{ + meta.maintainers = [ lib.maintainers.ibizaman ]; +} +// lib.contracts.fileSecrets.behaviorTest { + name = "hardcoded-secret"; + providerRoot = [ + "testing" + "hardcoded-secret" + "mysecret" + ]; + extraModules = [ + ../../../modules/testing/hardcoded-secret.nix + ( + { config, ... }: + { + testing.hardcoded-secret.mysecret.content = config.test.content; + } + ) + ]; +} diff --git a/nixos/tests/stash.nix b/nixos/tests/stash.nix index 838a5e8a43c6d..119072363ccc7 100644 --- a/nixos/tests/stash.nix +++ b/nixos/tests/stash.nix @@ -9,61 +9,75 @@ import ./make-test-python.nix ( name = "stash"; meta.maintainers = pkgs.stash.meta.maintainers; - nodes.machine = { - services.stash = { - inherit dataDir; - enable = true; + nodes.machine = + { config, ... }: + { + services.stash = { + inherit dataDir; + enable = true; - username = "test"; - passwordFile = pkgs.writeText "stash-password" "MyPassword"; + username = "test"; + passwordFile.output = config.testing.hardcoded-secret."stash-password".output; - jwtSecretKeyFile = pkgs.writeText "jwt_secret_key" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - sessionStoreKeyFile = pkgs.writeText "session_store_key" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + jwtSecretKey.output.path = config.testing.hardcoded-secret."jwt_secret_key".output.path; + sessionStoreKey.output.path = config.testing.hardcoded-secret."session_store_key".output.path; - plugins = - let - src = pkgs.fetchFromGitHub { - owner = "stashapp"; - repo = "CommunityScripts"; - rev = "9b6fac4934c2fac2ef0859ea68ebee5111fc5be5"; - hash = "sha256-PO3J15vaA7SD4r/LyHlXjnpaeYAN9Q++O94bIWdz7OA="; - }; - in - [ - (pkgs.runCommand "stashNotes" { inherit src; } '' - mkdir -p $out/plugins - cp -r $src/plugins/stashNotes $out/plugins/stashNotes - '') - (pkgs.runCommand "Theme-Plex" { inherit src; } '' - mkdir -p $out/plugins - cp -r $src/themes/Theme-Plex $out/plugins/Theme-Plex - '') - ]; + plugins = + let + src = pkgs.fetchFromGitHub { + owner = "stashapp"; + repo = "CommunityScripts"; + rev = "9b6fac4934c2fac2ef0859ea68ebee5111fc5be5"; + hash = "sha256-PO3J15vaA7SD4r/LyHlXjnpaeYAN9Q++O94bIWdz7OA="; + }; + in + [ + (pkgs.runCommand "stashNotes" { inherit src; } '' + mkdir -p $out/plugins + cp -r $src/plugins/stashNotes $out/plugins/stashNotes + '') + (pkgs.runCommand "Theme-Plex" { inherit src; } '' + mkdir -p $out/plugins + cp -r $src/themes/Theme-Plex $out/plugins/Theme-Plex + '') + ]; - mutableScrapers = true; - scrapers = - let - src = pkgs.fetchFromGitHub { - owner = "stashapp"; - repo = "CommunityScrapers"; - rev = "2ece82d17ddb0952c16842b0775274bcda598d81"; - hash = "sha256-AEmnvM8Nikhue9LNF9dkbleYgabCvjKHtzFpMse4otM="; - }; - in - [ - (pkgs.runCommand "FTV" { inherit src; } '' - mkdir -p $out/scrapers/FTV - cp -r $src/scrapers/FTV.yml $out/scrapers/FTV - '') - ]; + mutableScrapers = true; + scrapers = + let + src = pkgs.fetchFromGitHub { + owner = "stashapp"; + repo = "CommunityScrapers"; + rev = "2ece82d17ddb0952c16842b0775274bcda598d81"; + hash = "sha256-AEmnvM8Nikhue9LNF9dkbleYgabCvjKHtzFpMse4otM="; + }; + in + [ + (pkgs.runCommand "FTV" { inherit src; } '' + mkdir -p $out/scrapers/FTV + cp -r $src/scrapers/FTV.yml $out/scrapers/FTV + '') + ]; - settings = { - inherit host port; + settings = { + inherit host port; - stash = [ { path = "/srv"; } ]; + stash = [ { path = "/srv"; } ]; + }; + }; + testing.hardcoded-secret."stash-password" = { + input = config.services.stash.passwordFile.input; + content = "MyPassword"; + }; + testing.hardcoded-secret."jwt_secret_key" = { + input = config.services.stash.jwtSecretKey.input; + content = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + }; + testing.hardcoded-secret."session_store_key" = { + input = config.services.stash.sessionStoreKey.input; + content = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; }; }; - }; testScript = '' machine.wait_for_unit("stash.service")