at master 8.8 kB view raw
1{ 2 config, 3 pkgs, 4 lib, 5 ... 6}: 7 8let 9 cfg = config.services.windmill; 10in 11{ 12 options.services.windmill = { 13 enable = lib.mkEnableOption "windmill service"; 14 15 serverPort = lib.mkOption { 16 type = lib.types.port; 17 default = 8001; 18 description = "Port the windmill server listens on."; 19 }; 20 21 lspPort = lib.mkOption { 22 type = lib.types.port; 23 default = 3001; 24 description = "Port the windmill lsp listens on."; 25 }; 26 27 database = { 28 name = lib.mkOption { 29 type = lib.types.str; 30 # the simplest database setup is to have the database named like the user. 31 default = "windmill"; 32 description = "Database name."; 33 }; 34 35 user = lib.mkOption { 36 type = lib.types.str; 37 # the simplest database setup is to have the database user like the name. 38 default = "windmill"; 39 description = "Database user."; 40 }; 41 42 url = lib.mkOption { 43 type = lib.types.str; 44 default = "postgres://${config.services.windmill.database.name}?host=/var/run/postgresql"; 45 defaultText = lib.literalExpression '' 46 "postgres://\$\{config.services.windmill.database.name}?host=/var/run/postgresql"; 47 ''; 48 description = "Database url. Note that any secret here would be world-readable. Use `services.windmill.database.urlPath` unstead to include secrets in the url."; 49 }; 50 51 urlPath = lib.mkOption { 52 type = lib.types.nullOr lib.types.path; 53 description = '' 54 Path to the file containing the database url windmill should connect to. This is not deducted from database user and name as it might contain a secret 55 ''; 56 default = null; 57 example = "config.age.secrets.DATABASE_URL_FILE.path"; 58 }; 59 60 createLocally = lib.mkOption { 61 type = lib.types.bool; 62 default = true; 63 description = "Whether to create a local database automatically."; 64 }; 65 }; 66 67 baseUrl = lib.mkOption { 68 type = lib.types.str; 69 default = "https://localhost:${toString config.services.windmill.serverPort}"; 70 defaultText = lib.literalExpression '' 71 "https://localhost:\$\{toString config.services.windmill.serverPort}"; 72 ''; 73 description = '' 74 The base url that windmill will be served on. 75 ''; 76 example = "https://windmill.example.com"; 77 }; 78 79 logLevel = lib.mkOption { 80 type = lib.types.enum [ 81 "error" 82 "warn" 83 "info" 84 "debug" 85 "trace" 86 ]; 87 default = "info"; 88 description = "Log level"; 89 }; 90 }; 91 92 config = lib.mkIf cfg.enable { 93 94 assertions = [ 95 { 96 assertion = cfg.database.createLocally -> cfg.database.name == cfg.database.user; 97 message = '' 98 Automatically provisioning the windmill database requires both database name and database user to be equal. '${cfg.database.name}' != '${cfg.database.user}' 99 To fix this problem, assign the same value to both options services.windmill.database.{name,user}. 100 ''; 101 } 102 ]; 103 104 services.postgresql = lib.optionalAttrs (cfg.database.createLocally) { 105 enable = lib.mkDefault true; 106 107 ensureDatabases = [ cfg.database.name ]; 108 ensureUsers = [ 109 { 110 name = cfg.database.user; 111 ensureDBOwnership = true; 112 } 113 ]; 114 }; 115 116 systemd.targets.windmill = { 117 description = "Windmill"; 118 wantedBy = [ "multi-user.target" ]; 119 requires = 120 [ ] 121 ++ (lib.optionals config.systemd.services.windmill-server.enable [ "windmill-server.service" ]) 122 ++ (lib.optionals config.systemd.services.windmill-worker.enable [ "windmill-worker.service" ]) 123 ++ (lib.optionals config.systemd.services.windmill-worker-native.enable [ 124 "windmill-worker-native.service" 125 ]); 126 }; 127 128 systemd.services = 129 let 130 useUrlPath = (cfg.database.urlPath != null); 131 serviceConfig = { 132 DynamicUser = true; 133 # using the same user to simplify db connection 134 User = cfg.database.user; 135 ExecStart = "${pkgs.windmill}/bin/windmill"; 136 Restart = "always"; 137 } 138 // lib.optionalAttrs useUrlPath { 139 LoadCredential = [ 140 "DATABASE_URL_FILE:${cfg.database.urlPath}" 141 ]; 142 }; 143 db_url_envs = 144 lib.optionalAttrs useUrlPath { 145 DATABASE_URL_FILE = "%d/DATABASE_URL_FILE"; 146 } 147 // lib.optionalAttrs (!useUrlPath) { 148 DATABASE_URL = cfg.database.url; 149 }; 150 in 151 { 152 windmill-initdb = lib.mkIf cfg.database.createLocally { 153 description = "Windmill database setup"; 154 requires = [ "postgresql.target" ]; 155 after = [ "postgresql.target" ]; 156 requiredBy = 157 [ ] 158 ++ (lib.optionals config.systemd.services.windmill-server.enable [ "windmill-server.service" ]) 159 ++ (lib.optionals config.systemd.services.windmill-worker.enable [ "windmill-worker.service" ]) 160 ++ (lib.optionals config.systemd.services.windmill-worker-native.enable [ 161 "windmill-worker-native.service" 162 ]); 163 before = 164 [ ] 165 ++ (lib.optionals config.systemd.services.windmill-server.enable [ "windmill-server.service" ]) 166 ++ (lib.optionals config.systemd.services.windmill-worker.enable [ "windmill-worker.service" ]) 167 ++ (lib.optionals config.systemd.services.windmill-worker-native.enable [ 168 "windmill-worker-native.service" 169 ]); 170 171 path = [ config.services.postgresql.package ]; 172 # coming from https://github.com/windmill-labs/windmill/blob/main/init-db-as-superuser.sql 173 # modified to not grant privileges on all tables 174 # create role windmill_user and windmill_admin only if they don't exist 175 script = '' 176 psql -tA <<"EOF" 177 DO $$ 178 BEGIN 179 IF NOT EXISTS ( 180 SELECT FROM pg_catalog.pg_roles 181 WHERE rolname = 'windmill_user' 182 ) THEN 183 CREATE ROLE windmill_user; 184 GRANT ALL PRIVILEGES ON DATABASE ${cfg.database.name} TO windmill_user; 185 ELSE 186 RAISE NOTICE 'Role "windmill_user" already exists. Skipping.'; 187 END IF; 188 IF NOT EXISTS ( 189 SELECT FROM pg_catalog.pg_roles 190 WHERE rolname = 'windmill_admin' 191 ) THEN 192 CREATE ROLE windmill_admin WITH BYPASSRLS; 193 GRANT windmill_user TO windmill_admin; 194 ELSE 195 RAISE NOTICE 'Role "windmill_admin" already exists. Skipping.'; 196 END IF; 197 GRANT windmill_admin TO ${cfg.database.user}; 198 END 199 $$; 200 EOF 201 ''; 202 203 serviceConfig = { 204 Type = "oneshot"; 205 RemainAfterExit = true; 206 # Superuser because of required permission CREATE ROLE 207 User = "postgres"; 208 209 ProtectSystem = "strict"; 210 ProtectHome = "read-only"; 211 }; 212 }; 213 214 windmill-server = { 215 description = "Windmill server"; 216 after = [ "network.target" ]; 217 partOf = [ "windmill.target" ]; 218 219 serviceConfig = serviceConfig // { 220 StateDirectory = "windmill"; 221 }; 222 223 environment = { 224 PORT = builtins.toString cfg.serverPort; 225 WM_BASE_URL = cfg.baseUrl; 226 RUST_LOG = cfg.logLevel; 227 MODE = "server"; 228 } 229 // db_url_envs; 230 }; 231 232 windmill-worker = { 233 description = "Windmill worker"; 234 after = [ "network.target" ]; 235 partOf = [ "windmill.target" ]; 236 237 serviceConfig = serviceConfig // { 238 StateDirectory = "windmill-worker"; 239 }; 240 241 environment = { 242 WM_BASE_URL = cfg.baseUrl; 243 RUST_LOG = cfg.logLevel; 244 MODE = "worker"; 245 WORKER_GROUP = "default"; 246 KEEP_JOB_DIR = "false"; 247 } 248 // db_url_envs; 249 }; 250 251 windmill-worker-native = { 252 description = "Windmill worker native"; 253 after = [ "network.target" ]; 254 partOf = [ "windmill.target" ]; 255 256 serviceConfig = serviceConfig // { 257 StateDirectory = "windmill-worker-native"; 258 }; 259 260 environment = { 261 WM_BASE_URL = cfg.baseUrl; 262 RUST_LOG = cfg.logLevel; 263 MODE = "worker"; 264 WORKER_GROUP = "native"; 265 } 266 // db_url_envs; 267 }; 268 }; 269 }; 270}