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,17 @@
1
+ module ConfCtl
2
+ module Inputs
3
+ class GitCommit
4
+ def self.commit!(conf_dir:, message:, editor:, files:)
5
+ args = %w[git commit]
6
+ args << '-e' if editor && $stdout.tty?
7
+ args << '-m' << message
8
+ args.concat(files)
9
+
10
+ Dir.chdir(conf_dir) do
11
+ ok = Kernel.system(*args)
12
+ raise 'git commit failed' unless ok
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ module ConfCtl
2
+ module Inputs
3
+ module NixOutputGuard
4
+ GITHUB_API_CACHE_FALLBACK =
5
+ %r{unable to download 'https://api\.github\.com/.*using cached version}m
6
+ GITHUB_RATE_LIMIT = /rate limit exceeded/i
7
+
8
+ def self.check!(result)
9
+ output = [result&.stdout, result&.stderr].compact.join("\n")
10
+ return unless github_api_cache_fallback?(output)
11
+
12
+ if github_rate_limit?(output)
13
+ raise ConfCtl::Error, <<~MSG.strip
14
+ Nix could not refresh GitHub input metadata because the GitHub API rate limit was exceeded.
15
+ It used a cached version instead, so the selected input may not be the latest revision.
16
+ Configure a GitHub token in Nix access-tokens or retry later.
17
+ MSG
18
+ end
19
+
20
+ raise ConfCtl::Error, <<~MSG.strip
21
+ Nix could not refresh GitHub input metadata and used a cached version instead.
22
+ The selected input may not be the latest revision; retry after the GitHub API is reachable.
23
+ MSG
24
+ end
25
+
26
+ def self.github_api_cache_fallback?(output)
27
+ output.match?(GITHUB_API_CACHE_FALLBACK)
28
+ end
29
+
30
+ def self.github_rate_limit?(output)
31
+ output.match?(GITHUB_RATE_LIMIT)
32
+ end
33
+
34
+ private_class_method :github_api_cache_fallback?, :github_rate_limit?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,179 @@
1
+ require 'confctl/flake_lock'
2
+ require 'confctl/flake_lock_diff'
3
+ require 'confctl/inputs/commit_message'
4
+ require 'confctl/inputs/git_commit'
5
+ require 'confctl/inputs/nix_output_guard'
6
+ require 'confctl/system_command'
7
+ require 'fileutils'
8
+ require 'json'
9
+ require 'securerandom'
10
+
11
+ module ConfCtl
12
+ module Inputs
13
+ class Setter
14
+ def self.run!(conf_dir:, inputs:, rev:, commit:, changelog:, downgrade:, editor:)
15
+ raise ArgumentError, 'inputs empty' if inputs.nil? || inputs.empty?
16
+
17
+ lock_path = File.join(conf_dir, 'flake.lock')
18
+ unless File.exist?(lock_path)
19
+ raise ConfCtl::Error, 'flake.lock missing; run confctl inputs update first'
20
+ end
21
+
22
+ old_lock = ConfCtl::FlakeLock.load(lock_path)
23
+ old_lock_data = JSON.parse(File.read(lock_path))
24
+
25
+ saved_original = {}
26
+ override_urls = {}
27
+
28
+ inputs.each do |input|
29
+ node_id = resolve_node_id(old_lock_data, input)
30
+ saved_original[input] = deep_dup(old_lock_data.dig('nodes', node_id, 'original'))
31
+
32
+ input_info = old_lock.input_info(input)
33
+ type = input_info[:type]
34
+ unless %w[github git].include?(type)
35
+ raise ConfCtl::Error, "input '#{input}' has unsupported type '#{type}' for set"
36
+ end
37
+
38
+ override_urls[input] = build_override_url(input_info, rev)
39
+ end
40
+
41
+ apply_inputs!(conf_dir, inputs, override_urls, saved_original, lock_path)
42
+
43
+ new_lock = ConfCtl::FlakeLock.load(lock_path)
44
+ changes = ConfCtl::FlakeLockDiff.diff(old_lock, new_lock, inputs: inputs)
45
+
46
+ return { changed: false, changes: [] } if changes.empty?
47
+
48
+ if commit
49
+ msg = ConfCtl::Inputs::CommitMessage.build(
50
+ changes: changes,
51
+ changelog: changelog,
52
+ downgrade: downgrade,
53
+ action: :set
54
+ )
55
+ ConfCtl::Inputs::GitCommit.commit!(
56
+ conf_dir: conf_dir,
57
+ message: msg,
58
+ editor: editor,
59
+ files: ['flake.lock']
60
+ )
61
+ end
62
+
63
+ { changed: true, changes: changes }
64
+ end
65
+
66
+ def self.resolve_node_id(lock_data, input_name)
67
+ node_id = lock_data.dig('nodes', 'root', 'inputs', input_name)
68
+
69
+ return node_id if node_id.is_a?(String)
70
+ return node_id[0] if node_id.is_a?(Array) && node_id[0].is_a?(String)
71
+
72
+ raise ConfCtl::Error, "unknown input '#{input_name}'"
73
+ end
74
+
75
+ def self.build_override_url(input_info, rev)
76
+ locked = input_info[:locked] || {}
77
+ original = input_info[:original] || {}
78
+
79
+ case input_info[:type]
80
+ when 'github'
81
+ owner = locked['owner'] || original['owner']
82
+ repo = locked['repo'] || original['repo']
83
+ dir = locked['dir'] || original['dir']
84
+ raise ConfCtl::Error, 'missing github owner or repo' unless owner && repo
85
+
86
+ url = "github:#{owner}/#{repo}/#{rev}"
87
+ url += "?dir=#{dir}" if dir
88
+ url
89
+ when 'git'
90
+ url = locked['url'] || original['url']
91
+ raise ConfCtl::Error, 'missing git url' unless url
92
+
93
+ url = url.sub(/^git\+/, '')
94
+ separator = url.include?('?') ? '&' : '?'
95
+ "git+#{url}#{separator}rev=#{rev}"
96
+ else
97
+ raise ConfCtl::Error, "unsupported input type '#{input_info[:type]}'"
98
+ end
99
+ end
100
+
101
+ def self.run_nix_flake_lock!(conf_dir, input, override_url, tmpfile)
102
+ cmd = ConfCtl::SystemCommand.new
103
+ extra_experimental = false
104
+
105
+ loop do
106
+ args = ['nix']
107
+ if extra_experimental
108
+ args << '--extra-experimental-features' << 'nix-command'
109
+ args << '--extra-experimental-features' << 'flakes'
110
+ end
111
+ args << 'flake' << 'lock'
112
+ args << '--update-input' << input
113
+ args << '--override-input' << input << override_url
114
+ args << '--output-lock-file' << tmpfile
115
+
116
+ begin
117
+ result = Dir.chdir(conf_dir) { cmd.run(*args) }
118
+ ConfCtl::Inputs::NixOutputGuard.check!(result)
119
+ break
120
+ rescue TTY::Command::ExitError => e
121
+ if !extra_experimental && experimental_error?(e.message)
122
+ extra_experimental = true
123
+ next
124
+ end
125
+
126
+ raise
127
+ end
128
+ end
129
+ end
130
+
131
+ def self.experimental_error?(message)
132
+ message.match?(/experimental/i) && message.match?(/nix-command|flakes/i)
133
+ end
134
+
135
+ def self.apply_inputs!(conf_dir, inputs, override_urls, saved_original, lock_path)
136
+ inputs.each do |input|
137
+ tmpfile = build_tmp_lock_path(conf_dir)
138
+
139
+ begin
140
+ run_nix_flake_lock!(conf_dir, input, override_urls[input], tmpfile)
141
+
142
+ tmp_lock_data = JSON.parse(File.read(tmpfile))
143
+ inputs.each do |name|
144
+ node_id = resolve_node_id(tmp_lock_data, name)
145
+ node = tmp_lock_data.dig('nodes', node_id) || {}
146
+ original = saved_original[name]
147
+
148
+ if original.nil?
149
+ node.delete('original')
150
+ else
151
+ node['original'] = deep_dup(original)
152
+ end
153
+
154
+ tmp_lock_data['nodes'][node_id] = node
155
+ end
156
+
157
+ File.write(tmpfile, "#{JSON.pretty_generate(tmp_lock_data)}\n")
158
+ File.rename(tmpfile, lock_path)
159
+ ensure
160
+ FileUtils.rm_f(tmpfile) if tmpfile && File.exist?(tmpfile)
161
+ end
162
+ end
163
+ end
164
+
165
+ def self.deep_dup(obj)
166
+ return nil if obj.nil?
167
+
168
+ Marshal.load(Marshal.dump(obj))
169
+ end
170
+
171
+ def self.build_tmp_lock_path(conf_dir)
172
+ File.join(conf_dir, "flake.lock.tmp-#{Process.pid}-#{SecureRandom.hex(6)}")
173
+ end
174
+
175
+ private_class_method :resolve_node_id, :build_override_url, :run_nix_flake_lock!,
176
+ :experimental_error?, :apply_inputs!, :deep_dup, :build_tmp_lock_path
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,76 @@
1
+ require 'confctl/flake_lock'
2
+ require 'confctl/flake_lock_diff'
3
+ require 'confctl/inputs/commit_message'
4
+ require 'confctl/inputs/git_commit'
5
+ require 'confctl/inputs/nix_output_guard'
6
+ require 'confctl/system_command'
7
+
8
+ module ConfCtl
9
+ module Inputs
10
+ class Updater
11
+ def self.run!(conf_dir:, inputs:, commit:, changelog:, downgrade:, editor:)
12
+ raise ArgumentError, 'inputs empty' if inputs.nil? || inputs.empty?
13
+
14
+ lock_path = File.join(conf_dir, 'flake.lock')
15
+ old_lock = ConfCtl::FlakeLock.load_optional(lock_path)
16
+
17
+ run_nix_flake_update!(conf_dir, inputs)
18
+
19
+ new_lock = ConfCtl::FlakeLock.load(lock_path)
20
+ changes = ConfCtl::FlakeLockDiff.diff(old_lock, new_lock, inputs: inputs)
21
+
22
+ return { changed: false, changes: [] } if changes.empty?
23
+
24
+ if commit
25
+ msg = ConfCtl::Inputs::CommitMessage.build(
26
+ changes: changes,
27
+ changelog: changelog,
28
+ downgrade: downgrade
29
+ )
30
+ ConfCtl::Inputs::GitCommit.commit!(
31
+ conf_dir: conf_dir,
32
+ message: msg,
33
+ editor: editor,
34
+ files: ['flake.lock']
35
+ )
36
+ end
37
+
38
+ { changed: true, changes: changes }
39
+ end
40
+
41
+ def self.run_nix_flake_update!(conf_dir, inputs)
42
+ cmd = ConfCtl::SystemCommand.new
43
+ extra_experimental = false
44
+
45
+ loop do
46
+ args = ['nix']
47
+ if extra_experimental
48
+ args << '--extra-experimental-features' << 'nix-command'
49
+ args << '--extra-experimental-features' << 'flakes'
50
+ end
51
+ args << 'flake' << 'update'
52
+ args.concat(inputs) if inputs
53
+
54
+ begin
55
+ result = Dir.chdir(conf_dir) { cmd.run(*args) }
56
+ ConfCtl::Inputs::NixOutputGuard.check!(result)
57
+ break
58
+ rescue TTY::Command::ExitError => e
59
+ if !extra_experimental && experimental_error?(e.message)
60
+ extra_experimental = true
61
+ next
62
+ end
63
+
64
+ raise
65
+ end
66
+ end
67
+ end
68
+
69
+ def self.experimental_error?(message)
70
+ message.match?(/experimental/i) && message.match?(/nix-command|flakes/i)
71
+ end
72
+
73
+ private_class_method :run_nix_flake_update!, :experimental_error?
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ module ConfCtl
2
+ module Inputs; end
3
+ end
4
+
5
+ require_rel 'inputs'
@@ -0,0 +1,50 @@
1
+ require 'json'
2
+
3
+ module ConfCtl
4
+ class InputsInfo
5
+ KEYS = %w[input url rev shortRev lastModified].freeze
6
+
7
+ def self.parse(json)
8
+ normalize(JSON.parse(json))
9
+ rescue JSON::ParserError => e
10
+ raise Error, "unable to parse inputs info: #{e.message}"
11
+ end
12
+
13
+ def self.normalize(hash)
14
+ return nil unless hash.is_a?(Hash)
15
+
16
+ hash.each_with_object({}) do |(role, info), acc|
17
+ acc[role.to_s] = normalize_info(info)
18
+ end
19
+ end
20
+
21
+ def self.normalize_info(info)
22
+ case info
23
+ when String
24
+ rev = info
25
+ data = { 'rev' => rev, 'shortRev' => rev && rev[0, 8] }
26
+ data.compact
27
+ when Hash
28
+ normalized = {}
29
+
30
+ info.each do |k, v|
31
+ key = k.to_s
32
+ case key
33
+ when 'input', 'url', 'rev', 'shortRev', 'lastModified'
34
+ normalized[key] = v
35
+ when 'short_rev'
36
+ normalized['shortRev'] = v
37
+ end
38
+ end
39
+
40
+ if normalized['shortRev'].nil? && normalized['rev']
41
+ normalized['shortRev'] = normalized['rev'][0, 8]
42
+ end
43
+
44
+ normalized
45
+ else
46
+ {}
47
+ end
48
+ end
49
+ end
50
+ end
@@ -29,7 +29,7 @@ module ConfCtl
29
29
  return if nl.nil?
30
30
 
31
31
  line = buffer[0..nl]
32
- @buffer = buffer[nl + 1..]
32
+ @buffer = buffer[(nl + 1)..]
33
33
  line
34
34
  end
35
35
 
@@ -4,8 +4,7 @@ module ConfCtl
4
4
  :carrier,
5
5
  :name,
6
6
  :alias,
7
- :attribute,
8
- keyword_init: true
7
+ :attribute
9
8
  )
