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 services.postgresql = lib.optionalAttrs (cfg.database.createLocally) {
95 enable = lib.mkDefault true;
96
97 ensureDatabases = [ cfg.database.name ];
98 ensureUsers = [
99 {
100 name = cfg.database.user;
101 ensureDBOwnership = true;
102 }
103 ];
104
105 };
106
107 systemd.services =
108 let
109 useUrlPath = (cfg.database.urlPath != null);
110 serviceConfig =
111 {
112 DynamicUser = true;
113 # using the same user to simplify db connection
114 User = cfg.database.user;
115 ExecStart = "${pkgs.windmill}/bin/windmill";
116
117 Restart = "always";
118 }
119 // lib.optionalAttrs useUrlPath {
120 LoadCredential = [
121 "DATABASE_URL_FILE:${cfg.database.urlPath}"
122 ];
123 };
124 db_url_envs =
125 lib.optionalAttrs useUrlPath {
126 DATABASE_URL_FILE = "%d/DATABASE_URL_FILE";
127 }
128 // lib.optionalAttrs (!useUrlPath) {
129 DATABASE_URL = cfg.database.url;
130 };
131 in
132 {
133
134 # coming from https://github.com/windmill-labs/windmill/blob/main/init-db-as-superuser.sql
135 # modified to not grant priviledges on all tables
136 # create role windmill_user and windmill_admin only if they don't exist
137 postgresql.postStart = lib.mkIf cfg.database.createLocally (
138 lib.mkAfter ''
139 $PSQL -tA <<"EOF"
140 DO $$
141 BEGIN
142 IF NOT EXISTS (
143 SELECT FROM pg_catalog.pg_roles
144 WHERE rolname = 'windmill_user'
145 ) THEN
146 CREATE ROLE windmill_user;
147 GRANT ALL PRIVILEGES ON DATABASE ${cfg.database.name} TO windmill_user;
148 ELSE
149 RAISE NOTICE 'Role "windmill_user" already exists. Skipping.';
150 END IF;
151 IF NOT EXISTS (
152 SELECT FROM pg_catalog.pg_roles
153 WHERE rolname = 'windmill_admin'
154 ) THEN
155 CREATE ROLE windmill_admin WITH BYPASSRLS;
156 GRANT windmill_user TO windmill_admin;
157 ELSE
158 RAISE NOTICE 'Role "windmill_admin" already exists. Skipping.';
159 END IF;
160 GRANT windmill_admin TO windmill;
161 END
162 $$;
163 EOF
164 ''
165 );
166
167 windmill-server = {
168 description = "Windmill server";
169 after = [ "network.target" ] ++ lib.optional cfg.database.createLocally "postgresql.service";
170 wantedBy = [ "multi-user.target" ];
171
172 serviceConfig = serviceConfig // {
173 StateDirectory = "windmill";
174 };
175
176 environment = {
177 PORT = builtins.toString cfg.serverPort;
178 WM_BASE_URL = cfg.baseUrl;
179 RUST_LOG = cfg.logLevel;
180 MODE = "server";
181 } // db_url_envs;
182 };
183
184 windmill-worker = {
185 description = "Windmill worker";
186 after = [ "network.target" ] ++ lib.optional cfg.database.createLocally "postgresql.service";
187 wantedBy = [ "multi-user.target" ];
188
189 serviceConfig = serviceConfig // {
190 StateDirectory = "windmill-worker";
191 };
192
193 environment = {
194 WM_BASE_URL = cfg.baseUrl;
195 RUST_LOG = cfg.logLevel;
196 MODE = "worker";
197 WORKER_GROUP = "default";
198 KEEP_JOB_DIR = "false";
199 } // db_url_envs;
200 };
201
202 windmill-worker-native = {
203 description = "Windmill worker native";
204 after = [ "network.target" ] ++ lib.optional cfg.database.createLocally "postgresql.service";
205 wantedBy = [ "multi-user.target" ];
206
207 serviceConfig = serviceConfig // {
208 StateDirectory = "windmill-worker-native";
209 };
210
211 environment = {
212 WM_BASE_URL = cfg.baseUrl;
213 RUST_LOG = cfg.logLevel;
214 MODE = "worker";
215 WORKER_GROUP = "native";
216 } // db_url_envs;
217 };
218 };
219 };
220}