diff --git a/nixos/modules/services/web-apps/glance.nix b/nixos/modules/services/web-apps/glance.nix index fbc310daea77..9d01e701e9c7 100644 --- a/nixos/modules/services/web-apps/glance.nix +++ b/nixos/modules/services/web-apps/glance.nix @@ -8,15 +8,27 @@ let cfg = config.services.glance; inherit (lib) - mkEnableOption - mkPackageOption - mkOption - mkIf + catAttrs + concatMapStrings getExe + mkEnableOption + mkIf + mkOption + mkPackageOption types ; + inherit (builtins) + concatLists + isAttrs + isList + attrNames + getAttr + ; + settingsFormat = pkgs.formats.yaml { }; + settingsFile = settingsFormat.generate "glance.yaml" cfg.settings; + mergedSettingsFile = "/run/glance/glance.yaml"; in { options.services.glance = { @@ -69,7 +81,9 @@ in { type = "calendar"; } { type = "weather"; - location = "Nivelles, Belgium"; + location = { + _secret = "/var/lib/secrets/glance/location"; + }; } ]; } @@ -84,6 +98,13 @@ in Configuration written to a yaml file that is read by glance. See for more. + + Settings containing secret data should be set to an + attribute set containing the attribute + _secret - a string pointing to a file + containing the value the option should be set to. See the + example in `services.glance.settings.pages` at the weather widget + with a location secret to get a better picture of this. ''; }; @@ -102,13 +123,41 @@ in description = "Glance feed dashboard server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; + path = [ pkgs.replace-secret ]; serviceConfig = { - ExecStart = + ExecStartPre = let - glance-yaml = settingsFormat.generate "glance.yaml" cfg.settings; + findSecrets = + data: + if isAttrs data then + if data ? _secret then + [ data ] + else + concatLists (map (attr: findSecrets (getAttr attr data)) (attrNames data)) + else if isList data then + concatLists (map findSecrets data) + else + [ ]; + secretPaths = catAttrs "_secret" (findSecrets cfg.settings); + mkSecretReplacement = secretPath: '' + replace-secret ${ + lib.escapeShellArgs [ + "_secret: ${secretPath}" + secretPath + mergedSettingsFile + ] + } + ''; + secretReplacements = concatMapStrings mkSecretReplacement secretPaths; in - "${getExe cfg.package} --config ${glance-yaml}"; + # Use "+" to run as root because the secrets may not be accessible to glance + "+" + + pkgs.writeShellScript "glance-start-pre" '' + install -m 600 -o $USER ${settingsFile} ${mergedSettingsFile} + ${secretReplacements} + ''; + ExecStart = "${getExe cfg.package} --config ${mergedSettingsFile}"; WorkingDirectory = "/var/lib/glance"; StateDirectory = "glance"; RuntimeDirectory = "glance"; diff --git a/nixos/tests/glance.nix b/nixos/tests/glance.nix index 455ef0868513..254173e1eb71 100644 --- a/nixos/tests/glance.nix +++ b/nixos/tests/glance.nix @@ -5,19 +5,47 @@ nodes = { machine_default = - { pkgs, ... }: + { ... }: { services.glance = { enable = true; }; }; - machine_custom_port = + machine_configured = { pkgs, ... }: + let + # Do not use this in production. This will make the secret world-readable + # in the Nix store + secrets.glance-location.path = builtins.toString ( + pkgs.writeText "location-secret" "Nivelles, Belgium" + ); + in { services.glance = { enable = true; - settings.server.port = 5678; + settings = { + server.port = 5678; + pages = [ + { + name = "Home"; + columns = [ + { + size = "full"; + widgets = [ + { type = "calendar"; } + { + type = "weather"; + location = { + _secret = secrets.glance-location.path; + }; + } + ]; + } + ]; + } + ]; + }; }; }; }; @@ -25,23 +53,31 @@ extraPythonPackages = p: with p; [ beautifulsoup4 + pyyaml + types-pyyaml types-beautifulsoup4 ]; testScript = '' from bs4 import BeautifulSoup + import yaml machine_default.start() machine_default.wait_for_unit("glance.service") machine_default.wait_for_open_port(8080) - machine_custom_port.start() - machine_custom_port.wait_for_unit("glance.service") - machine_custom_port.wait_for_open_port(5678) + machine_configured.start() + machine_configured.wait_for_unit("glance.service") + machine_configured.wait_for_open_port(5678) soup = BeautifulSoup(machine_default.succeed("curl http://localhost:8080")) expected_version = "v${config.nodes.machine_default.services.glance.package.version}" assert any(a.text == expected_version for a in soup.select(".footer a")) + + yaml_contents = machine_configured.succeed("cat /run/glance/glance.yaml") + yaml_parsed = yaml.load(yaml_contents, Loader=yaml.FullLoader) + location = yaml_parsed["pages"][0]["columns"][0]["widgets"][1]["location"] + assert location == "Nivelles, Belgium" ''; meta.maintainers = [ lib.maintainers.drupol ];