10
9
 
11
10
  # @return [String]
@@ -17,6 +16,9 @@ module ConfCtl
17
16
  # @return [Boolean]
18
17
  attr_reader :managed
19
18
 
19
+ # @return [String, nil]
20
+ attr_reader :key
21
+
20
22
  # @return [String]
21
23
  attr_reader :spin
22
24
 
@@ -47,6 +49,7 @@ module ConfCtl
47
49
  @name = opts['name']
48
50
  @safe_name = name.gsub('/', ':')
49
51
  @managed = meta['managed']
52
+ @key = opts['key'] || opts['machineKey'] || opts['flakeKey']
50
53
  @spin = meta['spin']
51
54
  @is_carrier = meta.fetch('carrier', {}).fetch('enable', false)
52
55
  @carrier_name = opts['carrier']
@@ -94,10 +97,18 @@ module ConfCtl
94
97
  meta.fetch('host', {}).fetch('target', name)
95
98
  end
96
99
 
100
+ def target_port
101
+ meta.fetch('host', {}).fetch('port', 22).to_i
102
+ end
103
+
97
104
  def localhost?
98
105
  target_host == 'localhost'
99
106
  end
100
107
 
108
+ def runnable?
109
+ !carried? && !target_host.nil?
110
+ end
111
+
101
112
  # @return [String] path to nix-env managed profile
