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
@@ -139,6 +139,11 @@ module ConfCtl::Cli
139
139
  end
140
140
 
141
141
  def determine_color
142
+ if gopts[:color] == 'auto' && ConfCtl::Ui.no_color?
143
+ Rainbow.enabled = false
144
+ return false
145
+ end
146
+
142
147
  case gopts[:color]
143
148
  when 'always'
144
149
  Rainbow.enabled = true
@@ -152,7 +157,7 @@ module ConfCtl::Cli
152
157
  end
153
158
 
154
159
  def determine_pager
155
- ENV.fetch('PAGER', nil) && ENV['PAGER'].strip != ''
160
+ ConfCtl::Ui.tty? && ENV.fetch('PAGER', nil) && ENV['PAGER'].strip != ''
156
161
  end
157
162
 
158
163
  def select_machines(pattern)
@@ -161,8 +166,25 @@ module ConfCtl::Cli
161
166
  attr_filters = AttrFilters.new(opts[:attr])
162
167
  tag_filters = TagFilters.new(opts[:tag])
163
168
 
169
+ if pattern.nil?
170
+ return machines.select do |_host, d|
171
+ attr_filters.pass?(d) && tag_filters.pass?(d)
172
+ end
173
+ end
174
+
175
+ if machines[pattern]
176
+ return machines.select do |host, d|
177
+ host == pattern && attr_filters.pass?(d) && tag_filters.pass?(d)
178
+ end
179
+ end
180
+
181
+ key_matches = machines.select do |_host, d|
182
+ d['key'] == pattern && attr_filters.pass?(d) && tag_filters.pass?(d)
183
+ end
184
+ return key_matches unless key_matches.empty?
185
+
164
186
  machines.select do |host, d|
165
- (pattern.nil? || ConfCtl::Pattern.match?(pattern, host)) \
187
+ ConfCtl::Pattern.match?(pattern, host) \
166
188
  && attr_filters.pass?(d) \
167
189
  && tag_filters.pass?(d)
168
190
  end
@@ -17,6 +17,106 @@ module ConfCtl::Cli
17
17
  .gitignore].include?(v)
18
18
  end
19
19
 
20
+ init_common
21
+
22
+ if swpins_mode?
23
+ init_swpins
24
+ else
25
+ init_flake
26
+ end
27
+ end
28
+
29
+ def add
30
+ require_args!('name')
31
+
32
+ name = args[0]
33
+ dir = File.join('cluster', name)
34
+ depth = name.count('/')
35
+
36
+ raise "#{dir} already exists" if Dir.exist?(dir)
37
+
38
+ mkdir_p(dir)
39
+
40
+ mkfile(File.join(dir, 'module.nix')) do |f|
41
+ channels_attr = flake_config? ? 'inputs.channels' : 'swpins.channels'
42
+ f.puts(<<~END
43
+ { config, ... }:
44
+ {
45
+ cluster."#{name}" = {
46
+ spin = "nixos";
47
+ #{channels_attr} = [ "nixos-unstable" ];
48
+ host = { name = "machine"; domain = "example.com"; };
49
+ };
50
+ }
51
+ END
52
+ )
53
+ end
54
+
55
+ mkfile(File.join(dir, 'config.nix')) do |f|
56
+ f.puts(<<~END
57
+ { config, pkgs, lib, ... }:
58
+ {
59
+ imports = [
60
+ #{'../' * (depth + 2)}environments/base.nix
61
+ ];
62
+
63
+ # ... standard NixOS configuration ...
64
+
65
+ networking.hostName = "#{name.gsub('/', '-')}";
66
+ }
67
+ END
68
+ )
69
+ end
70
+
71
+ rediscover
72
+ end
73
+
74
+ def rename
75
+ require_args!('old-name', 'new-name')
76
+
77
+ src = args[0]
78
+ dst = args[1]
79
+
80
+ src_path = File.join('cluster', src)
81
+ dst_path = File.join('cluster', dst)
82
+
83
+ if !Dir.exist?(src_path)
84
+ raise "'#{src}' not found"
85
+ elsif Dir.exist?(dst_path)
86
+ raise "'#{dst}' already exists"
87
+ end
88
+
89
+ dst_dir = File.dirname(dst_path)
90
+ mkdir_p(dst_dir)
91
+ mv(src_path, dst_path)
92
+
93
+ src_swpins = File.join('swpins/cluster', "#{src.gsub('/', ':')}.json")
94
+ dst_swpins = File.join('swpins/cluster', "#{dst.gsub('/', ':')}.json")
95
+ mv(src_swpins, dst_swpins) if File.exist?(src_swpins)
96
+
97
+ rediscover
98
+ end
99
+
100
+ def rediscover
101
+ hosts = discover_dir('cluster').sort
102
+
103
+ replace_file('cluster/cluster.nix') do |f|
104
+ f.puts('# This file is generated by confctl, changes will be lost')
105
+ f.puts('[')
106
+
107
+ hosts.each do |host|
108
+ f.puts(" ./#{host}/module.nix")
109
+ end
110
+
111
+ f.puts(']')
112
+ end
113
+
114
+ ConfCtl::Hook.call(:configuration_rediscover)
115
+ end
116
+
117
+ protected
118
+
119
+ def init_common
20
120
  mkdir('cluster')
