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.
- 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 +57 -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 -7
- 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 +29 -3
- 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 -16
- 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,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,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
|