lol

nixos/meilisearch: generic settings; handle secrets better. + fix racy test (#424481)

authored by

Yt and committed by
GitHub
ff3ffc81 4e64a7de

+181 -114
+1 -3
nixos/doc/manual/redirects.json
··· 888 888 "index.html#module-services-meilisearch-quickstart-search" 889 889 ], 890 890 "module-services-meilisearch-defaults": [ 891 - "index.html#module-services-meilisearch-defaults" 892 - ], 893 - "module-services-meilisearch-missing": [ 891 + "index.html#module-services-meilisearch-defaults", 894 892 "index.html#module-services-meilisearch-missing" 895 893 ], 896 894 "module-services-networking-yggdrasil": [
+6 -4
nixos/modules/services/search/meilisearch.md
··· 32 32 33 33 - The default nixos package doesn't come with the [dashboard](https://docs.meilisearch.com/learn/getting_started/quick_start.html#search), since the dashboard features makes some assets downloads at compile time. 34 34 35 - - Anonymized Analytics sent to meilisearch are disabled by default. 35 + - `no_analytics` is set to true by default. 36 36 37 - - Default deployment is development mode. It doesn't require a secret master key. All routes are not protected and accessible. 37 + - `http_addr` is derived from {option}`services.meilisearch.listenAddress` and {option}`services.meilisearch.listenPort`. The two sub-fields are separate because this makes it easier to consume in certain other modules. 38 38 39 - ## Missing {#module-services-meilisearch-missing} 39 + - `db_path` is set to `/var/lib/meilisearch` by default. Upstream, the default value is equivalent to `/var/lib/meilisearch/data.ms`. 40 + 41 + - `dump_dir` and `snapshot_dir` are set to `/var/lib/meilisearch/dumps` and `/var/lib/meilisearch/snapshots`, respectively. This is equivalent to the upstream defaults. 40 42 41 - - the snapshot feature is not yet configurable from the module, it's just a matter of adding the relevant environment variables. 43 + - All other options inherit their upstream defaults. In particular, the default configuration uses `env = "development"`, which doesn't require a master key, in which case all routes are unprotected.
+160 -90
nixos/modules/services/search/meilisearch.nix
··· 7 7 let 8 8 cfg = config.services.meilisearch; 9 9 10 + settingsFormat = pkgs.formats.toml { }; 11 + 12 + # These secrets are used in the config file and can be set to paths. 13 + secrets-with-path = 14 + builtins.map 15 + ( 16 + { environment, name }: 17 + { 18 + inherit name environment; 19 + setting = cfg.settings.${name}; 20 + } 21 + ) 22 + [ 23 + { 24 + environment = "MEILI_SSL_CERT_PATH"; 25 + name = "ssl_cert_path"; 26 + } 27 + { 28 + environment = "MEILI_SSL_KEY_PATH"; 29 + name = "ssl_key_path"; 30 + } 31 + { 32 + environment = "MEILI_SSL_AUTH_PATH"; 33 + name = "ssl_auth_path"; 34 + } 35 + { 36 + environment = "MEILI_SSL_OCSP_PATH"; 37 + name = "ssl_ocsp_path"; 38 + } 39 + ]; 40 + 41 + # We also handle `master_key` separately. 42 + # It cannot be set to a path, so we template it. 43 + master-key-placeholder = "@MASTER_KEY@"; 44 + 45 + configFile = settingsFormat.generate "config.toml" ( 46 + builtins.removeAttrs ( 47 + if cfg.masterKeyFile != null then 48 + cfg.settings // { master_key = master-key-placeholder; } 49 + else 50 + builtins.removeAttrs cfg.settings [ "master_key" ] 51 + ) (map (secret: secret.name) secrets-with-path) 52 + ); 53 + 10 54 in 11 55 { 12 - 13 56 meta.maintainers = with lib.maintainers; [ 14 57 Br1ght0ne 15 58 happysalada 16 59 ]; 17 60 meta.doc = ./meilisearch.md; 18 61 19 - ###### interface 62 + imports = [ 63 + (lib.mkRenamedOptionModule 64 + [ "services" "meilisearch" "environment" ] 65 + [ "services" "meilisearch" "settings" "env" ] 66 + ) 67 + (lib.mkRenamedOptionModule 68 + [ "services" "meilisearch" "logLevel" ] 69 + [ "services" "meilisearch" "settings" "log_level" ] 70 + ) 71 + (lib.mkRenamedOptionModule 72 + [ "services" "meilisearch" "noAnalytics" ] 73 + [ "services" "meilisearch" "settings" "no_analytics" ] 74 + ) 75 + (lib.mkRenamedOptionModule 76 + [ "services" "meilisearch" "maxIndexSize" ] 77 + [ "services" "meilisearch" "settings" "max_index_size" ] 78 + ) 79 + (lib.mkRenamedOptionModule 80 + [ "services" "meilisearch" "payloadSizeLimit" ] 81 + [ "services" "meilisearch" "settings" "http_payload_size_limit" ] 82 + ) 83 + (lib.mkRenamedOptionModule 84 + [ "services" "meilisearch" "dumplessUpgrade" ] 85 + [ "services" "meilisearch" "settings" "experimental_dumpless_upgrade" ] 86 + ) 87 + (lib.mkRemovedOptionModule [ "services" "meilisearch" "masterKeyEnvironmentFile" ] '' 88 + Use `services.meilisearch.masterKeyFile` instead. It does not require you to prefix the file with "MEILI_MASTER_KEY=". 89 + If you were abusing this option to set other options, you can now configure them with `services.meilisearch.settings`. 90 + '') 91 + ]; 20 92 21 93 options.services.meilisearch = { 22 - enable = lib.mkEnableOption "MeiliSearch - a RESTful search API"; 94 + enable = lib.mkEnableOption "Meilisearch - a RESTful search API"; 23 95 24 96 package = lib.mkPackageOption pkgs "meilisearch" { 25 97 extraDescription = '' ··· 28 100 }; 29 101 30 102 listenAddress = lib.mkOption { 31 - description = "MeiliSearch listen address."; 32 - default = "127.0.0.1"; 103 + default = "localhost"; 33 104 type = lib.types.str; 105 + description = '' 106 + The IP address that Meilisearch will listen on. 107 + 108 + It can also be a hostname like "localhost". If it resolves to an IPv4 and IPv6 address, Meilisearch will listen on both. 109 + ''; 34 110 }; 35 111 36 112 listenPort = lib.mkOption { 37 - description = "MeiliSearch port to listen on."; 38 113 default = 7700; 39 114 type = lib.types.port; 40 - }; 41 - 42 - environment = lib.mkOption { 43 - description = "Defines the running environment of MeiliSearch."; 44 - default = "development"; 45 - type = lib.types.enum [ 46 - "development" 47 - "production" 48 - ]; 115 + description = '' 116 + The port that Meilisearch will listen on. 117 + ''; 49 118 }; 50 119 51 - # TODO change this to LoadCredentials once possible 52 - masterKeyEnvironmentFile = lib.mkOption { 120 + masterKeyFile = lib.mkOption { 53 121 description = '' 54 122 Path to file which contains the master key. 55 123 By doing so, all routes will be protected and will require a key to be accessed. 56 124 If no master key is provided, all routes can be accessed without requiring any key. 57 - The format is the following: 58 - MEILI_MASTER_KEY=my_secret_key 59 125 ''; 60 126 default = null; 61 - type = with lib.types; nullOr path; 62 - }; 63 - 64 - noAnalytics = lib.mkOption { 65 - description = '' 66 - Deactivates analytics. 67 - Analytics allow MeiliSearch to know how many users are using MeiliSearch, 68 - which versions and which platforms are used. 69 - This process is entirely anonymous. 70 - ''; 71 - default = true; 72 - type = lib.types.bool; 73 - }; 74 - 75 - logLevel = lib.mkOption { 76 - description = '' 77 - Defines how much detail should be present in MeiliSearch's logs. 78 - MeiliSearch currently supports four log levels, listed in order of increasing verbosity: 79 - - 'ERROR': only log unexpected events indicating MeiliSearch is not functioning as expected 80 - - 'WARN:' log all unexpected events, regardless of their severity 81 - - 'INFO:' log all events. This is the default value 82 - - 'DEBUG': log all events and including detailed information on MeiliSearch's internal processes. 83 - Useful when diagnosing issues and debugging 84 - ''; 85 - default = "INFO"; 86 - type = lib.types.str; 127 + type = lib.types.nullOr lib.types.path; 87 128 }; 88 129 89 - maxIndexSize = lib.mkOption { 130 + settings = lib.mkOption { 90 131 description = '' 91 - Sets the maximum size of the index. 92 - Value must be given in bytes or explicitly stating a base unit. 93 - For example, the default value can be written as 107374182400, '107.7Gb', or '107374 Mb'. 94 - Default is 100 GiB 132 + Configuration settings for Meilisearch. 133 + Look at the documentation for available options: 134 + https://github.com/meilisearch/meilisearch/blob/main/config.toml 135 + https://www.meilisearch.com/docs/learn/self_hosted/configure_meilisearch_at_launch#all-instance-options 95 136 ''; 96 - default = "107374182400"; 97 - type = lib.types.str; 98 - }; 99 137 100 - payloadSizeLimit = lib.mkOption { 101 - description = '' 102 - Sets the maximum size of accepted JSON payloads. 103 - Value must be given in bytes or explicitly stating a base unit. 104 - For example, the default value can be written as 107374182400, '107.7Gb', or '107374 Mb'. 105 - Default is ~ 100 MB 106 - ''; 107 - default = "104857600"; 108 - type = lib.types.str; 109 - }; 138 + default = { }; 110 139 111 - # TODO: turn on by default when it stops being experimental 112 - dumplessUpgrade = lib.mkOption { 113 - default = false; 114 - example = true; 115 - description = '' 116 - Whether to enable (experimental) dumpless upgrade. 140 + type = lib.types.submodule { 141 + freeformType = settingsFormat.type; 117 142 118 - Allows upgrading from Meilisearch >=v1.12 to Meilisearch >=v1.13 without manually 119 - dumping and importing the database. 143 + imports = builtins.map (secret: { 144 + # give them proper types, just so they're easier to consume from this file 145 + options.${secret.name} = lib.mkOption { 146 + # but they should not show up in documentation as special in any way. 147 + visible = false; 120 148 121 - More information at https://www.meilisearch.com/docs/learn/update_and_migration/updating#dumpless-upgrade 122 - ''; 123 - type = lib.types.bool; 149 + type = lib.types.nullOr lib.types.path; 150 + default = null; 151 + }; 152 + }) secrets-with-path; 153 + }; 124 154 }; 155 + }; 125 156 126 - }; 157 + config = lib.mkIf cfg.enable { 158 + assertions = [ 159 + { 160 + assertion = !cfg.settings ? master_key; 161 + message = '' 162 + Do not set `services.meilisearch.settings.master_key` in your configuration. 163 + Use `services.meilisearch.masterKeyFile` instead. 164 + ''; 165 + } 166 + ]; 127 167 128 - ###### implementation 168 + services.meilisearch.settings = { 169 + # we use `listenAddress` and `listenPort` to derive the `http_addr` setting. 170 + # this is the only setting we treat like this. 171 + # we do this because some dependent services like Misskey/Sharkey need separate host,port for no good reason. 172 + http_addr = "${cfg.listenAddress}:${toString cfg.listenPort}"; 173 + 174 + # upstream's default for `db_path` is `/var/lib/meilisearch/data.ms/`, but ours is different for no reason. 175 + db_path = lib.mkDefault "/var/lib/meilisearch"; 176 + # these are equivalent to the upstream defaults, because we set a working directory. 177 + # they are only set here for consistency with `db_path`. 178 + dump_dir = lib.mkDefault "/var/lib/meilisearch/dumps"; 179 + snapshot_dir = lib.mkDefault "/var/lib/meilisearch/snapshots"; 129 180 130 - config = lib.mkIf cfg.enable { 181 + # this is intentionally different from upstream's default. 182 + no_analytics = lib.mkDefault true; 183 + }; 131 184 132 185 warnings = lib.optional (lib.versionOlder cfg.package.version "1.12") '' 133 186 Meilisearch 1.11 will be removed in NixOS 25.11. As it was the last ··· 149 202 environment.systemPackages = [ cfg.package ]; 150 203 151 204 systemd.services.meilisearch = { 152 - description = "MeiliSearch daemon"; 205 + description = "Meilisearch daemon"; 153 206 wantedBy = [ "multi-user.target" ]; 154 207 after = [ "network.target" ]; 155 - environment = { 156 - MEILI_DB_PATH = "/var/lib/meilisearch"; 157 - MEILI_HTTP_ADDR = "${cfg.listenAddress}:${toString cfg.listenPort}"; 158 - MEILI_NO_ANALYTICS = lib.boolToString cfg.noAnalytics; 159 - MEILI_ENV = cfg.environment; 160 - MEILI_DUMP_DIR = "/var/lib/meilisearch/dumps"; 161 - MEILI_LOG_LEVEL = cfg.logLevel; 162 - MEILI_MAX_INDEX_SIZE = cfg.maxIndexSize; 163 - MEILI_EXPERIMENTAL_DUMPLESS_UPGRADE = lib.boolToString cfg.dumplessUpgrade; 164 - }; 208 + 209 + preStart = lib.mkMerge [ 210 + '' 211 + install -m 700 '${configFile}' "$RUNTIME_DIRECTORY/config.toml" 212 + '' 213 + (lib.mkIf (cfg.masterKeyFile != null) '' 214 + ${lib.getExe pkgs.replace-secret} '${master-key-placeholder}' "$CREDENTIALS_DIRECTORY/master_key" "$RUNTIME_DIRECTORY/config.toml" 215 + '') 216 + ]; 217 + 218 + environment = builtins.listToAttrs ( 219 + builtins.map (secret: { 220 + name = secret.environment; 221 + value = lib.mkIf (secret.setting != null) "%d/${secret.name}"; 222 + }) secrets-with-path 223 + ); 224 + 165 225 serviceConfig = { 166 - ExecStart = "${cfg.package}/bin/meilisearch"; 226 + LoadCredential = lib.mkMerge ( 227 + [ 228 + (lib.mkIf (cfg.masterKeyFile != null) [ "master_key:${cfg.masterKeyFile}" ]) 229 + ] 230 + ++ builtins.map ( 231 + secret: lib.mkIf (secret.setting != null) [ "${secret.name}:${secret.setting}" ] 232 + ) secrets-with-path 233 + ); 234 + ExecStart = "${lib.getExe cfg.package} --config-file-path \${RUNTIME_DIRECTORY}/config.toml"; 167 235 DynamicUser = true; 168 236 StateDirectory = "meilisearch"; 169 - EnvironmentFile = lib.mkIf (cfg.masterKeyEnvironmentFile != null) cfg.masterKeyEnvironmentFile; 237 + WorkingDirectory = "%S/meilisearch"; 238 + RuntimeDirectory = "meilisearch"; 239 + RuntimeDirectoryMode = "0700"; 170 240 }; 171 241 }; 172 242 };
+2 -2
nixos/modules/services/web-apps/sharkey.nix
··· 54 54 description = '' 55 55 Whether to automatically set up a local Meilisearch instance and configure Sharkey to use it. 56 56 57 - You need to ensure `services.meilisearch.masterKeyEnvironmentFile` is correctly configured for a working 57 + You need to ensure `services.meilisearch.masterKeyFile` is correctly configured for a working 58 58 Meilisearch setup. You also need to configure Sharkey to use an API key obtained from Meilisearch with the 59 59 `MK_CONFIG_MEILISEARCH_APIKEY` environment variable, and set `services.sharkey.settings.meilisearch.index` to 60 60 the created index. See https://docs.joinsharkey.org/docs/customisation/search/meilisearch/ for how to create ··· 240 240 (mkIf cfg.setupMeilisearch { 241 241 services.meilisearch = { 242 242 enable = mkDefault true; 243 - environment = mkDefault "production"; 243 + settings.env = mkDefault "production"; 244 244 }; 245 245 246 246 services.sharkey.settings = {
+11 -12
nixos/tests/meilisearch.nix
··· 35 35 machine.wait_for_unit("meilisearch") 36 36 machine.wait_for_open_port(7700) 37 37 38 + def wait_task(cmd): 39 + response = json.loads(machine.succeed(cmd)) 40 + task_uid = response["taskUid"] 41 + machine.wait_until_succeeds( 42 + f"curl ${apiUrl}/tasks/{task_uid} | jq -e '.status | IN(\"succeeded\", \"failed\", \"canceled\")'" 43 + ) 44 + machine.succeed(f"curl ${apiUrl}/tasks/{task_uid} | jq -e '.status == \"succeeded\"'") 45 + return response 46 + 38 47 with subtest("check version"): 39 48 version = json.loads(machine.succeed("curl ${apiUrl}/version")) 40 49 assert version["pkgVersion"] == "${pkgs.meilisearch.version}" 41 50 42 51 with subtest("create index"): 43 - machine.succeed( 44 - "curl -X POST -H 'Content-Type: application/json' ${apiUrl}/indexes --data @${indexJSON}" 45 - ) 52 + wait_task("curl -X POST -H 'Content-Type: application/json' ${apiUrl}/indexes --data @${indexJSON}") 46 53 indexes = json.loads(machine.succeed("curl ${apiUrl}/indexes")) 47 54 assert indexes["total"] == 1, "index wasn't created" 48 55 49 56 with subtest("add documents"): 50 - response = json.loads( 51 - machine.succeed( 52 - "curl -X POST -H 'Content-Type: application/json' ${apiUrl}/indexes/${uid}/documents --data-binary @${moviesJSON}" 53 - ) 54 - ) 55 - task_uid = response["taskUid"] 56 - machine.wait_until_succeeds( 57 - f"curl ${apiUrl}/tasks/{task_uid} | jq -e '.status == \"succeeded\"'" 58 - ) 57 + wait_task("curl -X POST -H 'Content-Type: application/json' ${apiUrl}/indexes/${uid}/documents --data-binary @${moviesJSON}") 59 58 60 59 with subtest("search"): 61 60 response = json.loads(
+1 -3
nixos/tests/web-apps/sharkey.nix
··· 19 19 }; 20 20 }; 21 21 22 - services.meilisearch.masterKeyEnvironmentFile = pkgs.writeText "meilisearch-key" '' 23 - MEILI_MASTER_KEY=${meilisearchKey} 24 - ''; 22 + services.meilisearch.masterKeyFile = pkgs.writeText "meilisearch-key" meilisearchKey; 25 23 }; 26 24 27 25 testScript =