21
121
 
22
122
  mkfile('cluster/module-list.nix') do |f|
@@ -53,40 +153,6 @@ module ConfCtl::Cli
53
153
  )
54
154
  end
55
155
 
56
- mkfile('configs/swpins.nix') do |f|
57
- f.puts(<<~END
58
- { config, ... }:
59
- let
60
- nixpkgsBranch = branch: {
61
- type = "git-rev";
62
-
63
- git-rev = {
64
- url = "https://github.com/NixOS/nixpkgs";
65
- update.ref = "refs/heads/${branch}";
66
- };
67
- };
68
-
69
- vpsadminosBranch = branch: {
70
- type = "git-rev";
71
-
72
- git-rev = {
73
- url = "https://github.com/vpsfreecz/vpsadminos";
74
- update.ref = "refs/heads/${branch}";
75
- };
76
- };
77
- in {
78
- confctl.swpins.channels = {
79
- nixos-unstable = { nixpkgs = nixpkgsBranch "nixos-unstable"; };
80
-
81
- # nixos-stable = { nixpkgs = nixpkgsBranch "nixos-20.09"; };
82
-
83
- # vpsadminos-staging = { vpsadminos = vpsadminosBranch "staging"; };
84
- };
85
- }
86
- END
87
- )
88
- end
89
-
90
156
  mkdir('data')
91
157
 
92
158
  mkfile('data/default.nix') do |f|
@@ -153,100 +219,99 @@ module ConfCtl::Cli
153
219
  END
154
220
  )
155
221
  end
156
-
157
- mkdir('swpins')
158
- mkdir('swpins/channels')
159
222
  end
160
223
 
