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,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'confctl'
|
|
5
|
+
require 'confctl/cli'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'flake input set output' do
|
|
8
|
+
let(:gopts) { { color: 'never' } }
|
|
9
|
+
let(:common_opts) { { commit: false, changelog: true, downgrade: false, editor: false } }
|
|
10
|
+
let(:lock) { instance_double(ConfCtl::FlakeLock) }
|
|
11
|
+
let(:resolved_info) { { short_rev: '5361648e', rev: '5361648e4a29f58b5c208d3f185d95cd0a43fe54' } }
|
|
12
|
+
|
|
13
|
+
around do |example|
|
|
14
|
+
old_tty = ENV.fetch('CONFCTL_TTY', nil)
|
|
15
|
+
ENV['CONFCTL_TTY'] = '0'
|
|
16
|
+
example.run
|
|
17
|
+
ensure
|
|
18
|
+
ENV['CONFCTL_TTY'] = old_tty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
before do
|
|
22
|
+
allow(ConfCtl::ConfDir).to receive(:path).and_return('/conf')
|
|
23
|
+
allow(ConfCtl::ConfigType).to receive(:flake?).with('/conf').and_return(true)
|
|
24
|
+
allow(ConfCtl::Inputs::Setter).to receive(:run!).and_return(changed: true, changes: [])
|
|
25
|
+
allow(ConfCtl::FlakeLock).to receive(:load).with('/conf/flake.lock').and_return(lock)
|
|
26
|
+
allow(lock).to receive(:input_info).with('vpsadmin-input').and_return(resolved_info)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'prints the resolved locked revision for channel set' do
|
|
30
|
+
command = ConfCtl::Cli::Inputs::Channels.new(
|
|
31
|
+
gopts,
|
|
32
|
+
common_opts.merge(allow_shared: false),
|
|
33
|
+
%w[vpsadmin vpsadmin devel]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
allow(command).to receive(:eval_channels).and_return(
|
|
37
|
+
'vpsadmin' => { 'vpsadmin' => 'vpsadmin-input' }
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expect do
|
|
41
|
+
command.set
|
|
42
|
+
end.to output("Configuring vpsadmin in vpsadmin -> 5361648e\n").to_stdout
|
|
43
|
+
|
|
44
|
+
expect(ConfCtl::Inputs::Setter).to have_received(:run!).with(
|
|
45
|
+
hash_including(conf_dir: '/conf', inputs: ['vpsadmin-input'], rev: 'devel')
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'prints the resolved locked revision for machine set' do
|
|
50
|
+
nix = instance_double(ConfCtl::Nix)
|
|
51
|
+
allow(ConfCtl::Nix).to receive(:new).and_return(nix)
|
|
52
|
+
allow(nix).to receive(:eval_inputs_info).with('build.vpsfree.cz').and_return(
|
|
53
|
+
'vpsadmin' => { 'input' => 'vpsadmin-input' }
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
command = ConfCtl::Cli::Inputs::Machines.new(
|
|
57
|
+
gopts,
|
|
58
|
+
common_opts,
|
|
59
|
+
%w[build.vpsfree.cz vpsadmin devel]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect do
|
|
63
|
+
command.set
|
|
64
|
+
end.to output("Configuring vpsadmin in build.vpsfree.cz -> 5361648e\n").to_stdout
|
|
65
|
+
|
|
66
|
+
expect(ConfCtl::Inputs::Setter).to have_received(:run!).with(
|
|
67
|
+
hash_including(conf_dir: '/conf', inputs: ['vpsadmin-input'], rev: 'devel')
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'configuration commands' do
|
|
6
|
+
include CliHelper
|
|
7
|
+
|
|
8
|
+
it 'initializes a flake configuration directory' do
|
|
9
|
+
Dir.mktmpdir('confctl-config-init-') do |dir|
|
|
10
|
+
result = run_confctl('init', chdir: dir)
|
|
11
|
+
flake = File.read(File.join(dir, 'flake.nix'))
|
|
12
|
+
|
|
13
|
+
expect(result.success?).to be(true), result.err
|
|
14
|
+
expect(File).to exist(File.join(dir, 'flake.nix'))
|
|
15
|
+
expect(File).to exist(File.join(dir, 'cluster', 'cluster.nix'))
|
|
16
|
+
expect(File).to exist(File.join(dir, 'configs', 'confctl.nix'))
|
|
17
|
+
expect(File).to exist(File.join(dir, 'data', 'ssh-keys.nix'))
|
|
18
|
+
expect(flake).to include('nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";')
|
|
19
|
+
expect(flake).to include('# vpsadminos.url = "github:vpsfreecz/vpsadminos/staging";')
|
|
20
|
+
expect(flake).to include('# vpsadminos.inputs.nixpkgs.follows = "nixpkgs";')
|
|
21
|
+
expect(flake).to include('confctl.lib.mkConfigDevShell')
|
|
22
|
+
expect(flake).to include('mode = "minimal";')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'adds and renames machines and rewrites cluster inventory' do
|
|
27
|
+
Dir.mktmpdir('confctl-config-add-rename-') do |dir|
|
|
28
|
+
expect(run_confctl('init', chdir: dir).success?).to be(true)
|
|
29
|
+
|
|
30
|
+
add_result = run_confctl('add', 'nested/test-machine', chdir: dir)
|
|
31
|
+
expect(add_result.success?).to be(true), add_result.err
|
|
32
|
+
expect(File).to exist(File.join(dir, 'cluster', 'nested', 'test-machine', 'module.nix'))
|
|
33
|
+
expect(File.read(File.join(dir, 'cluster', 'cluster.nix'))).to include('./nested/test-machine/module.nix')
|
|
34
|
+
|
|
35
|
+
rename_result = run_confctl('rename', 'nested/test-machine', 'renamed/machine', chdir: dir)
|
|
36
|
+
expect(rename_result.success?).to be(true), rename_result.err
|
|
37
|
+
expect(File).not_to exist(File.join(dir, 'cluster', 'nested', 'test-machine'))
|
|
38
|
+
expect(File).to exist(File.join(dir, 'cluster', 'renamed', 'machine', 'module.nix'))
|
|
39
|
+
|
|
40
|
+
cluster_nix = File.read(File.join(dir, 'cluster', 'cluster.nix'))
|
|
41
|
+
expect(cluster_nix).to include('./renamed/machine/module.nix')
|
|
42
|
+
expect(cluster_nix).not_to include('./nested/test-machine/module.nix')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'inputs workflows' do
|
|
7
|
+
include CliHelper
|
|
8
|
+
|
|
9
|
+
def create_dummy_repo(path)
|
|
10
|
+
FileUtils.mkdir_p(path)
|
|
11
|
+
init_git_repo(path)
|
|
12
|
+
|
|
13
|
+
File.write(
|
|
14
|
+
File.join(path, 'flake.nix'),
|
|
15
|
+
<<~NIX
|
|
16
|
+
{
|
|
17
|
+
description = "dummy flake input for confctl tests";
|
|
18
|
+
outputs = { self, ... }: {};
|
|
19
|
+
}
|
|
20
|
+
NIX
|
|
21
|
+
)
|
|
22
|
+
File.write(File.join(path, 'dummy.txt'), "A\n")
|
|
23
|
+
git_commit_all(path, 'dummy: A')
|
|
24
|
+
rev_a = run_cmd('git', 'rev-parse', 'HEAD', chdir: path).out.strip
|
|
25
|
+
|
|
26
|
+
[rev_a]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def commit_dummy(path, label)
|
|
30
|
+
count = run_cmd('git', 'rev-list', '--count', 'HEAD', chdir: path).out.strip.to_i
|
|
31
|
+
File.write(File.join(path, 'dummy.txt'), "#{label} #{count + 1}\n")
|
|
32
|
+
git_commit_all(path, "dummy: #{label}")
|
|
33
|
+
run_cmd('git', 'rev-parse', 'HEAD', chdir: path).out.strip
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def locked_rev(lock_path, input)
|
|
37
|
+
lock = JSON.parse(File.read(lock_path))
|
|
38
|
+
node_id = lock.fetch('nodes').fetch('root').fetch('inputs').fetch(input)
|
|
39
|
+
lock.fetch('nodes').fetch(node_id).fetch('locked').fetch('rev')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'updates and sets local git inputs with and without commits/changelog' do
|
|
43
|
+
Dir.mktmpdir('confctl-inputs-spec-') do |dir|
|
|
44
|
+
dummy_repo = File.join(dir, 'dummy-input')
|
|
45
|
+
rev_a, = create_dummy_repo(dummy_repo)
|
|
46
|
+
|
|
47
|
+
conf_dir = File.join(dir, 'conf')
|
|
48
|
+
FileUtils.mkdir_p(conf_dir)
|
|
49
|
+
init_git_repo(conf_dir)
|
|
50
|
+
|
|
51
|
+
File.write(File.join(conf_dir, 'flake.nix'), <<~NIX)
|
|
52
|
+
{
|
|
53
|
+
description = "confctl inputs test";
|
|
54
|
+
|
|
55
|
+
inputs = {
|
|
56
|
+
dummy-input.url = "git+file://#{dummy_repo}";
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
outputs = { self, ... }: {};
|
|
60
|
+
}
|
|
61
|
+
NIX
|
|
62
|
+
|
|
63
|
+
git_commit_all(conf_dir, 'fixture: initial')
|
|
64
|
+
|
|
65
|
+
res = run_confctl('inputs', 'update', 'dummy-input', chdir: conf_dir)
|
|
66
|
+
expect(res.success?).to be(true), "#{res.out}\n#{res.err}"
|
|
67
|
+
|
|
68
|
+
lock_path = File.join(conf_dir, 'flake.lock')
|
|
69
|
+
expect(File).to exist(lock_path)
|
|
70
|
+
expect(locked_rev(lock_path, 'dummy-input')).to eq(rev_a)
|
|
71
|
+
|
|
72
|
+
log_count = run_cmd('git', 'rev-list', '--count', 'HEAD', chdir: conf_dir).out.strip.to_i
|
|
73
|
+
expect(log_count).to eq(1)
|
|
74
|
+
|
|
75
|
+
rev_b = commit_dummy(dummy_repo, 'B')
|
|
76
|
+
|
|
77
|
+
res = run_confctl(
|
|
78
|
+
'inputs', 'update', '--commit', '--no-changelog', '--no-editor', 'dummy-input',
|
|
79
|
+
chdir: conf_dir
|
|
80
|
+
)
|
|
81
|
+
expect(res.success?).to be(true), "#{res.out}\n#{res.err}"
|
|
82
|
+
expect(locked_rev(lock_path, 'dummy-input')).to eq(rev_b)
|
|
83
|
+
|
|
84
|
+
log_count = run_cmd('git', 'rev-list', '--count', 'HEAD', chdir: conf_dir).out.strip.to_i
|
|
85
|
+
expect(log_count).to eq(2)
|
|
86
|
+
|
|
87
|
+
last_msg = run_cmd('git', 'log', '-1', '--pretty=%s', chdir: conf_dir).out.strip
|
|
88
|
+
expect(last_msg).to include('inputs: update dummy-input')
|
|
89
|
+
|
|
90
|
+
res = run_confctl(
|
|
91
|
+
'inputs', 'set', '--commit', '--changelog', '--no-editor', 'dummy-input', rev_a,
|
|
92
|
+
chdir: conf_dir
|
|
93
|
+
)
|
|
94
|
+
expect(res.success?).to be(true), "#{res.out}\n#{res.err}"
|
|
95
|
+
expect(locked_rev(lock_path, 'dummy-input')).to eq(rev_a)
|
|
96
|
+
|
|
97
|
+
log_count = run_cmd('git', 'rev-list', '--count', 'HEAD', chdir: conf_dir).out.strip.to_i
|
|
98
|
+
expect(log_count).to eq(3)
|
|
99
|
+
|
|
100
|
+
last_msg = run_cmd('git', 'log', '-1', '--pretty=%s', chdir: conf_dir).out.strip
|
|
101
|
+
expect(last_msg).to include('inputs: set dummy-input')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'confctl'
|
|
5
|
+
require 'shellwords'
|
|
6
|
+
|
|
7
|
+
RSpec.describe ConfCtl::MachineControl do
|
|
8
|
+
let(:runner_class) do
|
|
9
|
+
Class.new do
|
|
10
|
+
attr_reader :calls, :result
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@calls = []
|
|
14
|
+
@result = Object.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run(*args, **kwargs, &)
|
|
18
|
+
calls << { method: :run, args:, kwargs:, block: block_given? }
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run!(*args, **kwargs, &)
|
|
23
|
+
calls << { method: :run!, args:, kwargs:, block: block_given? }
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
let(:runner) { runner_class.new }
|
|
30
|
+
|
|
31
|
+
before do
|
|
32
|
+
allow(ConfCtl::SystemCommand).to receive(:new).and_return(runner)
|
|
33
|
+
allow(ENV).to receive(:fetch).and_call_original
|
|
34
|
+
allow(ENV).to receive(:fetch).with('CONFCTL_SSH_CONFIG', nil).and_return(nil)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def machine(localhost:, target_host: 'web.int.vpsfree.cz', target_port: 22)
|
|
38
|
+
instance_double(
|
|
39
|
+
ConfCtl::Machine,
|
|
40
|
+
localhost?: localhost,
|
|
41
|
+
target_host:,
|
|
42
|
+
target_port:
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'passes localhost commands as literal argv' do
|
|
47
|
+
command = %w[printf %s] + ['Host: vpsfree.cz']
|
|
48
|
+
|
|
49
|
+
ret = described_class.new(machine(localhost: true)).execute(*command)
|
|
50
|
+
|
|
51
|
+
expect(ret).to equal(runner.result)
|
|
52
|
+
expect(runner.calls).to contain_exactly(
|
|
53
|
+
method: :run,
|
|
54
|
+
args: command,
|
|
55
|
+
kwargs: {},
|
|
56
|
+
block: false
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'shell-quotes remote argv into one ssh command argument' do
|
|
61
|
+
command = [
|
|
62
|
+
'printf',
|
|
63
|
+
'%s',
|
|
64
|
+
'Host: vpsfree.cz',
|
|
65
|
+
"quote'arg",
|
|
66
|
+
'',
|
|
67
|
+
'semi;colon',
|
|
68
|
+
'dollar$arg'
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
ret = described_class.new(machine(localhost: false, target_port: 2222)).execute!(
|
|
72
|
+
*command,
|
|
73
|
+
input: 'stdin'
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(ret).to equal(runner.result)
|
|
77
|
+
expect(runner.calls).to contain_exactly(
|
|
78
|
+
method: :run!,
|
|
79
|
+
args: [
|
|
80
|
+
'ssh',
|
|
81
|
+
'-l',
|
|
82
|
+
'root',
|
|
83
|
+
'-p',
|
|
84
|
+
'2222',
|
|
85
|
+
'web.int.vpsfree.cz',
|
|
86
|
+
Shellwords.join(command)
|
|
87
|
+
],
|
|
88
|
+
kwargs: { input: 'stdin' },
|
|
89
|
+
block: false
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'keeps extra ssh options before the target host' do
|
|
94
|
+
mc = described_class.new(machine(localhost: false))
|
|
95
|
+
|
|
96
|
+
mc.send(:with_ssh_opts, '-o', 'ConnectTimeout=3') do
|
|
97
|
+
mc.execute!('true')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
expect(runner.calls.first[:args]).to eq(
|
|
101
|
+
[
|
|
102
|
+
'ssh',
|
|
103
|
+
'-l',
|
|
104
|
+
'root',
|
|
105
|
+
'-o',
|
|
106
|
+
'ConnectTimeout=3',
|
|
107
|
+
'web.int.vpsfree.cz',
|
|
108
|
+
'true'
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'confctl'
|
|
5
|
+
|
|
6
|
+
RSpec.describe ConfCtl::MachineList do
|
|
7
|
+
def machine(managed: true, runnable: true, checks: [])
|
|
8
|
+
instance_double(
|
|
9
|
+
ConfCtl::Machine,
|
|
10
|
+
managed:,
|
|
11
|
+
runnable?: runnable,
|
|
12
|
+
health_checks: checks
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'selects only runnable machines' do
|
|
17
|
+
direct = machine
|
|
18
|
+
carried = machine(runnable: false)
|
|
19
|
+
no_target = machine(runnable: false)
|
|
20
|
+
|
|
21
|
+
list = described_class.new(
|
|
22
|
+
machines: {
|
|
23
|
+
'direct' => direct,
|
|
24
|
+
'carried' => carried,
|
|
25
|
+
'no-target' => no_target
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
expect(list.runnable.to_a).to eq([direct])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'returns health checks only from runnable machines' do
|
|
33
|
+
direct_check = instance_double(ConfCtl::HealthChecks::Base)
|
|
34
|
+
carried_check = instance_double(ConfCtl::HealthChecks::Base)
|
|
35
|
+
direct = machine(checks: [direct_check])
|
|
36
|
+
carried = machine(runnable: false, checks: [carried_check])
|
|
37
|
+
|
|
38
|
+
list = described_class.new(
|
|
39
|
+
machines: {
|
|
40
|
+
'direct' => direct,
|
|
41
|
+
'carried' => carried
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(list.health_checks).to eq([direct_check])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'confctl'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
RSpec.describe ConfCtl::NixFlake do
|
|
8
|
+
it 'loads machine metadata from a built JSON file' do
|
|
9
|
+
Dir.mktmpdir do |dir|
|
|
10
|
+
json_path = File.join(dir, 'machine-list.json')
|
|
11
|
+
File.write(
|
|
12
|
+
json_path,
|
|
13
|
+
{
|
|
14
|
+
'host.example' => {
|
|
15
|
+
'key' => 'm_host_example',
|
|
16
|
+
'_module' => {},
|
|
17
|
+
'metaConfig' => {
|
|
18
|
+
'_module' => {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}.to_json
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
nix = described_class.new(conf_dir: dir, max_jobs: 'auto')
|
|
25
|
+
allow(nix).to receive(:nix_build_json)
|
|
26
|
+
.with(['.#confctl.machinesJson'])
|
|
27
|
+
.and_return([{ 'outputs' => { 'out' => json_path } }])
|
|
28
|
+
allow(nix).to receive(:nix_eval_json)
|
|
29
|
+
|
|
30
|
+
machines = nix.list_machines
|
|
31
|
+
|
|
32
|
+
expect(machines).to eq(
|
|
33
|
+
'host.example' => {
|
|
34
|
+
'key' => 'm_host_example',
|
|
35
|
+
'metaConfig' => {}
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
expect(nix).not_to have_received(:nix_eval_json)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'swpins workflows' do
|
|
7
|
+
include CliHelper
|
|
8
|
+
|
|
9
|
+
def create_repo_with_commit(path, content, message)
|
|
10
|
+
File.write(File.join(path, 'dummy.txt'), content)
|
|
11
|
+
git_commit_all(path, message)
|
|
12
|
+
run_cmd('git', 'rev-parse', 'HEAD', chdir: path).out.strip
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def channel_rev(conf_dir)
|
|
16
|
+
json = JSON.parse(File.read(File.join(conf_dir, 'swpins', 'channels', 'nixos-unstable.json')))
|
|
17
|
+
json.fetch('nixpkgs').fetch('state').fetch('rev')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'updates and sets swpins from a local git repository with commit variants' do
|
|
21
|
+
Dir.mktmpdir('confctl-swpins-spec-') do |dir|
|
|
22
|
+
nixpkgs_path = ENV.fetch('CONFCTL_TEST_NIXPKGS', nil)
|
|
23
|
+
if nixpkgs_path.nil? || nixpkgs_path.empty?
|
|
24
|
+
lookup = run_cmd('nix-instantiate', '--find-file', 'nixpkgs', chdir: dir)
|
|
25
|
+
expect(lookup.success?).to be(true), "#{lookup.out}\n#{lookup.err}"
|
|
26
|
+
nixpkgs_path = lookup.out.strip
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
dummy_repo = File.join(dir, 'dummy-repo')
|
|
30
|
+
FileUtils.mkdir_p(dummy_repo)
|
|
31
|
+
init_git_repo(dummy_repo)
|
|
32
|
+
rev_a = create_repo_with_commit(dummy_repo, "A\n", 'dummy: A')
|
|
33
|
+
|
|
34
|
+
branch = run_cmd('git', 'rev-parse', '--abbrev-ref', 'HEAD', chdir: dummy_repo).out.strip
|
|
35
|
+
|
|
36
|
+
conf_dir = File.join(dir, 'conf')
|
|
37
|
+
FileUtils.mkdir_p(conf_dir)
|
|
38
|
+
expect(run_confctl('init', '--swpins', chdir: conf_dir).success?).to be(true)
|
|
39
|
+
|
|
40
|
+
File.write(File.join(conf_dir, 'configs', 'swpins.nix'), <<~NIX)
|
|
41
|
+
{ config, ... }:
|
|
42
|
+
{
|
|
43
|
+
confctl.swpins.core.pins.nixpkgs = {
|
|
44
|
+
type = "directory";
|
|
45
|
+
directory.path = "#{nixpkgs_path}";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
confctl.swpins.channels = {
|
|
49
|
+
nixos-unstable = {
|
|
50
|
+
nixpkgs = {
|
|
51
|
+
type = "git-rev";
|
|
52
|
+
git-rev = {
|
|
53
|
+
url = "file://#{dummy_repo}";
|
|
54
|
+
fetchSubmodules = false;
|
|
55
|
+
update = {
|
|
56
|
+
auto = false;
|
|
57
|
+
interval = 0;
|
|
58
|
+
ref = "refs/heads/#{branch}";
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
NIX
|
|
66
|
+
|
|
67
|
+
init_git_repo(conf_dir)
|
|
68
|
+
git_commit_all(conf_dir, 'fixture: initial')
|
|
69
|
+
|
|
70
|
+
res =
|
|
71
|
+
if ENV['CONFCTL_RSPEC_SANDBOX'] == '1'
|
|
72
|
+
run_confctl('swpins', 'channel', 'update', 'nixos-unstable', 'nixpkgs', chdir: conf_dir)
|
|
73
|
+
else
|
|
74
|
+
run_confctl('swpins', 'update', chdir: conf_dir)
|
|
75
|
+
end
|
|
76
|
+
expect(res.success?).to be(true), "#{res.out}\n#{res.err}"
|
|
77
|
+
expect(channel_rev(conf_dir)).to eq(rev_a)
|
|
78
|
+
|
|
79
|
+
rev_b = create_repo_with_commit(dummy_repo, "B\n", 'dummy: B')
|
|
80
|
+
|
|
81
|
+
res = run_confctl(
|
|
82
|
+
'swpins', 'channel', 'update',
|
|
83
|
+
'--commit', '--no-changelog', '--no-editor',
|
|
84
|
+
'nixos-unstable', 'nixpkgs',
|
|
85
|
+
chdir: conf_dir
|
|
86
|
+
)
|
|
87
|
+
expect(res.success?).to be(true), "#{res.out}\n#{res.err}"
|
|
88
|
+
expect(channel_rev(conf_dir)).to eq(rev_b)
|
|
89
|
+
|
|
90
|
+
log_count = run_cmd('git', 'rev-list', '--count', 'HEAD', chdir: conf_dir).out.strip.to_i
|
|
91
|
+
expect(log_count).to eq(2)
|
|
92
|
+
|
|
93
|
+
res = run_confctl(
|
|
94
|
+
'swpins', 'channel', 'set',
|
|
95
|
+
'--commit', '--changelog', '--no-editor',
|
|
96
|
+
'nixos-unstable', 'nixpkgs', rev_a,
|
|
97
|
+
chdir: conf_dir
|
|
98
|
+
)
|
|
99
|
+
expect(res.success?).to be(true), "#{res.out}\n#{res.err}"
|
|
100
|
+
expect(channel_rev(conf_dir)).to eq(rev_a)
|
|
101
|
+
|
|
102
|
+
log_count = run_cmd('git', 'rev-list', '--count', 'HEAD', chdir: conf_dir).out.strip.to_i
|
|
103
|
+
expect(log_count).to eq(3)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'generation modes' do
|
|
8
|
+
it 'preserves swpins and flakes generation modes and metadata' do
|
|
9
|
+
Dir.mktmpdir('confctl-generation-modes-') do |tmp|
|
|
10
|
+
Dir.chdir(tmp) do
|
|
11
|
+
require 'confctl'
|
|
12
|
+
|
|
13
|
+
allow(ConfCtl::GCRoot).to receive(:dir).and_return(File.join(ConfCtl::ConfDir.cache_dir, 'gcroots'))
|
|
14
|
+
|
|
15
|
+
host = 'example.test'
|
|
16
|
+
host_dir = File.join(ConfCtl::ConfDir.generation_dir, ConfCtl.safe_host_name(host))
|
|
17
|
+
FileUtils.mkdir_p(host_dir)
|
|
18
|
+
|
|
19
|
+
legacy_name = '2026-02-24--00-00-00'
|
|
20
|
+
legacy_dir = File.join(host_dir, legacy_name)
|
|
21
|
+
FileUtils.mkdir_p(legacy_dir)
|
|
22
|
+
|
|
23
|
+
legacy_data = {
|
|
24
|
+
'date' => '2026-02-24T12:34:56Z',
|
|
25
|
+
'toplevel' => '/nix/store/legacy-system',
|
|
26
|
+
'auto_rollback' => '/nix/store/legacy-auto-rollback.rb',
|
|
27
|
+
'swpins' => {
|
|
28
|
+
'nixpkgs' => {
|
|
29
|
+
'path' => '/nix/store/legacy-nixpkgs',
|
|
30
|
+
'spec' => {
|
|
31
|
+
'type' => 'git-rev',
|
|
32
|
+
'name' => 'nixpkgs',
|
|
33
|
+
'nix_options' => {
|
|
34
|
+
'url' => 'https://example.com/nixpkgs',
|
|
35
|
+
'fetchSubmodules' => false,
|
|
36
|
+
'update' => {
|
|
37
|
+
'auto' => false,
|
|
38
|
+
'interval' => 0,
|
|
39
|
+
'ref' => nil
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
'state' => {
|
|
43
|
+
'rev' => 'deadbeef'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
File.write(File.join(legacy_dir, 'generation.json'), JSON.pretty_generate(legacy_data))
|
|
51
|
+
|
|
52
|
+
legacy_gen = ConfCtl::Generation::Build.new(host)
|
|
53
|
+
legacy_gen.load(legacy_name)
|
|
54
|
+
expect(legacy_gen.mode).to eq('swpins')
|
|
55
|
+
|
|
56
|
+
swpin_spec = ConfCtl::Swpins::Spec.for(:'git-rev').new(
|
|
57
|
+
'nixpkgs',
|
|
58
|
+
{
|
|
59
|
+
'url' => 'https://example.com/nixpkgs',
|
|
60
|
+
'fetchSubmodules' => false,
|
|
61
|
+
'update' => {
|
|
62
|
+
'auto' => false,
|
|
63
|
+
'interval' => 0,
|
|
64
|
+
'ref' => nil
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
'type' => 'git-rev',
|
|
69
|
+
'name' => 'nixpkgs',
|
|
70
|
+
'nix_options' => {
|
|
71
|
+
'url' => 'https://example.com/nixpkgs',
|
|
72
|
+
'fetchSubmodules' => false,
|
|
73
|
+
'update' => {
|
|
74
|
+
'auto' => false,
|
|
75
|
+
'interval' => 0,
|
|
76
|
+
'ref' => nil
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
'state' => {
|
|
80
|
+
'rev' => 'beadfeed'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
swpins_gen = ConfCtl::Generation::Build.new(host)
|
|
86
|
+
swpins_gen.create(
|
|
87
|
+
'/nix/store/new-system',
|
|
88
|
+
'/nix/store/new-auto-rollback.rb',
|
|
89
|
+
{ 'nixpkgs' => '/nix/store/new-nixpkgs' },
|
|
90
|
+
{ 'nixpkgs' => swpin_spec },
|
|
91
|
+
date: Time.utc(2026, 2, 24, 13, 0, 0)
|
|
92
|
+
)
|
|
93
|
+
swpins_gen.save
|
|
94
|
+
|
|
95
|
+
swpins_list = ConfCtl::Generation::BuildList.new(host)
|
|
96
|
+
swpins_loaded = swpins_list[swpins_gen.name]
|
|
97
|
+
expect(swpins_loaded.mode).to eq('swpins')
|
|
98
|
+
expect(swpins_loaded.swpin_paths).to eq(swpins_gen.swpin_paths)
|
|
99
|
+
|
|
100
|
+
flakes_gen = ConfCtl::Generation::Build.new(host)
|
|
101
|
+
flakes_gen.create_flake(
|
|
102
|
+
'/nix/store/flake-system',
|
|
103
|
+
'/nix/store/flake-auto-rollback.rb',
|
|
104
|
+
inputs: { 'nixpkgs' => '/nix/store/flake-nixpkgs' },
|
|
105
|
+
inputs_info: {
|
|
106
|
+
'nixpkgs' => {
|
|
107
|
+
'input' => 'nixpkgs',
|
|
108
|
+
'url' => 'https://github.com/NixOS/nixpkgs',
|
|
109
|
+
'rev' => 'cafebabe',
|
|
110
|
+
'shortRev' => 'cafebabe',
|
|
111
|
+
'lastModified' => 0
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
date: Time.utc(2026, 2, 24, 14, 0, 0)
|
|
115
|
+
)
|
|
116
|
+
flakes_gen.save
|
|
117
|
+
|
|
118
|
+
flakes_list = ConfCtl::Generation::BuildList.new(host)
|
|
119
|
+
flakes_loaded = flakes_list[flakes_gen.name]
|
|
120
|
+
expect(flakes_loaded.mode).to eq('flakes')
|
|
121
|
+
expect(flakes_loaded.inputs).to eq(flakes_gen.inputs)
|
|
122
|
+
expect(flakes_loaded.swpin_specs).to be_empty
|
|
123
|
+
|
|
124
|
+
flakes_json = JSON.parse(File.read(File.join(flakes_loaded.dir, 'generation.json')))
|
|
125
|
+
expect(flakes_json).not_to have_key('swpins')
|
|
126
|
+
|
|
127
|
+
expect(swpins_list.find(swpins_loaded.toplevel, swpins_loaded.swpin_paths)).to be_truthy
|
|
128
|
+
expect(flakes_list.find(flakes_loaded.toplevel, flakes_loaded.inputs, mode: 'flakes')).to be_truthy
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|