confctl 2.2.3 → 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 +54 -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 -5
  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 +24 -2
  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 -13
  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,449 @@
1
+ require 'json'
2
+ require_relative 'nix_build_flake'
3
+ require_relative 'nix/args'
4
+
5
+ module ConfCtl
6
+ class NixFlake < NixLegacy
7
+ def confctl_settings
8
+ @confctl_settings ||= begin
9
+ settings = nix_eval_json('.#confctl.settings', impure: false, settings: {})
10
+ demodulify(settings)
11
+ end
12
+ end
13
+
14
+ # Returns an array with options from all confctl modules
15
+ # @return [Array]
16
+ def module_options
17
+ []
18
+ end
19
+
20
+ # Returns an array with machine fqdns
21
+ # @return [Array<String>]
22
+ def list_machine_fqdns
23
+ nix_eval_json('.#confctl.machineNames')
24
+ end
25
+
26
+ # Return machines and their config in a hash
27
+ # @return [Hash]
28
+ def list_machines
29
+ machines_json = nix_build_output_path('.#confctl.machinesJson')
30
+ machines = JSON.parse(File.read(machines_json))
31
+ machines = demodulify(machines)
32
+ refresh_machine_key_maps(machines)
33
+ machines
34
+ end
35
+
36
+ # Evaluate flake installable and parse JSON
37
+ # @param installable [String]
38
+ # @return [Object]
39
+ def eval_json(installable)
40
+ nix_eval_json(installable)
41
+ end
42
+
43
+ # Return list of swpins channels
44
+ # @return [Hash]
45
+ def list_swpins_channels
46
+ confctl_settings.fetch('swpins', {}).fetch('channels', {})
47
+ end
48
+
49
+ # Evaluate core swpins for host
50
+ # @return [Hash]
51
+ def eval_core_swpins
52
+ {}
53
+ end
54
+
55
+ # Evaluate swpins for hosts
56
+ # @param hosts [Array<String>]
57
+ # @return [Hash] host => swpins
58
+ def eval_host_swpins(hosts)
59
+ plan = build_plan
60
+
61
+ hosts.to_h do |host|
62
+ host_plan = plan.fetch(host)
63
+ [host, host_plan['inputs'] || {}]
64
+ end
65
+ end
66
+
67
+ # Evaluate inputs info for host
68
+ # @param host [String]
69
+ # @return [Hash]
70
+ def eval_inputs_info(host)
71
+ nix_eval_json(inputs_info_installable(host))
72
+ end
73
+
74
+ # Evaluate inputs for host
75
+ # @param host [String]
76
+ # @return [Hash]
77
+ def eval_inputs(host)
78
+ nix_eval_json(inputs_installable(host))
79
+ end
80
+
81
+ # Build config.system.build.toplevel for selected hosts
82
+ #
83
+ # @param hosts [Array<String>]
84
+ # @param swpin_paths [Hash]
85
+ # @param host_swpin_specs [Hash]
86
+ # @param time [Time]
87
+ #
88
+ # @yieldparam type [:build, :fetch]
89
+ # @yieldparam progress [Integer]
90
+ # @yieldparam total [Integer]
91
+ # @yieldparam path [String]
92
+ #
93
+ # @return [Hash<String, Generation::Build>]
94
+ def build_attributes(hosts: [], swpin_paths: {}, host_swpin_specs: {}, time: nil, &)
95
+ plan = build_plan
96
+ time ||= Time.now
97
+ ret_generations = {}
98
+ host_plans = {}
99
+ installables = []
100
+ machine_keys = []
101
+
102
+ hosts.each do |host|
103
+ host_plan = plan.fetch(host)
104
+ machine_key = host_plan['key'] || host_plan['machineKey'] || host_plan['flakeKey'] || machine_key_for(host)
105
+ host_plans[host] = host_plan
106
+ installables << ".#confctl.build.#{machine_key}.toplevel"
107
+ installables << ".#confctl.build.#{machine_key}.autoRollback"
108
+ machine_keys << machine_key
109
+ end
110
+
111
+ legacy_args = legacy_nix_path_args(hosts)
112
+ nix_build_json(installables, legacy_nix_path_args: legacy_args, &)
113
+ build_outputs = build_outputs_for_keys(machine_keys)
114
+ pending_generations = {}
115
+
116
+ hosts.each do |host|
117
+ host_plan = host_plans[host]
118
+ machine_key = host_plan['key'] || host_plan['machineKey'] || host_plan['flakeKey'] || machine_key_for(host)
119
+ host_input_paths = host_plan['inputs'] || {}
120
+
121
+ outputs = build_outputs[machine_key]
122
+ if outputs.nil?
123
+ raise ConfCtl::Error, "missing build outputs for #{machine_key.inspect}"
124
+ end
125
+
126
+ toplevel_path = outputs['toplevel']
127
+ auto_rollback_path = outputs['autoRollback']
128
+
129
+ if toplevel_path.nil? || auto_rollback_path.nil?
130
+ raise ConfCtl::Error, "invalid build outputs for #{machine_key.inspect}"
131
+ end
132
+
133
+ host_generations = Generation::BuildList.new(host)
134
+ generation = host_generations.find(toplevel_path, host_input_paths, mode: 'flakes')
135
+
136
+ if generation.nil?
137
+ pending_generations[host] = {
138
+ host_generations: host_generations,
139
+ machine_key: machine_key,
140
+ toplevel_path: toplevel_path,
141
+ auto_rollback_path: auto_rollback_path,
142
+ host_input_paths: host_input_paths
143
+ }
144
+ next
145
+ end
146
+
147
+ host_generations.current = generation
148
+ ret_generations[host] = generation
149
+ end
150
+
151
+ if pending_generations.any?
152
+ inputs_infos = inputs_info_for_keys(pending_generations.values.map { |v| v[:machine_key] })
153
+
154
+ pending_generations.each do |host, data|
155
+ inputs_info = inputs_infos[data[:machine_key]] || eval_inputs_info(host)
156
+ generation = Generation::Build.new(host)
157
+ generation.create_flake(
158
+ data[:toplevel_path],
159
+ data[:auto_rollback_path],
160
+ inputs: data[:host_input_paths],
161
+ inputs_info: inputs_info,
162
+ date: time
163
+ )
164
+ generation.save
165
+
166
+ data[:host_generations].current = generation
167
+ ret_generations[host] = generation
168
+ end
169
+ end
170
+
171
+ ret_generations
172
+ end
173
+
174
+ protected
175
+
176
+ def build_plan
177
+ @build_plan ||= nix_eval_json('.#confctl.buildPlan')
178
+ end
179
+
180
+ def inputs_info_installable(host)
181
+ ".#confctl.inputsInfo.#{machine_key_for(host)}"
182
+ end
183
+
184
+ def inputs_installable(host)
185
+ ".#confctl.inputs.#{machine_key_for(host)}"
186
+ end
187
+
188
+ def nix_eval_json(installable, impure: nil, settings: nil, apply: nil)
189
+ settings_for_args = settings || confctl_settings
190
+
191
+ result = run_nix_with_fallback do |extra_experimental, no_update_lock_file|
192
+ args_builder = nix_args(
193
+ settings: settings_for_args,
194
+ impure: impure,
195
+ no_update_lock_file: no_update_lock_file
196
+ )
197
+
198
+ nix_eval_args(
199
+ installable,
200
+ args_builder: args_builder,
201
+ extra_experimental: extra_experimental,
202
+ apply: apply
203
+ )
204
+ end
205
+
206
+ out, = result.stdout
207
+ JSON.parse(out)
208
+ end
209
+
210
+ def nix_build_json(installables, legacy_nix_path_args: [], &block)
211
+ extra_experimental = false
212
+ no_update_lock_file = true
213
+
214
+ loop do
215
+ args_builder = nix_args(
216
+ settings: confctl_settings,
217
+ no_update_lock_file: no_update_lock_file
218
+ )
219
+
220
+ args = nix_build_args(
221
+ installables,
222
+ args_builder: args_builder,
223
+ extra_experimental: extra_experimental,
224
+ legacy_nix_path_args: legacy_nix_path_args
225
+ )
226
+
227
+ nb = NixBuildFlake.new(args, chdir: conf_dir)
228
+ result = nb.run(&block)
229
+ out, = result.stdout
230
+ return JSON.parse(out)
231
+ rescue TTY::Command::ExitError => e
232
+ if no_update_lock_file && no_update_lock_file_error?(e.message)
233
+ no_update_lock_file = false
234
+ retry
235
+ elsif !extra_experimental && experimental_error?(e.message)
236
+ extra_experimental = true
237
+ retry
238
+ else
239
+ raise
240
+ end
241
+ end
242
+ end
243
+
244
+ def nix_build_output_path(installable)
245
+ outputs = nix_build_json([installable])
246
+ output_path = outputs.dig(0, 'outputs', 'out')
247
+
248
+ unless output_path
249
+ raise ConfCtl::Error, "missing output path for #{installable.inspect}"
250
+ end
251
+
252
+ output_path
253
+ end
254
+
255
+ def run_nix_with_fallback
256
+ extra_experimental = false
257
+ no_update_lock_file = true
258
+
259
+ loop do
260
+ args = yield(extra_experimental, no_update_lock_file)
261
+
262
+ begin
263
+ return cmd.run(*args, chdir: conf_dir)
264
+ rescue TTY::Command::ExitError => e
265
+ if no_update_lock_file && no_update_lock_file_error?(e.message)
266
+ no_update_lock_file = false
267
+ next
268
+ end
269
+
270
+ if !extra_experimental && experimental_error?(e.message)
271
+ extra_experimental = true
272
+ next
273
+ end
274
+
275
+ raise
276
+ end
277
+ end
278
+ end
279
+
280
+ def nix_args(settings:, impure: nil, no_update_lock_file: true)
281
+ ConfCtl::Nix::Args.new(
282
+ settings: settings,
283
+ impure: impure,
284
+ no_update_lock_file: no_update_lock_file
285
+ )
286
+ end
287
+
288
+ def nix_eval_args(installable, args_builder:, extra_experimental:, apply: nil)
289
+ args = ['nix', 'eval', '--json']
290
+ args.concat(
291
+ nix_common_args(
292
+ args_builder.eval_args,
293
+ extra_experimental: extra_experimental
294
+ )
295
+ )
296
+ if apply
297
+ args << '--apply' << apply
298
+ end
299
+ args << installable
300
+ args
301
+ end
302
+
303
+ def nix_build_args(installables, args_builder:, extra_experimental:, legacy_nix_path_args: [])
304
+ args = ['--json', '--no-link']
305
+ args.concat(
306
+ nix_common_args(
307
+ args_builder.build_args,
308
+ extra_experimental: extra_experimental
309
+ )
310
+ )
311
+ args.concat(legacy_nix_path_args)
312
+ args.concat(installables)
313
+ args
314
+ end
315
+
316
+ def nix_common_args(base_args, extra_experimental:)
317
+ args = []
318
+ if extra_experimental
319
+ args << '--extra-experimental-features' << 'nix-command'
320
+ args << '--extra-experimental-features' << 'flakes'
321
+ end
322
+
323
+ args.concat(base_args)
324
+
325
+ args << '--show-trace' if show_trace
326
+
327
+ if max_jobs
328
+ args << '--option' << 'max-jobs' << max_jobs.to_s
329
+ end
330
+
331
+ if cores
332
+ args << '--option' << 'cores' << cores.to_s
333
+ end
334
+ args
335
+ end
336
+
337
+ def nix_attr_string(value)
338
+ escaped = value.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
339
+ "\"#{escaped}\""
340
+ end
341
+
342
+ def build_outputs_for_keys(machine_keys)
343
+ return {} if machine_keys.empty?
344
+
345
+ keys = machine_keys.uniq
346
+ apply = list_to_attrs_apply_expr(keys)
347
+ nix_eval_json('.#confctl.build', apply: apply)
348
+ end
349
+
350
+ def inputs_info_for_keys(machine_keys)
351
+ return {} if machine_keys.empty?
352
+
353
+ keys = machine_keys.uniq
354
+ apply = list_to_attrs_apply_expr(keys)
355
+ nix_eval_json('.#confctl.inputsInfo', apply: apply)
356
+ end
357
+
358
+ def inputs_for_keys(machine_keys)
359
+ return {} if machine_keys.empty?
360
+
361
+ keys = machine_keys.uniq
362
+ apply = list_to_attrs_apply_expr(keys)
363
+ nix_eval_json('.#confctl.inputs', apply: apply)
364
+ end
365
+
366
+ def list_to_attrs_apply_expr(keys)
367
+ list = keys.map { |k| nix_attr_string(k) }.join(' ')
368
+ "x: builtins.listToAttrs (map (k: { name = k; value = x.${k}; }) [ #{list} ])"
369
+ end
370
+
371
+ def legacy_nix_path_args(hosts)
372
+ return [] if hosts.empty?
373
+
374
+ args_builder = nix_args(settings: confctl_settings)
375
+ return [] unless args_builder.legacy_nix_path?
376
+
377
+ unless args_builder.impure?
378
+ raise ConfCtl::Error, 'legacyNixPath requires impureEval'
379
+ end
380
+
381
+ merged_inputs = {}
382
+ host_keys = {}
383
+ hosts.each { |host| host_keys[host] = machine_key_for(host) }
384
+ inputs_by_key = inputs_for_keys(host_keys.values)
385
+
386
+ hosts.each do |host|
387
+ machine_key = host_keys[host]
388
+ inputs = inputs_by_key[machine_key] || nix_eval_json(inputs_installable(host))
389
+
390
+ inputs.each do |name, path|
391
+ next unless path.is_a?(String) && !path.empty?
392
+
393
+ key = name.to_s
394
+ if merged_inputs.has_key?(key) && merged_inputs[key] != path
395
+ raise ConfCtl::Error,
396
+ "legacyNixPath requires consistent input #{key} across hosts; build hosts separately"
397
+ end
398
+ merged_inputs[key] = path
399
+ end
400
+ end
401
+
402
+ args_builder.legacy_names.each_with_object([]) do |name, acc|
403
+ path = merged_inputs[name.to_s]
404
+ next unless path.is_a?(String) && !path.empty?
405
+
406
+ acc << '-I' << "#{name}=#{path}"
407
+ end
408
+ end
409
+
410
+ def refresh_machine_key_maps(machines)
411
+ @machine_name_to_key = {}
412
+ @machine_key_to_name = {}
413
+
414
+ machines.each do |name, info|
415
+ key = info['key'] || info['machineKey'] || info['flakeKey'] || name
416
+ @machine_name_to_key[name] = key
417
+ @machine_key_to_name[key] = name
418
+ end
419
+ end
420
+
421
+ def ensure_machine_key_maps
422
+ return if @machine_name_to_key && @machine_key_to_name
423
+
424
+ mapping = nix_eval_json('.#confctl.machineKeys')
425
+ @machine_name_to_key = mapping
426
+ @machine_key_to_name = mapping.to_h { |name, key| [key, name] }
427
+ end
428
+
429
+ def machine_key_for(host)
430
+ ensure_machine_key_maps
431
+
432
+ if @machine_name_to_key.has_key?(host)
433
+ @machine_name_to_key[host]
434
+ elsif @machine_key_to_name.has_key?(host)
435
+ host
436
+ else
437
+ raise ConfCtl::Error, "Unknown machine #{host.inspect}"
438
+ end
439
+ end
440
+
441
+ def no_update_lock_file_error?(message)
442
+ message.match?(/--no-update-lock-file/) && message.match?(/unknown|unrecognized|invalid|unsupported/i)
443
+ end
444
+
445
+ def experimental_error?(message)
446
+ message.match?(/experimental/i) && message.match?(/nix-command|flakes/i)
447
+ end
448
+ end
449
+ end
@@ -80,7 +80,7 @@ module ConfCtl
80
80
  'implement #to_nix()?'
81
81
  end
82
82
 
83
- pad("#{simple}#{semicolon ? ';' : ''}#{nl ? "\n" : ''}", indent:)
83
+ pad("#{simple}#{';' if semicolon}#{"\n" if nl}", indent:)
84
84
  end
85
85
  end
86
86
 
@@ -105,7 +105,7 @@ module ConfCtl
105
105
  end
106
106
  end
107
107
 
108
- ret << pad("}#{semicolon ? ';' : ''}#{nl ? "\n" : ''}")
108
+ ret << pad("}#{';' if semicolon}#{"\n" if nl}")
109
109
  ret
110
110
  end
111
111
 
@@ -118,7 +118,7 @@ module ConfCtl
118
118
  end
119
119
  end
120
120
 
121
- ret << pad("]#{semicolon ? ';' : ''}#{nl ? "\n" : ''}")
121
+ ret << pad("]#{';' if semicolon}#{"\n" if nl}")
122
122
  end
123
123
  end
124
124
  end