161
- def add
162
- require_args!('name')
224
+ def init_swpins
225
+ mkfile('configs/swpins.nix') do |f|
226
+ f.puts(<<~END
227
+ { config, ... }:
228
+ let
229
+ nixpkgsBranch = branch: {
230
+ type = "git-rev";
163
231
 
164
- name = args[0]
165
- dir = File.join('cluster', name)
166
- depth = name.count('/')
232
+ git-rev = {
233
+ url = "https://github.com/NixOS/nixpkgs";
234
+ update.ref = "refs/heads/${branch}";
235
+ };
236
+ };
167
237
 
168
- raise "#{dir} already exists" if Dir.exist?(dir)
238
+ vpsadminosBranch = branch: {
239
+ type = "git-rev";
169
240
 
170
- mkdir_p(dir)
241
+ git-rev = {
242
+ url = "https://github.com/vpsfreecz/vpsadminos";
243
+ update.ref = "refs/heads/${branch}";
244
+ };
245
+ };
246
+ in {
247
+ confctl.swpins.channels = {
248
+ nixos-unstable = { nixpkgs = nixpkgsBranch "nixos-unstable"; };
171
249
 
172
- mkfile(File.join(dir, 'module.nix')) do |f|
173
- f.puts(<<~END
174
- { config, ... }:
175
- {
176
- cluster."#{name}" = {
177
- spin = "nixos";
178
- swpins.channels = [ "nixos-unstable" ];
179
- host = { name = "machine"; domain = "example.com"; };
250
+ # nixos-stable = { nixpkgs = nixpkgsBranch "nixos-20.09"; };
251
+
252
+ # vpsadminos-staging = { vpsadminos = vpsadminosBranch "staging"; };
180
253
  };
181
254
  }
182
255
  END
183
256
  )
184
257
  end
185
258
 
186
- mkfile(File.join(dir, 'config.nix')) do |f|
259
+ mkdir('swpins')
260
+ mkdir('swpins/channels')
261
+ end
262
+
263
+ def init_flake
264
+ mkfile('flake.nix') do |f|
187
265
  f.puts(<<~END
188
- { config, pkgs, lib, ... }:
189
266
  {
190
- imports = [
191
- #{'../' * (depth + 2)}environments/base.nix
192
- ];
267
+ description = "confctl configuration (flake)";
193
268
 
194
- # ... standard NixOS configuration ...
269
+ inputs = {
270
+ confctl.url = "github:vpsfreecz/confctl";
271
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
195
272
 
196
- networking.hostName = "#{name.gsub('/', '-')}";
273
+ # vpsadminos.url = "github:vpsfreecz/vpsadminos/staging";
274
+ # vpsadminos.inputs.nixpkgs.follows = "nixpkgs";
275
+ };
276
+
277
+ outputs = inputs@{ self, confctl, ... }:
278
+ let
279
+ channels = {
280
+ nixos-unstable = { nixpkgs = "nixpkgs"; };
281
+
282
+ # vpsadminos = {
283
+ # nixpkgs = "nixpkgs";
284
+ # vpsadminos = "vpsadminos";
285
+ # };
286
+ };
287
+
288
+ confctlOutputs = confctl.lib.mkConfctlOutputs {
289
+ confDir = ./.;
290
+ inherit inputs channels;
291
+ };
292
+ in
293
+ {
294
+ confctl = confctlOutputs;
295
+
296
+ devShells.x86_64-linux.default = confctl.lib.mkConfigDevShell {
297
+ system = "x86_64-linux";
298
+ mode = "minimal";
299
+ };
300
+ };
197
301
  }
198
302
  END
199
303
  )
200
304
  end
201
-
202
- rediscover
203
305
  end
204
306
 
205
- def rename
206
- require_args!('old-name', 'new-name')
207
-
208
- src = args[0]
209
- dst = args[1]
210
-
211
- src_path = File.join('cluster', src)
212
- dst_path = File.join('cluster', dst)
213
-
214
- if !Dir.exist?(src_path)
215
- raise "'#{src}' not found"
216
- elsif Dir.exist?(dst_path)
217
- raise "'#{dst}' already exists"
218
- end
219
-
220
- dst_dir = File.dirname(dst_path)
221
- mkdir_p(dst_dir)
222
- mv(src_path, dst_path)
223
-
224
- src_swpins = File.join('swpins/cluster', "#{src.gsub('/', ':')}.json")
225
- dst_swpins = File.join('swpins/cluster', "#{dst.gsub('/', ':')}.json")
226
- mv(src_swpins, dst_swpins) if File.exist?(src_swpins)
227
-
228
- rediscover
307
+ def swpins_mode?
308
+ opts[:swpins] || opts[:legacy]
229
309
  end
230
310
 
231
- def rediscover
232
- hosts = discover_dir('cluster').sort
233
-
234
- replace_file('cluster/cluster.nix') do |f|
235
- f.puts('# This file is generated by confctl, changes will be lost')
236
- f.puts('[')
237
-
238
- hosts.each do |host|
239
- f.puts(" ./#{host}/module.nix")
240
- end
241
-
242
- f.puts(']')
243
- end
244
-
245
- ConfCtl::Hook.call(:configuration_rediscover)
311
+ def flake_config?
312
+ ConfCtl::ConfigType.flake?(ConfCtl::ConfDir.path)
246
313
  end
247
314
 
248
- protected
249
-
250
315
  def discover_dir(dir_path, rel_path = nil)
251
316
  ret = []
252
317
 
@@ -277,11 +342,11 @@ module ConfCtl::Cli
277
342
  FileUtils.mkdir_p(name, mode: DIR_MODE)
278
343
  end
279
344
 
280
- def mkfile(name)
345
+ def mkfile(name, &block)
281
346
  puts "mkfile #{name}"
282
- f = File.open(name, 'w', FILE_MODE)
283
- yield(f)
284
- f.close
347
+ File.open(name, 'w', FILE_MODE) do |f|
348
+ block.call(f)
349
+ end
285
350
  end
286
351
 
287
352
  def replace_file(name, &)
@@ -273,6 +273,30 @@ module ConfCtl::Cli
273
273
  Rainbow('Collecting garbage on ').bright + Rainbow(machines.first.to_s).yellow
274
274
  end
275
275
 
276
+ unless ConfCtl::Ui.tty?
277
+ puts header
278
+
279
+ executor = ConfCtl::ParallelExecutor.new(opts['max-concurrent-gc'])
280
+
281
+ machines.each do |host, machine|
282
+ executor.add do
283
+ end_stats = nil
284
+ ret = nix.collect_garbage(machine) do |progress|
285
+ puts "#{host}> #{progress}"
286
+ end_stats = progress.line if /^\d+ store paths deleted/ =~ progress.line
287
+ end
288
+
289
+ puts(ret ? "#{host}: #{end_stats || 'done'}" : "#{host}: error occurred")
290
+ ret ? nil : host
291
+ end
292
+ end
293
+
294
+ failed = executor.run.compact
295
+ raise "Gargabe collection failed on: #{failed.join(', ')}" if failed.any?
296
+
297
+ return
298
+ end
299
+
276
300
  LogView.open(
277
301
  header: "#{header}\n",
278
302
  title: Rainbow('Live view').bright,
@@ -331,11 +355,28 @@ module ConfCtl::Cli
331
355
 
332
356
  def list_generations(gens)
333
357
  swpin_names = []
358
+ input_roles = []
334
359
 
335
360
  gens.each do |gen|
336
361
  gen.swpin_names.each do |name|
337
362
  swpin_names << name unless swpin_names.include?(name)
338
363
  end
364
+
365
+ next unless flake_generation?(gen)
366
+
367
+ inputs_info = normalized_inputs_info(gen)
368
+ next if inputs_info.nil? || inputs_info.empty?
369
+
370
+ inputs_info.each_key do |role|
371
+ input_roles << role unless input_roles.include?(role)
372
+ end
373
+ end
374
+
375
+ input_column_names = {}
376
+ input_columns = input_roles.map do |role|
377
+ name = swpin_names.include?(role) ? "input:#{role}" : role
378
+ input_column_names[role] = name
379
+ { name: name, label: role.to_s.upcase }
339
380
  end
340
381
 
341
382
  rows = gens.map do |gen|
@@ -352,15 +393,38 @@ module ConfCtl::Cli
352
393
  row[name] = spec.version
353
394
  end
354
395
 
396
+ if flake_generation?(gen)
397
+ inputs_info = normalized_inputs_info(gen) || {}
398
+ input_roles.each do |role|
399
+ row[input_column_names[role]] = inputs_short_rev(inputs_info[role])
400
+ end
401
+ end
402
+
355
403
  row
356
404
  end
357
405
 
358
406
  OutputFormatter.print(
359
407
  rows,
360
- %w[host name id presence current kernel] + swpin_names,
408
+ %w[host name id presence current kernel] + swpin_names + input_columns,
361
409
  layout: :columns,
362
410
  sort: %w[name host]
363
411
  )
364
412
  end
413
+
414
+ def normalized_inputs_info(gen)
415
+ return nil unless gen.respond_to?(:inputs_info)
416
+
417
+ ConfCtl::InputsInfo.normalize(gen.inputs_info)
418
+ end
419
+
420
+ def inputs_short_rev(info)
421
+ return nil unless info.is_a?(Hash)
422
+
423
+ info['shortRev'] || (info['rev'] && info['rev'][0, 8])
424
+ end
425
+
426
+ def flake_generation?(gen)
427
+ gen.respond_to?(:flakes_mode?) && gen.flakes_mode?
428
+ end
365
429
  end
366
430
  end
@@ -0,0 +1,190 @@
1
+ require 'confctl/cli/command'
2
+ require 'confctl/config_type'
3
+ require 'confctl/flake_lock'
4
+ require 'confctl/nix'
5
+ require 'confctl/pattern'
6
+ require 'confctl/inputs/setter'
7
+ require 'confctl/inputs/updater'
8
+
9
+ module ConfCtl::Cli
10
+ class Inputs::Channels < Command
11
+ def list
12
+ ensure_flake_config!
13
+
14
+ raise GLI::BadCommandLine, 'usage: confctl inputs channel ls [channel-pattern]' if args.length > 1
15
+
16
+ conf_dir = ConfCtl::ConfDir.path
17
+ lock = ConfCtl::FlakeLock.load(File.join(conf_dir, 'flake.lock'))
18
+ channels = eval_channels
19
+
20
+ selected = select_channels(channels.keys, args[0])
21
+
22
+ rows = []
23
+ selected.each do |ch|
24
+ (channels[ch] || {}).each do |role, input|
25
+ info = lock.input_info(input)
26
+ rows << {
27
+ channel: ch,
28
+ role: role,
29
+ input: input,
30
+ rev: info[:short_rev] || '-',
31
+ url: info[:url] || '-'
32
+ }
33
+ end
34
+ end
35
+
36
+ OutputFormatter.print(rows, %i[channel role input rev url], layout: :columns)
37
+ end
38
+
39
+ def update
40
+ ensure_flake_config!
41
+ require_args!('channels', optional: ['role'])
42
+
43
+ selector = args[0]
44
+ role = args[1]
45
+ role_name = role&.to_s
46
+
47
+ channels = eval_channels
48
+ selected = select_channels(channels.keys, selector)
49
+ raise ConfCtl::Error, "no channels matched '#{selector}'" if selected.empty?
50
+
51
+ targets = selected.flat_map do |ch|
52
+ mapping = channels[ch] || {}
53
+ if role_name
54
+ input = mapping[role] || mapping[role_name]
55
+ input ? [{ channel: ch, role: role_name, input: input }] : []
56
+ else
57
+ mapping.map { |r, input| { channel: ch, role: r.to_s, input: input } }
58
+ end
59
+ end
60
+
61
+ inputs = targets.map { |t| t[:input] }.uniq
62
+
63
+ raise ConfCtl::Error, 'no inputs selected (check role name?)' if inputs.empty?
64
+
65
+ ConfCtl::Inputs::Updater.run!(
66
+ conf_dir: ConfCtl::ConfDir.path,
67
+ inputs: inputs,
68
+ commit: opts[:commit],
69
+ changelog: opts[:changelog],
70
+ downgrade: opts[:downgrade],
71
+ editor: opts[:editor]
72
+ )
73
+
74
+ lock = ConfCtl::FlakeLock.load(File.join(ConfCtl::ConfDir.path, 'flake.lock'))
75
+ targets.each do |t|
76
+ info = lock.input_info(t[:input])
77
+ rev = info[:short_rev] || info[:rev] || '-'
78
+ puts "Updating #{t[:role]} in #{t[:channel]} -> #{rev}"
79
+ end
80
+ end
81
+
82
+ def set
83
+ ensure_flake_config!
84
+ require_args!('channels', 'role', 'rev')
85
+
86
+ selector = args[0]
87
+ role = args[1]
88
+ rev = args[2]
89
+
90
+ channels = eval_channels
91
+ selected = select_channels(channels.keys, selector)
92
+ raise ConfCtl::Error, "no channels matched '#{selector}'" if selected.empty?
93
+
94
+ role_name = role.to_s
95
+ targets = selected.filter_map do |ch|
96
+ mapping = channels[ch] || {}
97
+ input = mapping[role] || mapping[role_name]
98
+ input ? { channel: ch, role: role_name, input: input } : nil
99
+ end
100
+
101
+ raise ConfCtl::Error, 'no inputs selected (check role name?)' if targets.empty?
102
+
103
+ inputs = targets.map { |t| t[:input] }.uniq
104
+
105
+ shared = shared_input_uses(channels, selected, role_name, inputs)
106
+ if shared.any?
107
+ msg = build_shared_input_message(shared)
108
+ raise ConfCtl::Error, "#{msg} (use --allow-shared to proceed)" unless opts[:allow_shared]
109
+
110
+ puts "Warning: #{msg}"
111
+ end
112
+
113
+ ConfCtl::Inputs::Setter.run!(
114
+ conf_dir: ConfCtl::ConfDir.path,
115
+ inputs: inputs,
116
+ rev: rev,
117
+ commit: opts[:commit],
118
+ changelog: opts[:changelog],
119
+ downgrade: opts[:downgrade],
120
+ editor: opts[:editor]
121
+ )
122
+
123
+ lock = ConfCtl::FlakeLock.load(File.join(ConfCtl::ConfDir.path, 'flake.lock'))
124
+ targets.each do |t|
125
+ info = lock.input_info(t[:input])
126
+ resolved_rev = info[:short_rev] || info[:rev] || '-'
127
+ puts "Configuring #{t[:role]} in #{t[:channel]} -> #{resolved_rev}"
128
+ end
129
+ end
130
+
131
+ protected
132
+
133
+ def ensure_flake_config!
134
+ return if ConfCtl::ConfigType.flake?(ConfCtl::ConfDir.path)
135
+
136
+ raise ConfCtl::Error, 'confctl inputs channel is available only in flake configs'
137
+ end
138
+
139
+ def eval_channels
140
+ json = ConfCtl::Nix.new.eval_json('.#confctl.channels')
141
+ json.is_a?(Hash) ? json : {}
142
+ end
143
+
144
+ def select_channels(all, selector)
145
+ return all.sort if selector.nil?
146
+
147
+ if selector.include?('{') || selector.include?(',')
148
+ names = parse_channel_selectors(selector)
149
+ return names.select { |n| all.include?(n) }
150
+ end
151
+
152
+ all.select { |n| ConfCtl::Pattern.match?(selector, n) }.sort
153
+ end
154
+
155
+ def parse_channel_selectors(str)
156
+ s = str.strip
157
+ s = s[1..-2] if s.start_with?('{') && s.end_with?('}')
158
+ s.split(',').map(&:strip).reject(&:empty?)
159
+ end
160
+
161
+ def shared_input_uses(channels, selected, role, inputs)
162
+ requested = {}
163
+ selected.each { |ch| requested[[ch, role.to_s]] = true }
164
+ shared = {}
165
+
166
+ channels.each do |ch, mapping|
167
+ (mapping || {}).each do |r, input|
168
+ next unless inputs.include?(input)
169
+
170
+ key = [ch, r.to_s]
171
+ next if requested[key]
172
+
173
+ shared[input] ||= []
174
+ shared[input] << { channel: ch, role: r.to_s }
175
+ end
176
+ end
177
+
178
+ shared
179
+ end
180
+
181
+ def build_shared_input_message(shared)
182
+ details = shared.sort.map do |input, uses|
183
+ roles = uses.map { |u| "#{u[:channel]}/#{u[:role]}" }.sort.join(', ')
184
+ "#{input} (also used by #{roles})"
185
+ end.join('; ')
186
+
187
+ "selected input(s) are shared outside the requested channel/role: #{details}"
188
+ end
189
+ end
190
+ end