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.
- checksums.yaml +4 -4
- data/.git-hooks/pre_commit/nixfmt.rb +13 -0
- data/.github/workflows/rspec.yml +64 -0
- data/.github/workflows/rubocop.yml +27 -0
- data/.github/workflows/tests.yml +139 -0
- data/.gitignore +12 -7
- data/.overcommit.yml +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +8 -1
- data/AGENTS.md +42 -0
- data/CHANGELOG.md +54 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +198 -0
- data/README.md +166 -58
- data/Rakefile +5 -0
- data/confctl.gemspec +20 -2
- data/docs/carrier.md +3 -3
- data/docs/flake-inputs.md +159 -0
- data/docs/swpins-to-flakes.md +325 -0
- data/example/cluster/module-list.nix +2 -1
- data/example/cluster/nixos-machine/config.nix +12 -2
- data/example/cluster/nixos-machine/hardware.nix +6 -1
- data/example/cluster/vpsadminos-container/config.nix +6 -1
- data/example/cluster/vpsadminos-container/module.nix +4 -1
- data/example/cluster/vpsadminos-machine/config.nix +6 -1
- data/example/cluster/vpsadminos-machine/hardware.nix +6 -1
- data/example/cluster/vpsadminos-machine/module.nix +5 -2
- data/example/cluster/vpsfreecz-vps/config.nix +6 -1
- data/example/cluster/vpsfreecz-vps/module.nix +4 -1
- data/example/configs/swpins.nix +8 -3
- data/example/environments/base.nix +6 -1
- data/example/swpins/core.json +35 -0
- data/example-flake/.gitignore +2 -0
- data/example-flake/README.md +38 -0
- data/example-flake/cluster/cluster.nix +5 -0
- data/example-flake/cluster/module-list.nix +4 -0
- data/example-flake/cluster/nested/nixos-machine/config.nix +25 -0
- data/example-flake/cluster/nested/nixos-machine/hardware.nix +9 -0
- data/example-flake/cluster/nested/nixos-machine/module.nix +8 -0
- data/example-flake/cluster/nixos-machine/config.nix +25 -0
- data/example-flake/cluster/nixos-machine/hardware.nix +9 -0
- data/example-flake/cluster/nixos-machine/module.nix +8 -0
- data/example-flake/cluster/vpsadminos-container/config.nix +28 -0
- data/example-flake/cluster/vpsadminos-container/module.nix +8 -0
- data/example-flake/cluster/vpsadminos-machine/config.nix +27 -0
- data/example-flake/cluster/vpsadminos-machine/hardware.nix +9 -0
- data/example-flake/cluster/vpsadminos-machine/module.nix +8 -0
- data/example-flake/cluster/vpsfreecz-vps/config.nix +31 -0
- data/example-flake/cluster/vpsfreecz-vps/module.nix +8 -0
- data/example-flake/configs/confctl.nix +10 -0
- data/example-flake/data/default.nix +5 -0
- data/example-flake/data/ssh-keys.nix +7 -0
- data/example-flake/environments/base.nix +18 -0
- data/example-flake/flake.lock +75 -0
- data/example-flake/flake.nix +36 -0
- data/example-flake/modules/module-list.nix +13 -0
- data/example-flake/shell.nix +11 -0
- data/flake.lock +159 -0
- data/flake.nix +189 -0
- data/gemset.nix +1022 -0
- data/lib/confctl/cli/app.rb +145 -0
- data/lib/confctl/cli/attr_filters.rb +1 -1
- data/lib/confctl/cli/cluster.rb +681 -108
- data/lib/confctl/cli/command.rb +24 -2
- data/lib/confctl/cli/configuration.rb +171 -106
- data/lib/confctl/cli/generation.rb +65 -1
- data/lib/confctl/cli/inputs/channels.rb +190 -0
- data/lib/confctl/cli/inputs/machines.rb +83 -0
- data/lib/confctl/cli/inputs/root.rb +101 -0
- data/lib/confctl/cli/inputs.rb +5 -0
- data/lib/confctl/cli/log_view.rb +21 -10
- data/lib/confctl/cli/migrate/swpins_to_flakes.rb +866 -0
- data/lib/confctl/cli/migrate.rb +5 -0
- data/lib/confctl/cli/output_formatter.rb +5 -5
- data/lib/confctl/cli/swpins/base.rb +9 -0
- data/lib/confctl/cli/swpins/channel.rb +2 -5
- data/lib/confctl/cli/swpins/cluster.rb +2 -5
- data/lib/confctl/cli/swpins/core.rb +2 -5
- data/lib/confctl/config_type.rb +7 -0
- data/lib/confctl/flake_lock.rb +78 -0
- data/lib/confctl/flake_lock_diff.rb +36 -0
- data/lib/confctl/generation/build.rb +131 -24
- data/lib/confctl/generation/build_list.rb +4 -3
- data/lib/confctl/generation/unified.rb +14 -1
- data/lib/confctl/git_repo_mirror.rb +2 -2
- data/lib/confctl/health_checks/run_command.rb +3 -2
- data/lib/confctl/health_checks/systemd/properties.rb +1 -1
- data/lib/confctl/health_checks/systemd/property_list.rb +2 -2
- data/lib/confctl/inputs/commit_message.rb +125 -0
- data/lib/confctl/inputs/git_commit.rb +17 -0
- data/lib/confctl/inputs/nix_output_guard.rb +37 -0
- data/lib/confctl/inputs/setter.rb +179 -0
- data/lib/confctl/inputs/updater.rb +76 -0
- data/lib/confctl/inputs.rb +5 -0
- data/lib/confctl/inputs_info.rb +50 -0
- data/lib/confctl/line_buffer.rb +1 -1
- data/lib/confctl/machine.rb +18 -3
- data/lib/confctl/machine_control.rb +8 -2
- data/lib/confctl/machine_list.rb +2 -2
- data/lib/confctl/machine_status.rb +63 -20
- data/lib/confctl/nix/args.rb +48 -0
- data/lib/confctl/nix.rb +30 -437
- data/lib/confctl/nix_build_flake.rb +95 -0
- data/lib/confctl/nix_copy.rb +14 -2
- data/lib/confctl/nix_flake.rb +449 -0
- data/lib/confctl/nix_format.rb +3 -3
- data/lib/confctl/nix_legacy.rb +467 -0
- data/lib/confctl/swpins/change_set.rb +24 -2
- data/lib/confctl/swpins/specs/base.rb +2 -2
- data/lib/confctl/ui.rb +19 -0
- data/lib/confctl/version.rb +1 -1
- data/man/index.html +11 -0
- data/man/man8/confctl-options.nix.8.html +113 -0
- data/man/man8/confctl.8 +152 -21
- data/man/man8/confctl.8.html +355 -0
- data/man/man8/confctl.8.md +148 -17
- data/man/style.css +301 -0
- data/nix/evaluator.nix +94 -69
- data/nix/flake/mk-confctl-devshell.nix +85 -0
- data/nix/flake/mk-confctl-outputs.nix +522 -0
- data/nix/flake/mk-config-devshell.nix +168 -0
- data/nix/lib/default.nix +118 -65
- data/nix/lib/machine/default.nix +65 -45
- data/nix/lib/machine/info.nix +16 -5
- data/nix/lib/swpins/eval.nix +42 -29
- data/nix/lib/swpins/options.nix +6 -2
- data/nix/machines.nix +23 -15
- data/nix/modules/cluster/default.nix +104 -41
- data/nix/modules/confctl/carrier/base.nix +11 -4
- data/nix/modules/confctl/carrier/carrier-env.rb +2 -2
- data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +50 -13
- data/nix/modules/confctl/carrier/netboot/nixos.nix +56 -24
- data/nix/modules/confctl/configuration-info.nix +17 -0
- data/nix/modules/confctl/generations.nix +2 -2
- data/nix/modules/confctl/host.nix +13 -0
- data/nix/modules/confctl/inputs-info.nix +21 -0
- data/nix/modules/confctl/kexec-netboot/default.nix +13 -6
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +3 -0
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +25 -16
- data/nix/modules/confctl/nix.nix +31 -1
- data/nix/modules/confctl/swpins.nix +13 -6
- data/nix/modules/module-list.nix +4 -2
- data/nix/modules/system-list.nix +4 -1
- data/nix/package.nix +37 -0
- data/shell.nix +39 -8
- data/skills/confctl-configuration-update/SKILL.md +228 -0
- data/skills/confctl-configuration-update/agents/openai.yaml +4 -0
- data/skills/confctl-release/SKILL.md +102 -0
- data/skills/confctl-release/agents/openai.yaml +4 -0
- data/spec/confctl/cli/cluster_health_check_spec.rb +43 -0
- data/spec/confctl/cli/cluster_skip_current_deploy_spec.rb +157 -0
- data/spec/confctl/cli/cluster_status_flake_spec.rb +83 -0
- data/spec/confctl/cli/inputs_set_output_spec.rb +70 -0
- data/spec/confctl/configuration_spec.rb +45 -0
- data/spec/confctl/inputs_spec.rb +104 -0
- data/spec/confctl/machine_control_spec.rb +112 -0
- data/spec/confctl/machine_list_spec.rb +47 -0
- data/spec/confctl/nix_flake_spec.rb +41 -0
- data/spec/confctl/swpins_spec.rb +106 -0
- data/spec/generation/build_modes_spec.rb +132 -0
- data/spec/inputs/commit_message_spec.rb +205 -0
- data/spec/inputs/nix_cached_fallback_spec.rb +50 -0
- data/spec/inputs/nix_output_guard_spec.rb +45 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/cli_helper.rb +57 -0
- data/test-runner.sh +7 -0
- data/tests/all-tests.nix +30 -0
- data/tests/make-test.nix +15 -0
- data/tests/runner/extensions/confctl_helpers.rb +238 -0
- data/tests/runner/extensions/hostfwd_ports.rb +41 -0
- data/tests/suite/auto_rollback.nix +344 -0
- data/tests/suite/carrier/deploy.nix +751 -0
- data/tests/suite/carrier/netboot.nix +849 -0
- data/tests/suite/deploy/base.nix +806 -0
- data/tests/suite/deploy/flakes.nix +1 -0
- data/tests/suite/deploy/swpins.nix +1 -0
- metadata +104 -4
- 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,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
|
data/lib/confctl/line_buffer.rb
CHANGED
data/lib/confctl/machine.rb
CHANGED
|
@@ -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'
|
|
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
|
data/lib/confctl/machine_list.rb
CHANGED
|
@@ -64,7 +64,7 @@ module ConfCtl
|
|
|
64
64
|
|
|
65
65
|
# @return [MachineList]
|
|
66
66
|
def runnable
|
|
67
|
-
select { |_host, machine|
|
|
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
|
|
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
|
-
@
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|