nixos/glance: allow specifying secret settings

Changed files
+98 -13
nixos
modules
services
web-apps
tests
+56 -7
nixos/modules/services/web-apps/glance.nix
···
cfg = config.services.glance;
inherit (lib)
+
catAttrs
+
concatMapStrings
+
getExe
mkEnableOption
-
mkPackageOption
-
mkOption
mkIf
-
getExe
+
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 = {
···
{ type = "calendar"; }
{
type = "weather";
-
location = "Nivelles, Belgium";
+
location = {
+
_secret = "/var/lib/secrets/glance/location";
+
};
}
];
}
···
Configuration written to a yaml file that is read by glance. See
<https://github.com/glanceapp/glance/blob/main/docs/configuration.md>
for more.
+
+
Settings containing secret data should be set to an
+
attribute set containing the attribute
+
<literal>_secret</literal> - 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.
'';
};
···
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";
+42 -6
nixos/tests/glance.nix
···
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;
+
};
+
}
+
];
+
}
+
];
+
}
+
];
+
};
};
};
};
···
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 ];