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,125 @@
1
+ require 'confctl/git_repo_mirror'
2
+
3
+ module ConfCtl
4
+ module Inputs
5
+ class CommitMessage
6
+ def self.build(changes:, changelog:, downgrade: false, action: :update)
7
+ raise ArgumentError, 'no changes' if changes.nil? || changes.empty?
8
+
9
+ title = build_title(changes, action)
10
+
11
+ body =
12
+ if changelog
13
+ build_changelog_body(changes, downgrade)
14
+ else
15
+ build_plain_body(changes)
16
+ end
17
+
18
+ "#{title}\n\n#{body}".strip
19
+ end
20
+
21
+ def self.build_plain_body(changes)
22
+ changes.each_with_object(+'') do |ch, body|
23
+ body << change_summary(ch)
24
+ end
25
+ end
26
+
27
+ def self.build_changelog_body(changes, downgrade)
28
+ group_changes(changes).map do |group|
29
+ build_changelog_group(group, downgrade).rstrip
30
+ end.join("\n\n")
31
+ end
32
+
33
+ def self.build_changelog_group(group, downgrade)
34
+ body = +''
35
+
36
+ group.each do |ch|
37
+ body << change_summary(ch)
38
+ end
39
+
40
+ reference = group.first
41
+ return body unless reference.url && reference.old_rev && reference.new_rev
42
+ return body if reference.old_rev == reference.new_rev
43
+
44
+ from = downgrade ? reference.new_rev : reference.old_rev
45
+ to = downgrade ? reference.old_rev : reference.new_rev
46
+
47
+ begin
48
+ mirror = ConfCtl::GitRepoMirror.new(reference.url, quiet: true)
49
+ mirror.setup
50
+ body << mirror.log(from, to, opts: ['--oneline'])
51
+ rescue StandardError => e
52
+ body << "(changelog unavailable: #{e})"
53
+ end
54
+
55
+ body
56
+ end
57
+
58
+ def self.group_changes(changes)
59
+ groups = []
60
+ group_indexes = {}
61
+
62
+ changes.each do |ch|
63
+ key = group_key(ch)
64
+
65
+ if key && group_indexes.has_key?(key)
66
+ groups[group_indexes[key]] << ch
67
+ else
68
+ group_indexes[key] = groups.length if key
69
+ groups << [ch]
70
+ end
71
+ end
72
+
73
+ groups
74
+ end
75
+
76
+ def self.group_key(change)
77
+ url = normalize_url(change.url)
78
+ return nil if url.nil?
79
+
80
+ [url, change.old_rev, change.new_rev]
81
+ end
82
+
83
+ def self.change_summary(change)
84
+ "#{change.name}: #{change.old_rev ? change.old_short_rev : 'set'} -> #{change.new_short_rev || '-'}\n"
85
+ end
86
+
87
+ def self.build_title(changes, action)
88
+ names = changes.map(&:name).sort
89
+ verb = action == :set ? 'set' : 'update'
90
+ title = "inputs: #{verb} #{names.join(', ')}"
91
+ short_rev = common_short_rev(changes)
92
+
93
+ short_rev ? "#{title} to #{short_rev}" : title
94
+ end
95
+
96
+ def self.common_short_rev(changes)
97
+ reference = changes.first
98
+
99
+ return nil if reference.new_rev.nil? || reference.new_short_rev.nil?
100
+ return nil if changes.any? { |ch| ch.new_rev != reference.new_rev }
101
+ return reference.new_short_rev if changes.length == 1
102
+ return nil unless same_sources?(changes, reference.url)
103
+
104
+ reference.new_short_rev
105
+ end
106
+
107
+ def self.same_sources?(changes, reference_url)
108
+ normalized_reference = normalize_url(reference_url)
109
+ return false if normalized_reference.nil?
110
+
111
+ changes.all? { |ch| normalize_url(ch.url) == normalized_reference }
112
+ end
113
+
114
+ def self.normalize_url(url)
115
+ return nil if url.nil?
116
+
117
+ url.sub(%r{/+\z}, '')
118
+ end
119
+
120
+ private_class_method :build_plain_body, :build_changelog_body, :build_changelog_group,
121
+ :group_changes, :group_key, :change_summary, :build_title,
122
+ :common_short_rev, :same_sources?, :normalize_url
123
+ end
124
+ end
125
+ end
@@ -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