102
113
  def profile
103
114
  if carried?
@@ -112,7 +123,7 @@ module ConfCtl
112
123
  eq = v.index('=')
113
124
  raise "'#{v}' is not a valid nix path entry " if eq.nil?
114
125
 
115
- [v[0..eq - 1], v[eq + 1..]]
126
+ [v[0..(eq - 1)], v[(eq + 1)..]]
116
127
  end
117
128
  end
118
129
 
@@ -150,6 +161,8 @@ module ConfCtl
150
161
  end
151
162
 
152
163
  when 'builderCommands', 'machineCommands'
164
+ next if type == 'machineCommands' && !runnable?
165
+
153
166
  checks.each do |cmd|
154
167
  health_checks << HealthChecks::RunCommand.new(
155
168
  self,
@@ -171,6 +184,8 @@ module ConfCtl
171
184
  get(meta, key.split('.'))
172
185
  elsif key == 'name'
173
186
  name
187
+ elsif %w[key machineKey flakeKey].include?(key)
188
+ @key
174
189
  elsif key == 'checks'
175
190
  health_checks.length
176
191
  else
@@ -1,4 +1,5 @@
1
1
  require 'etc'
2
+ require 'shellwords'
2
3
 
3
4
  module ConfCtl
4
5
  class MachineControl
@@ -158,7 +159,7 @@ module ConfCtl
158
159
  if machine.localhost?
159
160
  command
160
161
  else
161
- ssh_args + command
162
+ ssh_args + [Shellwords.join(command)]
162
163
  end
163
164
 
164
165
  cmd.method(method).call(*args, **kwargs, &)
@@ -174,7 +175,12 @@ module ConfCtl
174
175
  end
175
176
 
176
177
  def ssh_args
177
- ['ssh', '-l', 'root'] + extra_ssh_opts + [machine.target_host]
178
+ ret = ['ssh']
179
+ ssh_config = ENV.fetch('CONFCTL_SSH_CONFIG', nil)
180
+ ret += ['-F', ssh_config] if ssh_config && !ssh_config.empty?
181
+ ret += ['-l', 'root']
182
+ ret += ['-p', machine.target_port.to_s] if machine.target_port != 22
183
+ ret + extra_ssh_opts + [machine.target_host]
178
184
  end
179
185
  end
180
186
  end
@@ -64,7 +64,7 @@ module ConfCtl
64
64
 
65
65
  # @return [MachineList]
66
66
  def runnable
67
- select { |_host, machine| !machine.carried? && machine.target_host }
67
+ select { |_host, machine| machine.runnable? }
68
68
  end
69
69
 
70
70
  # @param host [String]
@@ -99,7 +99,7 @@ module ConfCtl
99
99
  checks = []
100
100
 
101
101
  machines.each_value do |machine|
102
- next if machine.carried?
102
+ next unless machine.runnable?
103
103
 
104
104
  checks.concat(machine.health_checks)
105
105
  end
@@ -1,5 +1,6 @@
1
1
  require 'json'
2
2
  require 'tty-command'
3
+ require 'confctl/inputs_info'
3
4
 
4
5
  module ConfCtl
5
6
  class MachineStatus
@@ -73,9 +74,15 @@ module ConfCtl
73
74
  # @return [Hash]
74
75
  attr_accessor :target_swpin_specs
75
76
 
77
+ # @return [Hash]
78
+ attr_accessor :target_inputs_info
79
+
76
80
  # @return [Hash]
77
81
  attr_reader :swpins_info
78
82
 
83
+ # @return [Hash]
84
+ attr_reader :inputs_info
85
+
79
86
  # @return [Hash]
80
87
  attr_reader :swpins_state
81
88
 
@@ -86,7 +93,7 @@ module ConfCtl
86
93
  end
87
94
 
88
95
  # Connect to the machine and query its state
89
- def query(toplevel: true, generations: true)
96
+ def query(toplevel: true, generations: true, swpins: true, inputs: false)
90
97
  begin
91
98
  @uptime = mc.uptime
92
99
  rescue TTY::Command::ExitError
@@ -109,8 +116,18 @@ module ConfCtl
109
116
  end
110
117
  end
111
118
 
119
+ if swpins
120
+ begin
121
+ @swpins_info = query_swpins
122
+ rescue Error
123
+ nil
124
+ end
125
+ end
126
+
127
+ return unless inputs
128
+
112
129
  begin
113
- @swpins_info = query_swpins
130
+ @inputs_info = query_inputs_info
114
131
  rescue Error
115
132
  nil
116
133
  end
@@ -145,12 +162,7 @@ module ConfCtl
145
162
  end
146
163
 
147
164
  def query_swpins
148
- json =
149
- if machine.carried?
150
- query_carried_swpins
151
- else
152
- mc.read_file('/etc/confctl/swpins-info.json')
153
- end
165
+ json = read_swpins_info_json
154
166
 
155
167
  case json
156
168
  when String
@@ -160,22 +172,53 @@ module ConfCtl
160
172
  end
161
173
  end
162
174
 
163
- # @return [Hash, String]
164
- def query_carried_swpins
165
- begin
166
- json = mc.read_file(File.join(machine.profile, 'machine.json'))
167
- parsed = JSON.parse(json)
168
- return parsed['swpins-info'] if parsed['swpins-info']
169
- rescue TTY::Command::ExitError
170
- # pass
175
+ def query_inputs_info
176
+ json = read_inputs_info_json
177
+
178
+ case json
179
+ when String
180
+ InputsInfo.parse(json)
181
+ when Hash
182
+ InputsInfo.normalize(json)
171
183
  end
184
+ end
172
185
 
173
- begin
174
- return mc.read_file(File.join(machine.profile, '/etc/confctl/swpins-info.json'))
175
- rescue TTY::Command::ExitError
176
- # pass
186
+ # @return [Hash, String, nil]
187
+ def read_swpins_info_json
188
+ if machine.carried?
189
+ read_carried_info_json('swpins-info', '/etc/confctl/swpins-info.json')
190
+ else
191
+ read_file('/etc/confctl/swpins-info.json')
192
+ end
193
+ end
194
+
195
+ # @return [Hash, String, nil]
196
+ def read_inputs_info_json
197
+ if machine.carried?
198
+ read_carried_info_json('inputs-info', '/etc/confctl/inputs-info.json')
199
+ else
200
+ read_file('/etc/confctl/inputs-info.json')
177
201
  end
202
+ end
203
+
204
+ def read_carried_info_json(key, path)
205
+ info = read_carried_machine_json(key)
206
+ return info if info
207
+
208
+ read_file(File.join(machine.profile, path))
209
+ end
210
+
211
+ def read_carried_machine_json(key)
212
+ json = mc.read_file(File.join(machine.profile, 'machine.json'))
213
+ parsed = JSON.parse(json)
214
+ parsed[key] if parsed[key]
215
+ rescue TTY::Command::ExitError, JSON::ParserError
216
+ nil
217
+ end
178
218
 
219
+ def read_file(path)
220
+ mc.read_file(path)
221
+ rescue TTY::Command::ExitError
179
222
  nil
180
223
  end
181
224
  end
@@ -0,0 +1,48 @@
1
+ module ConfCtl
2
+ class Nix
3
+ class Args
4
+ DEFAULT_LEGACY_NAMES = %w[nixpkgs vpsadminos vpsadmin].freeze
5
+
6
+ def initialize(settings:, impure: nil, no_update_lock_file: true)
7
+ @settings = settings || {}
8
+ @impure_override = impure
9
+ @no_update_lock_file = no_update_lock_file
10
+ end
11
+
12
+ def impure?
13
+ return @impure_override unless @impure_override.nil?
14
+
15
+ @settings.dig('nix', 'impureEval') == true
16
+ end
17
+
18
+ def legacy_nix_path?
19
+ @settings.dig('nix', 'legacyNixPath') == true
20
+ end
21
+
22
+ def legacy_names
23
+ names = @settings.dig('nix', 'legacyNixPathMap')
24
+ return DEFAULT_LEGACY_NAMES if names.nil?
25
+
26
+ names
27
+ end
28
+
29
+ def eval_args
30
+ base_args
31
+ end
32
+
33
+ def build_args
34
+ base_args
35
+ end
36
+
37
+ private
38
+
39
+ def base_args
40
+ args = []
41
+ args << '--impure' if impure?
42
+ args << '--no-write-lock-file'
43
+ args << '--no-update-lock-file' if @no_update_lock_file
44
+ args
45
+ end
46
+ end
47
+ end
48
+ end