nixos/glance: allow specifying secret settings (#395551)

authored by Pol Dellaiera and committed by GitHub aaf817bb b4667dba

+98 -13
+56 -7
nixos/modules/services/web-apps/glance.nix
··· 8 8 cfg = config.services.glance; 9 9 10 10 inherit (lib) 11 + catAttrs 12 + concatMapStrings 13 + getExe 11 14 mkEnableOption 12 - mkPackageOption 13 - mkOption 14 15 mkIf 15 - getExe 16 + mkOption 17 + mkPackageOption 16 18 types 19 + ; 20 + 21 + inherit (builtins) 22 + concatLists 23 + isAttrs 24 + isList 25 + attrNames 26 + getAttr 17 27 ; 18 28 19 29 settingsFormat = pkgs.formats.yaml { }; 30 + settingsFile = settingsFormat.generate "glance.yaml" cfg.settings; 31 + mergedSettingsFile = "/run/glance/glance.yaml"; 20 32 in 21 33 { 22 34 options.services.glance = { ··· 69 81 { type = "calendar"; } 70 82 { 71 83 type = "weather"; 72 - location = "Nivelles, Belgium"; 84 + location = { 85 + _secret = "/var/lib/secrets/glance/location"; 86 + }; 73 87 } 74 88 ]; 75 89 } ··· 84 98 Configuration written to a yaml file that is read by glance. See 85 99 <https://github.com/glanceapp/glance/blob/main/docs/configuration.md> 86 100 for more. 101 + 102 + Settings containing secret data should be set to an 103 + attribute set containing the attribute 104 + <literal>_secret</literal> - a string pointing to a file 105 + containing the value the option should be set to. See the 106 + example in `services.glance.settings.pages` at the weather widget 107 + with a location secret to get a better picture of this. 87 108 ''; 88 109 }; 89 110 ··· 102 123 description = "Glance feed dashboard server"; 103 124 wantedBy = [ "multi-user.target" ]; 104 125 after = [ "network.target" ]; 126 + path = [ pkgs.replace-secret ]; 105 127 106 128 serviceConfig = { 107 - ExecStart = 129 + ExecStartPre = 108 130 let 109 - glance-yaml = settingsFormat.generate "glance.yaml" cfg.settings; 131 + findSecrets = 132 + data: 133 + if isAttrs data then 134 + if data ? _secret then 135 + [ data ] 136 + else 137 + concatLists (map (attr: findSecrets (getAttr attr data)) (attrNames data)) 138 + else if isList data then 139 + concatLists (map findSecrets data) 140 + else 141 + [ ]; 142 + secretPaths = catAttrs "_secret" (findSecrets cfg.settings); 143 + mkSecretReplacement = secretPath: '' 144 + replace-secret ${ 145 + lib.escapeShellArgs [ 146 + "_secret: ${secretPath}" 147 + secretPath 148 + mergedSettingsFile 149 + ] 150 + } 151 + ''; 152 + secretReplacements = concatMapStrings mkSecretReplacement secretPaths; 110 153 in 111 - "${getExe cfg.package} --config ${glance-yaml}"; 154 + # Use "+" to run as root because the secrets may not be accessible to glance 155 + "+" 156 + + pkgs.writeShellScript "glance-start-pre" '' 157 + install -m 600 -o $USER ${settingsFile} ${mergedSettingsFile} 158 + ${secretReplacements} 159 + ''; 160 + ExecStart = "${getExe cfg.package} --config ${mergedSettingsFile}"; 112 161 WorkingDirectory = "/var/lib/glance"; 113 162 StateDirectory = "glance"; 114 163 RuntimeDirectory = "glance";
+42 -6
nixos/tests/glance.nix
··· 5 5 6 6 nodes = { 7 7 machine_default = 8 - { pkgs, ... }: 8 + { ... }: 9 9 { 10 10 services.glance = { 11 11 enable = true; 12 12 }; 13 13 }; 14 14 15 - machine_custom_port = 15 + machine_configured = 16 16 { pkgs, ... }: 17 + let 18 + # Do not use this in production. This will make the secret world-readable 19 + # in the Nix store 20 + secrets.glance-location.path = builtins.toString ( 21 + pkgs.writeText "location-secret" "Nivelles, Belgium" 22 + ); 23 + in 17 24 { 18 25 services.glance = { 19 26 enable = true; 20 - settings.server.port = 5678; 27 + settings = { 28 + server.port = 5678; 29 + pages = [ 30 + { 31 + name = "Home"; 32 + columns = [ 33 + { 34 + size = "full"; 35 + widgets = [ 36 + { type = "calendar"; } 37 + { 38 + type = "weather"; 39 + location = { 40 + _secret = secrets.glance-location.path; 41 + }; 42 + } 43 + ]; 44 + } 45 + ]; 46 + } 47 + ]; 48 + }; 21 49 }; 22 50 }; 23 51 }; ··· 25 53 extraPythonPackages = 26 54 p: with p; [ 27 55 beautifulsoup4 56 + pyyaml 57 + types-pyyaml 28 58 types-beautifulsoup4 29 59 ]; 30 60 31 61 testScript = '' 32 62 from bs4 import BeautifulSoup 63 + import yaml 33 64 34 65 machine_default.start() 35 66 machine_default.wait_for_unit("glance.service") 36 67 machine_default.wait_for_open_port(8080) 37 68 38 - machine_custom_port.start() 39 - machine_custom_port.wait_for_unit("glance.service") 40 - machine_custom_port.wait_for_open_port(5678) 69 + machine_configured.start() 70 + machine_configured.wait_for_unit("glance.service") 71 + machine_configured.wait_for_open_port(5678) 41 72 42 73 soup = BeautifulSoup(machine_default.succeed("curl http://localhost:8080")) 43 74 expected_version = "v${config.nodes.machine_default.services.glance.package.version}" 44 75 assert any(a.text == expected_version for a in soup.select(".footer a")) 76 + 77 + yaml_contents = machine_configured.succeed("cat /run/glance/glance.yaml") 78 + yaml_parsed = yaml.load(yaml_contents, Loader=yaml.FullLoader) 79 + location = yaml_parsed["pages"][0]["columns"][0]["widgets"][1]["location"] 80 + assert location == "Nivelles, Belgium" 45 81 ''; 46 82 47 83 meta.maintainers = [ lib.maintainers.drupol ];