confctl 2.2.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.git-hooks/pre_commit/nixfmt.rb +13 -0
  3. data/.github/workflows/rspec.yml +64 -0
  4. data/.github/workflows/rubocop.yml +27 -0
  5. data/.github/workflows/tests.yml +139 -0
  6. data/.gitignore +12 -7
  7. data/.overcommit.yml +2 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +8 -1
  10. data/AGENTS.md +42 -0
  11. data/CHANGELOG.md +57 -0
  12. data/Gemfile +3 -2
  13. data/Gemfile.lock +198 -0
  14. data/README.md +166 -58
  15. data/Rakefile +5 -0
  16. data/confctl.gemspec +20 -2
  17. data/docs/carrier.md +3 -3
  18. data/docs/flake-inputs.md +159 -0
  19. data/docs/swpins-to-flakes.md +325 -0
  20. data/example/cluster/module-list.nix +2 -1
  21. data/example/cluster/nixos-machine/config.nix +12 -2
  22. data/example/cluster/nixos-machine/hardware.nix +6 -1
  23. data/example/cluster/vpsadminos-container/config.nix +6 -1
  24. data/example/cluster/vpsadminos-container/module.nix +4 -1
  25. data/example/cluster/vpsadminos-machine/config.nix +6 -1
  26. data/example/cluster/vpsadminos-machine/hardware.nix +6 -1
  27. data/example/cluster/vpsadminos-machine/module.nix +5 -2
  28. data/example/cluster/vpsfreecz-vps/config.nix +6 -1
  29. data/example/cluster/vpsfreecz-vps/module.nix +4 -1
  30. data/example/configs/swpins.nix +8 -3
  31. data/example/environments/base.nix +6 -1
  32. data/example/swpins/core.json +35 -0
  33. data/example-flake/.gitignore +2 -0
  34. data/example-flake/README.md +38 -0
  35. data/example-flake/cluster/cluster.nix +5 -0
  36. data/example-flake/cluster/module-list.nix +4 -0
  37. data/example-flake/cluster/nested/nixos-machine/config.nix +25 -0
  38. data/example-flake/cluster/nested/nixos-machine/hardware.nix +9 -0
  39. data/example-flake/cluster/nested/nixos-machine/module.nix +8 -0
  40. data/example-flake/cluster/nixos-machine/config.nix +25 -0
  41. data/example-flake/cluster/nixos-machine/hardware.nix +9 -0
  42. data/example-flake/cluster/nixos-machine/module.nix +8 -0
  43. data/example-flake/cluster/vpsadminos-container/config.nix +28 -0
  44. data/example-flake/cluster/vpsadminos-container/module.nix +8 -0
  45. data/example-flake/cluster/vpsadminos-machine/config.nix +27 -0
  46. data/example-flake/cluster/vpsadminos-machine/hardware.nix +9 -0
  47. data/example-flake/cluster/vpsadminos-machine/module.nix +8 -0
  48. data/example-flake/cluster/vpsfreecz-vps/config.nix +31 -0
  49. data/example-flake/cluster/vpsfreecz-vps/module.nix +8 -0
  50. data/example-flake/configs/confctl.nix +10 -0
  51. data/example-flake/data/default.nix +5 -0
  52. data/example-flake/data/ssh-keys.nix +7 -0
  53. data/example-flake/environments/base.nix +18 -0
  54. data/example-flake/flake.lock +75 -0
  55. data/example-flake/flake.nix +36 -0
  56. data/example-flake/modules/module-list.nix +13 -0
  57. data/example-flake/shell.nix +11 -0
  58. data/flake.lock +159 -0
  59. data/flake.nix +189 -0
  60. data/gemset.nix +1022 -0
  61. data/lib/confctl/cli/app.rb +145 -0
  62. data/lib/confctl/cli/attr_filters.rb +1 -1
  63. data/lib/confctl/cli/cluster.rb +681 -108
  64. data/lib/confctl/cli/command.rb +24 -2
  65. data/lib/confctl/cli/configuration.rb +171 -106
  66. data/lib/confctl/cli/generation.rb +65 -1
  67. data/lib/confctl/cli/inputs/channels.rb +190 -0
  68. data/lib/confctl/cli/inputs/machines.rb +83 -0
  69. data/lib/confctl/cli/inputs/root.rb +101 -0
  70. data/lib/confctl/cli/inputs.rb +5 -0
  71. data/lib/confctl/cli/log_view.rb +21 -10
  72. data/lib/confctl/cli/migrate/swpins_to_flakes.rb +866 -0
  73. data/lib/confctl/cli/migrate.rb +5 -0
  74. data/lib/confctl/cli/output_formatter.rb +5 -7
  75. data/lib/confctl/cli/swpins/base.rb +9 -0
  76. data/lib/confctl/cli/swpins/channel.rb +2 -5
  77. data/lib/confctl/cli/swpins/cluster.rb +2 -5
  78. data/lib/confctl/cli/swpins/core.rb +2 -5
  79. data/lib/confctl/config_type.rb +7 -0
  80. data/lib/confctl/flake_lock.rb +78 -0
  81. data/lib/confctl/flake_lock_diff.rb +36 -0
  82. data/lib/confctl/generation/build.rb +131 -24
  83. data/lib/confctl/generation/build_list.rb +4 -3
  84. data/lib/confctl/generation/unified.rb +14 -1
  85. data/lib/confctl/git_repo_mirror.rb +2 -2
  86. data/lib/confctl/health_checks/run_command.rb +3 -2
  87. data/lib/confctl/health_checks/systemd/properties.rb +1 -1
  88. data/lib/confctl/health_checks/systemd/property_list.rb +2 -2
  89. data/lib/confctl/inputs/commit_message.rb +125 -0
  90. data/lib/confctl/inputs/git_commit.rb +17 -0
  91. data/lib/confctl/inputs/nix_output_guard.rb +37 -0
  92. data/lib/confctl/inputs/setter.rb +179 -0
  93. data/lib/confctl/inputs/updater.rb +76 -0
  94. data/lib/confctl/inputs.rb +5 -0
  95. data/lib/confctl/inputs_info.rb +50 -0
  96. data/lib/confctl/line_buffer.rb +1 -1
  97. data/lib/confctl/machine.rb +18 -3
  98. data/lib/confctl/machine_control.rb +8 -2
  99. data/lib/confctl/machine_list.rb +2 -2
  100. data/lib/confctl/machine_status.rb +63 -20
  101. data/lib/confctl/nix/args.rb +48 -0
  102. data/lib/confctl/nix.rb +30 -437
  103. data/lib/confctl/nix_build_flake.rb +95 -0
  104. data/lib/confctl/nix_copy.rb +14 -2
  105. data/lib/confctl/nix_flake.rb +449 -0
  106. data/lib/confctl/nix_format.rb +3 -3
  107. data/lib/confctl/nix_legacy.rb +467 -0
  108. data/lib/confctl/swpins/change_set.rb +29 -3
  109. data/lib/confctl/swpins/specs/base.rb +2 -2
  110. data/lib/confctl/ui.rb +19 -0
  111. data/lib/confctl/version.rb +1 -1
  112. data/man/index.html +11 -0
  113. data/man/man8/confctl-options.nix.8.html +113 -0
  114. data/man/man8/confctl.8 +152 -21
  115. data/man/man8/confctl.8.html +355 -0
  116. data/man/man8/confctl.8.md +148 -17
  117. data/man/style.css +301 -0
  118. data/nix/evaluator.nix +94 -69
  119. data/nix/flake/mk-confctl-devshell.nix +85 -0
  120. data/nix/flake/mk-confctl-outputs.nix +522 -0
  121. data/nix/flake/mk-config-devshell.nix +168 -0
  122. data/nix/lib/default.nix +118 -65
  123. data/nix/lib/machine/default.nix +65 -45
  124. data/nix/lib/machine/info.nix +16 -5
  125. data/nix/lib/swpins/eval.nix +42 -29
  126. data/nix/lib/swpins/options.nix +6 -2
  127. data/nix/machines.nix +23 -15
  128. data/nix/modules/cluster/default.nix +104 -41
  129. data/nix/modules/confctl/carrier/base.nix +11 -4
  130. data/nix/modules/confctl/carrier/carrier-env.rb +2 -2
  131. data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +50 -16
  132. data/nix/modules/confctl/carrier/netboot/nixos.nix +56 -24
  133. data/nix/modules/confctl/configuration-info.nix +17 -0
  134. data/nix/modules/confctl/generations.nix +2 -2
  135. data/nix/modules/confctl/host.nix +13 -0
  136. data/nix/modules/confctl/inputs-info.nix +21 -0
  137. data/nix/modules/confctl/kexec-netboot/default.nix +13 -6
  138. data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +3 -0
  139. data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +25 -16
  140. data/nix/modules/confctl/nix.nix +31 -1
  141. data/nix/modules/confctl/swpins.nix +13 -6
  142. data/nix/modules/module-list.nix +4 -2
  143. data/nix/modules/system-list.nix +4 -1
  144. data/nix/package.nix +37 -0
  145. data/shell.nix +39 -8
  146. data/skills/confctl-configuration-update/SKILL.md +228 -0
  147. data/skills/confctl-configuration-update/agents/openai.yaml +4 -0
  148. data/skills/confctl-release/SKILL.md +102 -0
  149. data/skills/confctl-release/agents/openai.yaml +4 -0
  150. data/spec/confctl/cli/cluster_health_check_spec.rb +43 -0
  151. data/spec/confctl/cli/cluster_skip_current_deploy_spec.rb +157 -0
  152. data/spec/confctl/cli/cluster_status_flake_spec.rb +83 -0
  153. data/spec/confctl/cli/inputs_set_output_spec.rb +70 -0
  154. data/spec/confctl/configuration_spec.rb +45 -0
  155. data/spec/confctl/inputs_spec.rb +104 -0
  156. data/spec/confctl/machine_control_spec.rb +112 -0
  157. data/spec/confctl/machine_list_spec.rb +47 -0
  158. data/spec/confctl/nix_flake_spec.rb +41 -0
  159. data/spec/confctl/swpins_spec.rb +106 -0
  160. data/spec/generation/build_modes_spec.rb +132 -0
  161. data/spec/inputs/commit_message_spec.rb +205 -0
  162. data/spec/inputs/nix_cached_fallback_spec.rb +50 -0
  163. data/spec/inputs/nix_output_guard_spec.rb +45 -0
  164. data/spec/spec_helper.rb +12 -0
  165. data/spec/support/cli_helper.rb +57 -0
  166. data/test-runner.sh +7 -0
  167. data/tests/all-tests.nix +30 -0
  168. data/tests/make-test.nix +15 -0
  169. data/tests/runner/extensions/confctl_helpers.rb +238 -0
  170. data/tests/runner/extensions/hostfwd_ports.rb +41 -0
  171. data/tests/suite/auto_rollback.nix +344 -0
  172. data/tests/suite/carrier/deploy.nix +751 -0
  173. data/tests/suite/carrier/netboot.nix +849 -0
  174. data/tests/suite/deploy/base.nix +806 -0
  175. data/tests/suite/deploy/flakes.nix +1 -0
  176. data/tests/suite/deploy/swpins.nix +1 -0
  177. metadata +104 -4
  178. data/nix/modules/confctl/overlays.nix +0 -15
@@ -0,0 +1,806 @@
1
+ { deployMode }:
2
+ let
3
+ mode =
4
+ if
5
+ builtins.elem deployMode [
6
+ "flakes"
7
+ "swpins"
8
+ ]
9
+ then
10
+ deployMode
11
+ else
12
+ throw "unsupported deployMode '${deployMode}'";
13
+ in
14
+ { testFramework, ... }@args:
15
+ let
16
+ vpsadminosSource = testFramework.sourcePath;
17
+ in
18
+ import ../../make-test.nix (
19
+ {
20
+ pkgs,
21
+ confctlPackage,
22
+ confctlSrc,
23
+ ...
24
+ }:
25
+ let
26
+ confctlBin =
27
+ if confctlPackage == null then
28
+ throw "suiteArgs.confctlPackage is required"
29
+ else
30
+ "${confctlPackage}/bin/confctl";
31
+
32
+ confctlSource = if confctlSrc == null then throw "suiteArgs.confctlSrc is required" else confctlSrc;
33
+
34
+ in
35
+ {
36
+ name = "deploy-${mode}";
37
+
38
+ description = ''
39
+ Exercise confctl build/deploy lifecycle on NixOS and vpsAdminOS machines (${mode} mode).
40
+ '';
41
+
42
+ tags = [
43
+ "ci"
44
+ ];
45
+
46
+ machines = {
47
+ nixos = {
48
+ spin = "nixos";
49
+ diskSize = 20480;
50
+ networks = [
51
+ {
52
+ type = "user";
53
+ opts = {
54
+ hostForward = "tcp::net1-:22";
55
+ network = "10.0.2.0/24";
56
+ host = "10.0.2.2";
57
+ dns = "10.0.2.3";
58
+ };
59
+ }
60
+ ];
61
+ config = {
62
+ services.openssh = {
63
+ enable = true;
64
+ settings = {
65
+ PermitRootLogin = "yes";
66
+ PasswordAuthentication = false;
67
+ };
68
+ };
69
+ networking.firewall.enable = false;
70
+ environment.systemPackages = with pkgs; [
71
+ git
72
+ ];
73
+ };
74
+ };
75
+ }
76
+ // (
77
+ if mode == "flakes" then
78
+ {
79
+ vpsadminos = {
80
+ spin = "vpsadminos";
81
+ networks = [
82
+ {
83
+ type = "user";
84
+ opts = {
85
+ hostForward = "tcp::net2-:22";
86
+ network = "10.0.2.0/24";
87
+ host = "10.0.2.2";
88
+ dns = "10.0.2.3";
89
+ };
90
+ }
91
+ ];
92
+ config = {
93
+ services.openssh = {
94
+ enable = true;
95
+ settings = {
96
+ PermitRootLogin = "yes";
97
+ PasswordAuthentication = false;
98
+ };
99
+ };
100
+ boot.qemu = {
101
+ memory = 2048;
102
+ cpus = 4;
103
+ };
104
+ networking.firewall.enable = false;
105
+ environment.systemPackages = with pkgs; [
106
+ git
107
+ ];
108
+ };
109
+ };
110
+ }
111
+ else
112
+ { }
113
+ );
114
+
115
+ testScript = ''
116
+ require 'fileutils'
117
+
118
+ DEPLOY_MODE = '${mode}'
119
+ NIXPKGS_PATH = '${pkgs.path}'
120
+ VPSADMINOS_PATH = '${vpsadminosSource}'
121
+
122
+ NIXOS_MACHINE = 'nixos-machine'
123
+ VPSADMINOS_MACHINE = 'vpsadminos-machine'
124
+
125
+ def flake_mode?
126
+ DEPLOY_MODE == 'flakes'
127
+ end
128
+
129
+ def include_vpsadminos_machine?
130
+ flake_mode?
131
+ end
132
+
133
+ def expected_successful_hosts
134
+ include_vpsadminos_machine? ? 2 : 1
135
+ end
136
+
137
+ def fixture_template_name
138
+ flake_mode? ? 'example-flake' : 'example'
139
+ end
140
+
141
+ def git_commit!(repo, file, content, message)
142
+ File.write(File.join(repo, file), content)
143
+ run_local!(%w[git add .], chdir: repo)
144
+ run_local!(['git', 'commit', '-m', message], chdir: repo)
145
+ out, = run_local!(%w[git rev-parse HEAD], chdir: repo)
146
+ out.strip
147
+ end
148
+
149
+ def setup_dummy_repo!(dummy_repo)
150
+ FileUtils.rm_rf(dummy_repo)
151
+ FileUtils.mkdir_p(dummy_repo)
152
+ run_local!(%w[git init], chdir: dummy_repo)
153
+ run_local!(%w[git config user.email confctl-tests@example.invalid], chdir: dummy_repo)
154
+ run_local!(%w[git config user.name confctl-tests], chdir: dummy_repo)
155
+ File.write(File.join(dummy_repo, 'flake.nix'), <<~NIX)
156
+ {
157
+ description = "dummy input for confctl tests";
158
+ outputs = { self }: { };
159
+ }
160
+ NIX
161
+ File.write(File.join(dummy_repo, 'dummy.txt'), "A\n")
162
+ run_local!(%w[git add .], chdir: dummy_repo)
163
+ run_local!(['git', 'commit', '-m', 'dummy: A'], chdir: dummy_repo)
164
+ out, = run_local!(%w[git rev-parse HEAD], chdir: dummy_repo)
165
+ out.strip
166
+ end
167
+
168
+ def bump_dummy_repo!(dummy_repo, label)
169
+ out, = run_local!(%w[git rev-list --count HEAD], chdir: dummy_repo)
170
+ n = out.strip.to_i + 1
171
+ git_commit!(dummy_repo, 'dummy.txt', "#{label} #{n}\n", "dummy: #{label} #{n}")
172
+ end
173
+
174
+ def write_nixos_config!(conf_dir, marker:)
175
+ vpsadminos_path = VPSADMINOS_PATH
176
+ vpsadminos_import =
177
+ if flake_mode?
178
+ '(inputs.vpsadminos + "/tests/configs/nixos/base.nix")'
179
+ else
180
+ "(#{vpsadminos_path} + \"/tests/configs/nixos/base.nix\")"
181
+ end
182
+
183
+ File.write(File.join(conf_dir, 'cluster/nixos-machine/config.nix'), <<~NIX)
184
+ {
185
+ config,
186
+ pkgs,
187
+ lib,
188
+ inputs,
189
+ ...
190
+ }:
191
+ {
192
+ imports = [
193
+ ../../environments/base.nix
194
+ ./hardware.nix
195
+ (
196
+ { lib, ... }:
197
+ {
198
+ options.virtualisation.cores = lib.mkOption {
199
+ type = lib.types.int;
200
+ default = 2;
201
+ };
202
+ options.virtualisation.memorySize = lib.mkOption {
203
+ type = lib.types.int;
204
+ default = 2048;
205
+ };
206
+ options.virtualisation.mountHostNixStore = lib.mkOption {
207
+ type = lib.types.bool;
208
+ default = false;
209
+ };
210
+ options.virtualisation.sharedDirectories = lib.mkOption {
211
+ type = lib.types.attrsOf lib.types.anything;
212
+ default = { };
213
+ };
214
+ }
215
+ )
216
+ #{vpsadminos_import}
217
+ ];
218
+
219
+ networking.hostName = "nixos-machine";
220
+ time.timeZone = lib.mkForce "UTC";
221
+ boot.loader.grub.enable = false;
222
+
223
+ fileSystems."/" = {
224
+ device = "/dev/disk/by-label/nixos";
225
+ fsType = "ext4";
226
+ };
227
+
228
+ environment.etc."confctl-marker-nixos".text = "#{marker}\n";
229
+ }
230
+ NIX
231
+ end
232
+
233
+ def write_vpsadminos_config!(conf_dir, marker:)
234
+ vpsadminos_path = VPSADMINOS_PATH
235
+ vpsadminos_import =
236
+ if flake_mode?
237
+ '(inputs.vpsadminos + "/tests/configs/vpsadminos/base.nix")'
238
+ else
239
+ "(#{vpsadminos_path} + \"/tests/configs/vpsadminos/base.nix\")"
240
+ end
241
+
242
+ File.write(File.join(conf_dir, 'cluster/vpsadminos-machine/config.nix'), <<~NIX)
243
+ {
244
+ config,
245
+ pkgs,
246
+ lib,
247
+ inputs,
248
+ ...
249
+ }:
250
+ {
251
+ imports = [
252
+ ../../environments/base.nix
253
+ ./hardware.nix
254
+ #{vpsadminos_import}
255
+ ];
256
+
257
+ networking.hostName = "vpsadminos-machine";
258
+
259
+ boot.loader.grub.enable = false;
260
+
261
+ boot.supportedFilesystems = [ "zfs" ];
262
+ boot.kernelParams = [ "nolive" ];
263
+ boot.zfs.pools = { };
264
+
265
+ environment.etc."confctl-marker-vpsadminos".text = "#{marker}\n";
266
+
267
+ system.stateVersion = "20.09";
268
+ }
269
+ NIX
270
+ end
271
+
272
+ def write_flake_root!(conf_dir, dummy_repo)
273
+ File.write(File.join(conf_dir, 'flake.nix'), <<~NIX)
274
+ {
275
+ description = "confctl test fixture (flake)";
276
+
277
+ inputs = {
278
+ confctl.url = "path:#{confctl_src}";
279
+ nixpkgs.follows = "confctl/nixpkgs";
280
+ vpsadminos.follows = "confctl/vpsadminos";
281
+ dummy-input.url = "git+file://#{dummy_repo}";
282
+ };
283
+
284
+ outputs = inputs@{ self, confctl, ... }:
285
+ let
286
+ channels = {
287
+ deploy = {
288
+ nixpkgs = "nixpkgs";
289
+ vpsadminos = "vpsadminos";
290
+ dummy = "dummy-input";
291
+ };
292
+ };
293
+
294
+ confctlOutputs = confctl.lib.mkConfctlOutputs {
295
+ confDir = ./.;
296
+ inherit inputs channels;
297
+ };
298
+ in
299
+ {
300
+ confctl = confctlOutputs;
301
+
302
+ devShells.x86_64-linux.default = confctl.lib.mkConfigDevShell {
303
+ system = "x86_64-linux";
304
+ mode = "minimal";
305
+ };
306
+ };
307
+ }
308
+ NIX
309
+ end
310
+
311
+ def write_swpins_config!(conf_dir, dummy_repo)
312
+ out, = run_local!(%w[git rev-parse --abbrev-ref HEAD], chdir: dummy_repo)
313
+ dummy_branch = out.strip
314
+ dummy_repo_escaped = nix_escape_string(dummy_repo)
315
+ nixpkgs_path_escaped = nix_escape_string(NIXPKGS_PATH)
316
+ vpsadminos_path_escaped = nix_escape_string(VPSADMINOS_PATH)
317
+
318
+ File.write(File.join(conf_dir, 'configs/swpins.nix'), <<~NIX)
319
+ { config, ... }:
320
+ {
321
+ confctl.swpins.core.pins = {
322
+ nixpkgs = {
323
+ type = "directory";
324
+ directory.path = "#{nixpkgs_path_escaped}";
325
+ };
326
+
327
+ vpsadminos = {
328
+ type = "directory";
329
+ directory.path = "#{vpsadminos_path_escaped}";
330
+ };
331
+ };
332
+
333
+ confctl.swpins.channels = {
334
+ deploy = {
335
+ nixpkgs = {
336
+ type = "directory";
337
+ directory.path = "#{nixpkgs_path_escaped}";
338
+ };
339
+
340
+ vpsadminos = {
341
+ type = "directory";
342
+ directory.path = "#{vpsadminos_path_escaped}";
343
+ };
344
+
345
+ dummy = {
346
+ type = "git-rev";
347
+ git-rev = {
348
+ url = "file://#{dummy_repo_escaped}";
349
+ fetchSubmodules = false;
350
+ update = {
351
+ auto = false;
352
+ interval = 0;
353
+ ref = "refs/heads/#{dummy_branch}";
354
+ };
355
+ };
356
+ };
357
+ };
358
+ };
359
+ }
360
+ NIX
361
+ end
362
+
363
+ def write_machine_modules!(conf_dir, nixos_port:, vpsadminos_port:)
364
+ channel_line =
365
+ if flake_mode?
366
+ 'inputs.channels = [ "deploy" ];'
367
+ else
368
+ 'swpins.channels = [ "deploy" ];'
369
+ end
370
+
371
+ File.write(File.join(conf_dir, 'cluster/nixos-machine/module.nix'), <<~NIX)
372
+ { config, ... }:
373
+ {
374
+ cluster."nixos-machine" = {
375
+ spin = "nixos";
376
+ #{channel_line}
377
+ host.target = "127.0.0.1";
378
+ host.port = #{nixos_port};
379
+ healthChecks.machineCommands = [
380
+ {
381
+ description = "true";
382
+ command = [ "true" ];
383
+ }
384
+ {
385
+ description = "argv with space";
386
+ command = [
387
+ "printf"
388
+ "%s"
389
+ "space ok"
390
+ ];
391
+ standardOutput.match = "space ok";
392
+ }
393
+ ];
394
+ };
395
+ }
396
+ NIX
397
+
398
+ if include_vpsadminos_machine?
399
+ File.write(File.join(conf_dir, 'cluster/vpsadminos-machine/module.nix'), <<~NIX)
400
+ { config, ... }:
401
+ {
402
+ cluster."vpsadminos-machine" = {
403
+ spin = "vpsadminos";
404
+ #{channel_line}
405
+ host.target = "127.0.0.1";
406
+ host.port = #{vpsadminos_port};
407
+ healthChecks.machineCommands = [
408
+ {
409
+ description = "true";
410
+ command = [ "true" ];
411
+ }
412
+ {
413
+ description = "argv with space";
414
+ command = [
415
+ "printf"
416
+ "%s"
417
+ "space ok"
418
+ ];
419
+ standardOutput.match = "space ok";
420
+ }
421
+ ];
422
+ };
423
+ }
424
+ NIX
425
+ end
426
+ end
427
+
428
+ def prepare_fixture!(conf_dir:, dummy_repo:, nixos_port:, vpsadminos_port:, pubkey:)
429
+ prepare_fixture_dir!(conf_dir, template: fixture_template_name)
430
+
431
+ if flake_mode?
432
+ write_flake_root!(conf_dir, dummy_repo)
433
+ else
434
+ write_swpins_config!(conf_dir, dummy_repo)
435
+ end
436
+
437
+ write_machine_modules!(
438
+ conf_dir,
439
+ nixos_port:,
440
+ vpsadminos_port:
441
+ )
442
+
443
+ write_nixos_config!(conf_dir, marker: 'A')
444
+ write_vpsadminos_config!(conf_dir, marker: 'A') if include_vpsadminos_machine?
445
+
446
+ write_admin_ssh_keys!(conf_dir, pubkey)
447
+ cluster_modules = [ 'nixos-machine/module.nix' ]
448
+ cluster_modules << 'vpsadminos-machine/module.nix' if include_vpsadminos_machine?
449
+ write_cluster_modules!(conf_dir, cluster_modules)
450
+ run_local!(%w[nix flake lock], chdir: conf_dir) if flake_mode?
451
+ init_fixture_repo!(conf_dir)
452
+ end
453
+
454
+ def prepare_mode_state!
455
+ return if flake_mode?
456
+
457
+ confctl!('swpins', 'core', 'update')
458
+ confctl!('swpins', 'channel', 'update', 'deploy')
459
+ end
460
+
461
+ def update_dummy_without_commit!
462
+ if flake_mode?
463
+ confctl!('inputs', 'update', 'dummy-input')
464
+ else
465
+ confctl!('swpins', 'channel', 'update', 'deploy', 'dummy')
466
+ end
467
+ end
468
+
469
+ def commit_dummy_update_and_set!(target_rev)
470
+ if flake_mode?
471
+ confctl!('inputs', 'update', '--commit', '--no-changelog', '--no-editor', 'dummy-input')
472
+ confctl!('inputs', 'set', '--commit', '--changelog', '--no-editor', 'dummy-input', target_rev)
473
+ else
474
+ confctl!('swpins', 'channel', 'update', '--commit', '--no-changelog', '--no-editor', 'deploy', 'dummy')
475
+ confctl!('swpins', 'channel', 'set', '--commit', '--changelog', '--no-editor', 'deploy', 'dummy', target_rev)
476
+ end
477
+ end
478
+
479
+ def machine_state(machine_name)
480
+ confctl_machine_state(machine_name)
481
+ end
482
+
483
+ def assert_machine_state(machine_name, profile: nil, current: nil)
484
+ expect(confctl_remote_realpath(machine_name, '/nix/var/nix/profiles/system')).to eq(profile) unless profile.nil?
485
+ expect(confctl_remote_realpath(machine_name, '/run/current-system')).to eq(current) unless current.nil?
486
+ end
487
+
488
+ def assert_store_path_exists(machine_name, store_path)
489
+ confctl_store_path_exists!(machine_name, store_path)
490
+ end
491
+
492
+ def configuration_info(machine_name)
493
+ out, = confctl_ssh!(machine_name, 'cat', '/etc/confctl/configuration-info.json')
494
+ json = out.lines.find { |line| line.lstrip.start_with?('{') }
495
+ raise "unable to parse configuration info from #{machine_name}: #{out.inspect}" if json.nil?
496
+
497
+ JSON.parse(json)
498
+ end
499
+
500
+ def assert_no_configuration_info(machine_name)
501
+ confctl_ssh!(machine_name, 'sh', '-c', 'test ! -e /etc/confctl/configuration-info.json')
502
+ end
503
+
504
+ def build_generation!(host)
505
+ confctl!('build', '--yes', host)
506
+ confctl_generation_info(host)
507
+ end
508
+
509
+ before(:suite) do
510
+ @nixos_port = ConfctlHostfwdPorts.reserve('net1')
511
+ @vpsadminos_port = include_vpsadminos_machine? ? ConfctlHostfwdPorts.reserve('net2') : nil
512
+
513
+ nixos.start
514
+ vpsadminos.start if include_vpsadminos_machine?
515
+
516
+ @state_dir = @opts[:state_dir]
517
+ @conf_dir = File.join(@state_dir, 'conf')
518
+ @dummy_repo = File.join(@state_dir, 'dummy-input')
519
+ @home_dir = File.join(@state_dir, 'home')
520
+ confctl_setup!(
521
+ bin: "${confctlBin}",
522
+ src: "${confctlSource}",
523
+ conf_dir: @conf_dir,
524
+ home_dir: @home_dir,
525
+ nix_path: "nixpkgs=#{NIXPKGS_PATH}"
526
+ )
527
+
528
+ @dummy_rev_a = setup_dummy_repo!(@dummy_repo)
529
+ @pubkey = setup_ssh_home!(@home_dir)
530
+
531
+ install_pubkey!(nixos, @pubkey)
532
+ install_pubkey!(vpsadminos, @pubkey) if include_vpsadminos_machine?
533
+
534
+ prepare_fixture!(
535
+ conf_dir: @conf_dir,
536
+ dummy_repo: @dummy_repo,
537
+ nixos_port: @nixos_port,
538
+ vpsadminos_port: @vpsadminos_port,
539
+ pubkey: @pubkey
540
+ )
541
+ prepare_mode_state!
542
+ end
543
+
544
+ describe 'confctl deploy behavior', order: :defined do
545
+ before(:context) do
546
+ out = wait_for_confctl_connectivity!(expected_successes: expected_successful_hosts, timeout: 180)
547
+ expect(out).to include("#{expected_successful_hosts} successful")
548
+
549
+ if flake_mode?
550
+ out, = run_local!(%w[git rev-parse HEAD], chdir: @conf_dir)
551
+ @configuration_revision = out.strip
552
+ end
553
+
554
+ @nixos_gen_a = build_generation!(NIXOS_MACHINE)
555
+ @vps_gen_a = build_generation!(VPSADMINOS_MACHINE) if include_vpsadminos_machine?
556
+
557
+ out, = confctl!('deploy', '--yes')
558
+ expect(out).not_to match(/\e\[/)
559
+
560
+ @nixos_state_a = machine_state(NIXOS_MACHINE)
561
+ end
562
+
563
+ it 'deploys baseline generation on both machines' do
564
+ assert_machine_state(
565
+ NIXOS_MACHINE,
566
+ profile: @nixos_gen_a['toplevel'],
567
+ current: @nixos_gen_a['toplevel']
568
+ )
569
+ if include_vpsadminos_machine?
570
+ assert_machine_state(
571
+ VPSADMINOS_MACHINE,
572
+ profile: @vps_gen_a['toplevel'],
573
+ current: @vps_gen_a['toplevel']
574
+ )
575
+ end
576
+
577
+ if flake_mode?
578
+ expect(configuration_info(NIXOS_MACHINE)).to eq(
579
+ 'schemaVersion' => 1,
580
+ 'revision' => @configuration_revision,
581
+ 'revisionDirty' => false
582
+ )
583
+ expect(configuration_info(VPSADMINOS_MACHINE)).to eq(
584
+ 'schemaVersion' => 1,
585
+ 'revision' => @configuration_revision,
586
+ 'revisionDirty' => false
587
+ )
588
+ else
589
+ assert_no_configuration_info(NIXOS_MACHINE)
590
+ end
591
+ end
592
+
593
+ it 'builds second generation for activation-target tests' do
594
+ write_nixos_config!(@conf_dir, marker: 'B')
595
+ @nixos_gen_b = build_generation!(NIXOS_MACHINE)
596
+ expect(@nixos_gen_b['toplevel']).not_to eq(@nixos_gen_a['toplevel'])
597
+ end
598
+
599
+ it 'copy-only copies selected generation without changing profile or current system' do
600
+ expect(@nixos_gen_b).not_to be_nil
601
+
602
+ confctl!('deploy', '--yes', '--copy-only', '--generation', @nixos_gen_b['name'], NIXOS_MACHINE)
603
+ assert_store_path_exists(NIXOS_MACHINE, @nixos_gen_b['toplevel'])
604
+ assert_machine_state(
605
+ NIXOS_MACHINE,
606
+ profile: @nixos_state_a[:profile],
607
+ current: @nixos_state_a[:current]
608
+ )
609
+ end
610
+
611
+ it 'test action keeps system profile unchanged' do
612
+ expect(@nixos_gen_b).not_to be_nil
613
+
614
+ confctl!('deploy', '--yes', '--generation', @nixos_gen_b['name'], NIXOS_MACHINE, 'test')
615
+ assert_store_path_exists(NIXOS_MACHINE, @nixos_gen_b['toplevel'])
616
+ assert_machine_state(NIXOS_MACHINE, profile: @nixos_state_a[:profile])
617
+ end
618
+
619
+ it 'dry-activate does not change current or profile symlinks' do
620
+ expect(@nixos_gen_b).not_to be_nil
621
+
622
+ confctl!('deploy', '--yes', '--generation', @nixos_gen_a['name'], NIXOS_MACHINE, 'switch')
623
+ assert_machine_state(
624
+ NIXOS_MACHINE,
625
+ profile: @nixos_gen_a['toplevel'],
626
+ current: @nixos_gen_a['toplevel']
627
+ )
628
+
629
+ confctl!('deploy', '--yes', '--generation', @nixos_gen_b['name'], NIXOS_MACHINE, 'dry-activate')
630
+ assert_store_path_exists(NIXOS_MACHINE, @nixos_gen_b['toplevel'])
631
+ assert_machine_state(
632
+ NIXOS_MACHINE,
633
+ profile: @nixos_gen_a['toplevel'],
634
+ current: @nixos_gen_a['toplevel']
635
+ )
636
+ end
637
+
638
+ it 'boot action updates profile without changing current system' do
639
+ expect(@nixos_gen_b).not_to be_nil
640
+
641
+ confctl!('deploy', '--yes', '--generation', @nixos_gen_a['name'], NIXOS_MACHINE, 'switch')
642
+
643
+ confctl!('deploy', '--yes', '--generation', @nixos_gen_b['name'], NIXOS_MACHINE, 'boot')
644
+ assert_machine_state(
645
+ NIXOS_MACHINE,
646
+ profile: @nixos_gen_b['toplevel'],
647
+ current: @nixos_gen_a['toplevel']
648
+ )
649
+ end
650
+
651
+ it 'switch action updates both profile and current system' do
652
+ expect(@nixos_gen_b).not_to be_nil
653
+
654
+ confctl!('deploy', '--yes', '--generation', @nixos_gen_b['name'], NIXOS_MACHINE, 'switch')
655
+ assert_machine_state(
656
+ NIXOS_MACHINE,
657
+ profile: @nixos_gen_b['toplevel'],
658
+ current: @nixos_gen_b['toplevel']
659
+ )
660
+ if flake_mode?
661
+ expect(configuration_info(NIXOS_MACHINE)).to eq(
662
+ 'schemaVersion' => 1,
663
+ 'revision' => @configuration_revision,
664
+ 'revisionDirty' => true
665
+ )
666
+ else
667
+ assert_no_configuration_info(NIXOS_MACHINE)
668
+ end
669
+ end
670
+
671
+ it 'skips repeated boot deploy with reboot when generation is already current' do
672
+ expect(@nixos_gen_b).not_to be_nil
673
+
674
+ out, = confctl!('deploy', '--yes', '--reboot', '--generation', @nixos_gen_b['name'], NIXOS_MACHINE, 'boot')
675
+ expect(out).to include("Skipping #{NIXOS_MACHINE}: already using target generation")
676
+ expect(out).not_to include("Rebooting #{NIXOS_MACHINE}")
677
+ assert_machine_state(
678
+ NIXOS_MACHINE,
679
+ profile: @nixos_gen_b['toplevel'],
680
+ current: @nixos_gen_b['toplevel']
681
+ )
682
+ end
683
+
684
+ it 'builds third generation for generation-selection tests' do
685
+ expect(@nixos_gen_b).not_to be_nil
686
+
687
+ write_nixos_config!(@conf_dir, marker: 'C')
688
+ @nixos_gen_c = build_generation!(NIXOS_MACHINE)
689
+ expect(@nixos_gen_c['toplevel']).not_to eq(@nixos_gen_b['toplevel'])
690
+ end
691
+
692
+ it 'deploys selected generation by explicit name' do
693
+ expect(@nixos_gen_b).not_to be_nil
694
+
695
+ confctl!('deploy', '--yes', '--generation', @nixos_gen_b['name'], NIXOS_MACHINE, 'switch')
696
+ assert_machine_state(
697
+ NIXOS_MACHINE,
698
+ profile: @nixos_gen_b['toplevel'],
699
+ current: @nixos_gen_b['toplevel']
700
+ )
701
+ end
702
+
703
+ it 'deploys selected generation by current selector' do
704
+ expect(@nixos_gen_c).not_to be_nil
705
+
706
+ confctl!('deploy', '--yes', '--generation', 'current', NIXOS_MACHINE, 'switch')
707
+ assert_machine_state(
708
+ NIXOS_MACHINE,
709
+ profile: @nixos_gen_c['toplevel'],
710
+ current: @nixos_gen_c['toplevel']
711
+ )
712
+ end
713
+
714
+ it 'deploys selected generation by numeric offset' do
715
+ expect(@nixos_gen_b).not_to be_nil
716
+
717
+ confctl!('deploy', '--yes', '--generation', '-1', NIXOS_MACHINE, 'switch')
718
+ assert_machine_state(
719
+ NIXOS_MACHINE,
720
+ profile: @nixos_gen_b['toplevel'],
721
+ current: @nixos_gen_b['toplevel']
722
+ )
723
+ end
724
+
725
+ it 'deploys selected current generation with one-by-one and dry-activate-first' do
726
+ selected_nixos = confctl_generation_info(NIXOS_MACHINE)
727
+ selected_vpsadminos = confctl_generation_info(VPSADMINOS_MACHINE) if include_vpsadminos_machine?
728
+
729
+ confctl!('deploy', '--yes', '--one-by-one', '--dry-activate-first', '--generation', 'current')
730
+
731
+ assert_machine_state(
732
+ NIXOS_MACHINE,
733
+ profile: selected_nixos['toplevel'],
734
+ current: selected_nixos['toplevel']
735
+ )
736
+ if include_vpsadminos_machine?
737
+ assert_machine_state(
738
+ VPSADMINOS_MACHINE,
739
+ profile: selected_vpsadminos['toplevel'],
740
+ current: selected_vpsadminos['toplevel']
741
+ )
742
+ assert_store_path_exists(VPSADMINOS_MACHINE, selected_vpsadminos['toplevel'])
743
+ end
744
+ end
745
+
746
+ it 'runs status command for current generation' do
747
+ out, = confctl!('status', '--yes', '--generation', 'current')
748
+ expect(out).to include('nixos-machine')
749
+ expect(out).to include('vpsadminos-machine') if include_vpsadminos_machine?
750
+ end
751
+
752
+ it 'runs diff command for current generation' do
753
+ out, = confctl!('diff', '--yes', '--generation', 'current')
754
+ expect(out).to be_a(String)
755
+ end
756
+
757
+ it 'runs changelog command for dummy source' do
758
+ out, = confctl!('changelog', '--yes', 'dummy')
759
+ expect(out).to be_a(String)
760
+ end
761
+
762
+ it 'updates dummy source without creating commit' do
763
+ @dummy_rev_b = bump_dummy_repo!(@dummy_repo, 'B')
764
+ update_dummy_without_commit!
765
+ end
766
+
767
+ it 'commits dummy source update and sets revision back' do
768
+ expect(@dummy_rev_b).not_to be_nil
769
+
770
+ bump_dummy_repo!(@dummy_repo, 'C')
771
+ commit_dummy_update_and_set!(@dummy_rev_b)
772
+ end
773
+
774
+ it 'reports dummy source changes in status, diff and changelog output' do
775
+ status_out, = confctl!('status', '--yes', '--generation', 'current')
776
+ diff_out, = confctl!('diff', '--yes', '--generation', 'current')
777
+ changelog_out, = confctl!('changelog', '--yes', 'dummy')
778
+ expect(status_out).to include('DUMMY')
779
+ expect([status_out, diff_out, changelog_out].join("\n")).to match(/dummy/i)
780
+ end
781
+
782
+ it 'passes health-checks' do
783
+ out, = confctl!('health-check', '--yes')
784
+ expect(out).to include('checks passed')
785
+ end
786
+
787
+ it 'lists local generations' do
788
+ out, = confctl!('generation', 'ls', '--local')
789
+ expect(out).to include('nixos-machine')
790
+ expect(out).to include('vpsadminos-machine') if include_vpsadminos_machine?
791
+ end
792
+
793
+ it 'prints expected output from confctl ssh' do
794
+ ssh_out, = confctl!('ssh', '--yes', NIXOS_MACHINE, 'printf', '%s', 'ssh ok')
795
+ expect(ssh_out).to include('nixos-machine')
796
+ expect(ssh_out).to include('ssh ok')
797
+ end
798
+
799
+ it 'keeps both machines reachable at the end' do
800
+ out = wait_for_confctl_connectivity!(expected_successes: expected_successful_hosts, timeout: 180)
801
+ expect(out).to include("#{expected_successful_hosts} successful")
802
+ end
803
+ end
804
+ '';
805
+ }
806
+ ) args