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,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'confctl/flake_lock_diff'
|
|
4
|
+
require 'confctl/inputs/commit_message'
|
|
5
|
+
|
|
6
|
+
RSpec.describe ConfCtl::Inputs::CommitMessage do
|
|
7
|
+
def build_change(name:, old_rev:, new_rev:, url:)
|
|
8
|
+
ConfCtl::FlakeLockDiff::Change.new(
|
|
9
|
+
name: name,
|
|
10
|
+
old_info: old_rev ? { rev: old_rev, short_rev: old_rev[0, 8], url: url } : nil,
|
|
11
|
+
new_info: { rev: new_rev, short_rev: new_rev[0, 8], url: url }
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def expect_git_log(url:, from:, to:, output:)
|
|
16
|
+
mirror = instance_double(ConfCtl::GitRepoMirror)
|
|
17
|
+
|
|
18
|
+
expect(ConfCtl::GitRepoMirror).to receive(:new).with(url, quiet: true).and_return(mirror)
|
|
19
|
+
expect(mirror).to receive(:setup)
|
|
20
|
+
expect(mirror).to receive(:log).with(from, to, opts: ['--oneline']).and_return(output)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'includes the new revision in title for a single input' do
|
|
24
|
+
changes = [
|
|
25
|
+
build_change(
|
|
26
|
+
name: 'nixpkgs',
|
|
27
|
+
old_rev: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
28
|
+
new_rev: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
29
|
+
url: 'https://github.com/NixOS/nixpkgs'
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
title = described_class.build(changes: changes, changelog: false).lines.first.chomp
|
|
34
|
+
|
|
35
|
+
expect(title).to eq('inputs: update nixpkgs to bbbbbbbb')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'includes the new revision in title for multiple inputs from the same source' do
|
|
39
|
+
changes = [
|
|
40
|
+
build_change(
|
|
41
|
+
name: 'nixpkgsStable',
|
|
42
|
+
old_rev: '1111111111111111111111111111111111111111',
|
|
43
|
+
new_rev: '2222222222222222222222222222222222222222',
|
|
44
|
+
url: 'https://github.com/NixOS/nixpkgs'
|
|
45
|
+
),
|
|
46
|
+
build_change(
|
|
47
|
+
name: 'nixpkgsUnstable',
|
|
48
|
+
old_rev: '3333333333333333333333333333333333333333',
|
|
49
|
+
new_rev: '2222222222222222222222222222222222222222',
|
|
50
|
+
url: 'https://github.com/NixOS/nixpkgs/'
|
|
51
|
+
)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
title = described_class.build(changes: changes, changelog: false).lines.first.chomp
|
|
55
|
+
|
|
56
|
+
expect(title).to eq('inputs: update nixpkgsStable, nixpkgsUnstable to 22222222')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'keeps no-changelog input summaries compact' do
|
|
60
|
+
changes = [
|
|
61
|
+
build_change(
|
|
62
|
+
name: 'nixpkgsStable',
|
|
63
|
+
old_rev: '1111111111111111111111111111111111111111',
|
|
64
|
+
new_rev: '2222222222222222222222222222222222222222',
|
|
65
|
+
url: 'https://github.com/NixOS/nixpkgs'
|
|
66
|
+
),
|
|
67
|
+
build_change(
|
|
68
|
+
name: 'nixpkgsUnstable',
|
|
69
|
+
old_rev: '3333333333333333333333333333333333333333',
|
|
70
|
+
new_rev: '2222222222222222222222222222222222222222',
|
|
71
|
+
url: 'https://github.com/NixOS/nixpkgs'
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
message = described_class.build(changes: changes, changelog: false)
|
|
76
|
+
|
|
77
|
+
expect(message).to eq(<<~MSG.strip)
|
|
78
|
+
inputs: update nixpkgsStable, nixpkgsUnstable to 22222222
|
|
79
|
+
|
|
80
|
+
nixpkgsStable: 11111111 -> 22222222
|
|
81
|
+
nixpkgsUnstable: 33333333 -> 22222222
|
|
82
|
+
MSG
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'groups changelog for inputs with the same source and same revision change' do
|
|
86
|
+
old_rev = '1111111111111111111111111111111111111111'
|
|
87
|
+
new_rev = '2222222222222222222222222222222222222222'
|
|
88
|
+
url = 'https://github.com/vpsfreecz/vpsadminos'
|
|
89
|
+
log = <<~LOG.strip
|
|
90
|
+
git log for #{old_rev}..#{new_rev}
|
|
91
|
+
> 222222222 shared change
|
|
92
|
+
LOG
|
|
93
|
+
|
|
94
|
+
changes = [
|
|
95
|
+
build_change(
|
|
96
|
+
name: 'vpsadminosStaging',
|
|
97
|
+
old_rev: old_rev,
|
|
98
|
+
new_rev: new_rev,
|
|
99
|
+
url: url
|
|
100
|
+
),
|
|
101
|
+
build_change(
|
|
102
|
+
name: 'vpsadminosProduction',
|
|
103
|
+
old_rev: old_rev,
|
|
104
|
+
new_rev: new_rev,
|
|
105
|
+
url: "#{url}/"
|
|
106
|
+
)
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
expect_git_log(url: url, from: old_rev, to: new_rev, output: log)
|
|
110
|
+
|
|
111
|
+
message = described_class.build(changes: changes, changelog: true)
|
|
112
|
+
|
|
113
|
+
expect(message).to eq(<<~MSG.strip)
|
|
114
|
+
inputs: update vpsadminosProduction, vpsadminosStaging to 22222222
|
|
115
|
+
|
|
116
|
+
vpsadminosStaging: 11111111 -> 22222222
|
|
117
|
+
vpsadminosProduction: 11111111 -> 22222222
|
|
118
|
+
git log for #{old_rev}..#{new_rev}
|
|
119
|
+
> 222222222 shared change
|
|
120
|
+
MSG
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'keeps separate changelog groups for inputs with different old revisions' do
|
|
124
|
+
old_staging_rev = '1111111111111111111111111111111111111111'
|
|
125
|
+
old_os_staging_rev = '3333333333333333333333333333333333333333'
|
|
126
|
+
new_rev = '2222222222222222222222222222222222222222'
|
|
127
|
+
url = 'https://github.com/vpsfreecz/vpsadminos'
|
|
128
|
+
staging_log = <<~LOG.strip
|
|
129
|
+
git log for #{old_staging_rev}..#{new_rev}
|
|
130
|
+
> 222222222 staging change
|
|
131
|
+
LOG
|
|
132
|
+
os_staging_log = <<~LOG.strip
|
|
133
|
+
git log for #{old_os_staging_rev}..#{new_rev}
|
|
134
|
+
> 222222222 os staging change
|
|
135
|
+
LOG
|
|
136
|
+
|
|
137
|
+
changes = [
|
|
138
|
+
build_change(
|
|
139
|
+
name: 'vpsadminosStaging',
|
|
140
|
+
old_rev: old_staging_rev,
|
|
141
|
+
new_rev: new_rev,
|
|
142
|
+
url: url
|
|
143
|
+
),
|
|
144
|
+
build_change(
|
|
145
|
+
name: 'vpsadminosProduction',
|
|
146
|
+
old_rev: old_os_staging_rev,
|
|
147
|
+
new_rev: new_rev,
|
|
148
|
+
url: url
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
expect_git_log(url: url, from: old_staging_rev, to: new_rev, output: staging_log)
|
|
153
|
+
expect_git_log(url: url, from: old_os_staging_rev, to: new_rev, output: os_staging_log)
|
|
154
|
+
|
|
155
|
+
message = described_class.build(changes: changes, changelog: true)
|
|
156
|
+
|
|
157
|
+
expect(message).to eq(<<~MSG.strip)
|
|
158
|
+
inputs: update vpsadminosProduction, vpsadminosStaging to 22222222
|
|
159
|
+
|
|
160
|
+
vpsadminosStaging: 11111111 -> 22222222
|
|
161
|
+
git log for #{old_staging_rev}..#{new_rev}
|
|
162
|
+
> 222222222 staging change
|
|
163
|
+
|
|
164
|
+
vpsadminosProduction: 33333333 -> 22222222
|
|
165
|
+
git log for #{old_os_staging_rev}..#{new_rev}
|
|
166
|
+
> 222222222 os staging change
|
|
167
|
+
MSG
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'does not include the revision in title for changes from different sources' do
|
|
171
|
+
changes = [
|
|
172
|
+
build_change(
|
|
173
|
+
name: 'nixpkgs',
|
|
174
|
+
old_rev: '4444444444444444444444444444444444444444',
|
|
175
|
+
new_rev: '5555555555555555555555555555555555555555',
|
|
176
|
+
url: 'https://github.com/NixOS/nixpkgs'
|
|
177
|
+
),
|
|
178
|
+
build_change(
|
|
179
|
+
name: 'vpsadminos',
|
|
180
|
+
old_rev: '6666666666666666666666666666666666666666',
|
|
181
|
+
new_rev: '5555555555555555555555555555555555555555',
|
|
182
|
+
url: 'https://github.com/vpsfreecz/vpsadminos'
|
|
183
|
+
)
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
title = described_class.build(changes: changes, changelog: false).lines.first.chomp
|
|
187
|
+
|
|
188
|
+
expect(title).to eq('inputs: update nixpkgs, vpsadminos')
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'includes revision in title for set action' do
|
|
192
|
+
changes = [
|
|
193
|
+
build_change(
|
|
194
|
+
name: 'vpsadminos',
|
|
195
|
+
old_rev: nil,
|
|
196
|
+
new_rev: '7777777777777777777777777777777777777777',
|
|
197
|
+
url: 'https://github.com/vpsfreecz/vpsadminos'
|
|
198
|
+
)
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
title = described_class.build(changes: changes, changelog: false, action: :set).lines.first.chomp
|
|
202
|
+
|
|
203
|
+
expect(title).to eq('inputs: set vpsadminos to 77777777')
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'confctl/exceptions'
|
|
5
|
+
require 'confctl/inputs/setter'
|
|
6
|
+
require 'confctl/inputs/updater'
|
|
7
|
+
require 'tmpdir'
|
|
8
|
+
require 'tty-command'
|
|
9
|
+
|
|
10
|
+
RSpec.describe 'Nix cached fallback handling' do
|
|
11
|
+
def cached_fallback_result
|
|
12
|
+
stderr = <<~ERR
|
|
13
|
+
warning: error: unable to download 'https://api.github.com/repos/vpsfreecz/vpsadminos/commits/staging': HTTP error 403
|
|
14
|
+
{"message":"API rate limit exceeded"}; using cached version
|
|
15
|
+
ERR
|
|
16
|
+
|
|
17
|
+
TTY::Command::Result.new(0, '', stderr)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:cmd) { instance_double(TTY::Command) }
|
|
21
|
+
|
|
22
|
+
before do
|
|
23
|
+
allow(ConfCtl::SystemCommand).to receive(:new).and_return(cmd)
|
|
24
|
+
allow(cmd).to receive(:run).and_return(cached_fallback_result)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'fails flake updates that used cached GitHub metadata' do
|
|
28
|
+
Dir.mktmpdir do |dir|
|
|
29
|
+
expect do
|
|
30
|
+
ConfCtl::Inputs::Updater.send(:run_nix_flake_update!, dir, ['vpsadminos'])
|
|
31
|
+
end.to raise_error(ConfCtl::Error, /GitHub API rate limit was exceeded/)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'fails flake locks that used cached GitHub metadata' do
|
|
36
|
+
Dir.mktmpdir do |dir|
|
|
37
|
+
tmpfile = File.join(dir, 'flake.lock.tmp')
|
|
38
|
+
|
|
39
|
+
expect do
|
|
40
|
+
ConfCtl::Inputs::Setter.send(
|
|
41
|
+
:run_nix_flake_lock!,
|
|
42
|
+
dir,
|
|
43
|
+
'vpsadminos',
|
|
44
|
+
'github:vpsfreecz/vpsadminos/staging',
|
|
45
|
+
tmpfile
|
|
46
|
+
)
|
|
47
|
+
end.to raise_error(ConfCtl::Error, /GitHub API rate limit was exceeded/)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'confctl/exceptions'
|
|
5
|
+
require 'confctl/inputs/nix_output_guard'
|
|
6
|
+
require 'tty-command'
|
|
7
|
+
|
|
8
|
+
RSpec.describe ConfCtl::Inputs::NixOutputGuard do
|
|
9
|
+
def command_result(stderr)
|
|
10
|
+
TTY::Command::Result.new(0, '', stderr)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'raises when Nix falls back to cached GitHub metadata after rate limiting' do
|
|
14
|
+
stderr = <<~ERR
|
|
15
|
+
warning: error: unable to download 'https://api.github.com/repos/vpsfreecz/vpsadminos/commits/staging': HTTP error 403
|
|
16
|
+
|
|
17
|
+
response body:
|
|
18
|
+
|
|
19
|
+
{"message":"API rate limit exceeded for 192.0.2.1."}; using cached version
|
|
20
|
+
ERR
|
|
21
|
+
|
|
22
|
+
expect do
|
|
23
|
+
described_class.check!(command_result(stderr))
|
|
24
|
+
end.to raise_error(ConfCtl::Error, /GitHub API rate limit was exceeded/)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'raises when Nix falls back to cached GitHub metadata for another API error' do
|
|
28
|
+
stderr = <<~ERR
|
|
29
|
+
warning: error: unable to download 'https://api.github.com/repos/example/project/commits/main': HTTP error 503
|
|
30
|
+
; using cached version
|
|
31
|
+
ERR
|
|
32
|
+
|
|
33
|
+
expect do
|
|
34
|
+
described_class.check!(command_result(stderr))
|
|
35
|
+
end.to raise_error(ConfCtl::Error, /used a cached version/)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'does not raise for unrelated Nix warnings' do
|
|
39
|
+
stderr = "warning: '--update-input' is a deprecated alias for 'flake update'\n"
|
|
40
|
+
|
|
41
|
+
expect do
|
|
42
|
+
described_class.check!(command_result(stderr))
|
|
43
|
+
end.not_to raise_error
|
|
44
|
+
end
|
|
45
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$:.unshift(File.expand_path('../lib', __dir__))
|
|
4
|
+
|
|
5
|
+
RSpec.configure do |config|
|
|
6
|
+
config.disable_monkey_patching!
|
|
7
|
+
config.expect_with :rspec do |expectations|
|
|
8
|
+
expectations.syntax = :expect
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Dir[File.join(__dir__, 'support', '*.rb')].each { |f| require f }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module CliHelper
|
|
7
|
+
Result = Struct.new(:out, :err, :status) do
|
|
8
|
+
def success?
|
|
9
|
+
status.success?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def repo_root
|
|
14
|
+
File.expand_path('../..', __dir__)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def confctl_bin
|
|
18
|
+
ENV.fetch('CONFCTL_BIN', File.join(repo_root, 'bin', 'confctl'))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run_confctl(*, chdir:, env: {})
|
|
22
|
+
default_env = {
|
|
23
|
+
'CONFCTL_TTY' => '0',
|
|
24
|
+
'NO_COLOR' => '1',
|
|
25
|
+
'PAGER' => ''
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
out, err, status = Open3.capture3(default_env.merge(env), confctl_bin, *, chdir:)
|
|
29
|
+
Result.new(out, err, status)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run_cmd(*, chdir:, env: {})
|
|
33
|
+
out, err, status = Open3.capture3(env, *, chdir:)
|
|
34
|
+
Result.new(out, err, status)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def init_git_repo(path)
|
|
38
|
+
run_cmd('git', 'init', chdir: path).tap do |r|
|
|
39
|
+
raise "git init failed: #{r.err}" unless r.success?
|
|
40
|
+
end
|
|
41
|
+
run_cmd('git', 'config', 'user.email', 'confctl-tests@example.invalid', chdir: path).tap do |r|
|
|
42
|
+
raise "git config user.email failed: #{r.err}" unless r.success?
|
|
43
|
+
end
|
|
44
|
+
run_cmd('git', 'config', 'user.name', 'confctl-tests', chdir: path).tap do |r|
|
|
45
|
+
raise "git config user.name failed: #{r.err}" unless r.success?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def git_commit_all(path, message)
|
|
50
|
+
run_cmd('git', 'add', '.', chdir: path).tap do |r|
|
|
51
|
+
raise "git add failed: #{r.err}" unless r.success?
|
|
52
|
+
end
|
|
53
|
+
run_cmd('git', 'commit', '-m', message, chdir: path).tap do |r|
|
|
54
|
+
raise "git commit failed: #{r.err}" unless r.success?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/test-runner.sh
ADDED
data/tests/all-tests.nix
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
pkgs ? <nixpkgs>,
|
|
3
|
+
system ? builtins.currentSystem,
|
|
4
|
+
suiteArgs ? { },
|
|
5
|
+
testConfig ? { },
|
|
6
|
+
configuration ? null,
|
|
7
|
+
testFramework,
|
|
8
|
+
}:
|
|
9
|
+
let
|
|
10
|
+
nixpkgs = import pkgs { inherit system; };
|
|
11
|
+
lib = nixpkgs.lib;
|
|
12
|
+
testLib = testFramework.makeTestLib {
|
|
13
|
+
inherit
|
|
14
|
+
pkgs
|
|
15
|
+
system
|
|
16
|
+
lib
|
|
17
|
+
suiteArgs
|
|
18
|
+
testConfig
|
|
19
|
+
configuration
|
|
20
|
+
;
|
|
21
|
+
suitePath = ./suite;
|
|
22
|
+
};
|
|
23
|
+
in
|
|
24
|
+
testLib.makeTests [
|
|
25
|
+
"carrier/deploy"
|
|
26
|
+
"carrier/netboot"
|
|
27
|
+
"deploy/flakes"
|
|
28
|
+
"deploy/swpins"
|
|
29
|
+
"auto_rollback"
|
|
30
|
+
]
|
data/tests/make-test.nix
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
testFn:
|
|
2
|
+
{ testFramework, ... }@args:
|
|
3
|
+
let
|
|
4
|
+
upstream = testFramework.makeTest testFn;
|
|
5
|
+
mergedExtraArgs = {
|
|
6
|
+
vpsadminos = testFramework.sourcePath;
|
|
7
|
+
confctlSrc = args.confctlSrc or null;
|
|
8
|
+
confctlPackage = args.confctlPackage or null;
|
|
9
|
+
}
|
|
10
|
+
// (args.extraArgs or { });
|
|
11
|
+
argsWithExtra = args // {
|
|
12
|
+
extraArgs = mergedExtraArgs;
|
|
13
|
+
};
|
|
14
|
+
in
|
|
15
|
+
upstream argsWithExtra
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'open3'
|
|
6
|
+
|
|
7
|
+
module ConfctlTestHelpers
|
|
8
|
+
STORE_PATH_RE = %r{/nix/store/[0-9a-z]{32}-[^\s'"]+}
|
|
9
|
+
|
|
10
|
+
def confctl_setup!(bin:, src:, conf_dir:, home_dir:, nix_path: nil)
|
|
11
|
+
@confctl_bin = bin
|
|
12
|
+
@confctl_src = src
|
|
13
|
+
@conf_dir = conf_dir
|
|
14
|
+
@home_dir = home_dir
|
|
15
|
+
@nix_path = nix_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def confctl_bin
|
|
19
|
+
@confctl_bin || raise('confctl_setup! must be called before confctl helpers are used')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def confctl_src
|
|
23
|
+
@confctl_src || raise('confctl_setup! must be called before confctl helpers are used')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def conf_dir
|
|
27
|
+
@conf_dir || raise('confctl_setup! must be called before confctl helpers are used')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def home_dir
|
|
31
|
+
@home_dir || raise('confctl_setup! must be called before confctl helpers are used')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def nix_path
|
|
35
|
+
@nix_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run_local(argv, chdir: nil, env: {})
|
|
39
|
+
kwargs = {}
|
|
40
|
+
kwargs[:chdir] = chdir if chdir
|
|
41
|
+
out, err, status = Open3.capture3(env, *argv, **kwargs)
|
|
42
|
+
[out, err, status]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run_local!(argv, chdir: nil, env: {})
|
|
46
|
+
out, err, status = run_local(argv, chdir: chdir, env: env)
|
|
47
|
+
|
|
48
|
+
return [out, err] if status.success?
|
|
49
|
+
|
|
50
|
+
msg = []
|
|
51
|
+
msg << "Command failed: #{argv.join(' ')}"
|
|
52
|
+
msg << "Exit status: #{status.exitstatus}"
|
|
53
|
+
msg << "STDOUT:\n#{out}" unless out.empty?
|
|
54
|
+
msg << "STDERR:\n#{err}" unless err.empty?
|
|
55
|
+
raise msg.join("\n\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def prepare_fixture_dir!(path, template: 'example-flake')
|
|
59
|
+
FileUtils.rm_rf(path)
|
|
60
|
+
FileUtils.mkdir_p(path)
|
|
61
|
+
|
|
62
|
+
run_local!(['cp', '-r', "#{File.join(confctl_src, template)}/.", path])
|
|
63
|
+
run_local!(['chmod', '-R', 'u+w', path])
|
|
64
|
+
|
|
65
|
+
# Test fixtures regenerate local state such as swpins pins and channels.
|
|
66
|
+
# Drop any copied state from the repository template to keep runs deterministic.
|
|
67
|
+
%w[.bin .confctl .gems .man swpins].each do |dir|
|
|
68
|
+
FileUtils.rm_rf(File.join(path, dir))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def nix_escape_string(str)
|
|
73
|
+
str.gsub('\\', '\\\\').gsub('"', '\\"')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write_admin_ssh_keys!(path, pubkey, admin_name: 'someone')
|
|
77
|
+
escaped_key = nix_escape_string(pubkey)
|
|
78
|
+
|
|
79
|
+
File.write(File.join(path, 'data/ssh-keys.nix'), <<~NIX)
|
|
80
|
+
rec {
|
|
81
|
+
admins = [ #{admin_name} ];
|
|
82
|
+
|
|
83
|
+
#{admin_name} = "#{escaped_key}";
|
|
84
|
+
}
|
|
85
|
+
NIX
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def write_cluster_modules!(path, entries)
|
|
89
|
+
modules = entries.map { |v| " ./#{v}" }.join("\n")
|
|
90
|
+
|
|
91
|
+
File.write(File.join(path, 'cluster/cluster.nix'), <<~NIX)
|
|
92
|
+
# This file is generated by confctl, changes will be lost
|
|
93
|
+
[
|
|
94
|
+
#{modules}
|
|
95
|
+
]
|
|
96
|
+
NIX
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def init_fixture_repo!(path, message: 'fixture: initial')
|
|
100
|
+
run_local!(%w[git init], chdir: path)
|
|
101
|
+
run_local!(%w[git config user.email confctl-tests@example.invalid], chdir: path)
|
|
102
|
+
run_local!(%w[git config user.name confctl-tests], chdir: path)
|
|
103
|
+
run_local!(%w[git add .], chdir: path)
|
|
104
|
+
run_local!(['git', 'commit', '-m', message], chdir: path)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def setup_ssh_home!(path)
|
|
108
|
+
FileUtils.rm_rf(path)
|
|
109
|
+
ssh_dir = File.join(path, '.ssh')
|
|
110
|
+
key_path = File.join(ssh_dir, 'id_ed25519')
|
|
111
|
+
FileUtils.mkdir_p(ssh_dir)
|
|
112
|
+
|
|
113
|
+
run_local!(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', key_path])
|
|
114
|
+
|
|
115
|
+
File.write(File.join(ssh_dir, 'config'), <<~CFG)
|
|
116
|
+
Host *
|
|
117
|
+
StrictHostKeyChecking no
|
|
118
|
+
UserKnownHostsFile /dev/null
|
|
119
|
+
IdentityFile #{key_path}
|
|
120
|
+
IdentitiesOnly yes
|
|
121
|
+
ConnectTimeout 10
|
|
122
|
+
ServerAliveInterval 5
|
|
123
|
+
ServerAliveCountMax 6
|
|
124
|
+
LogLevel ERROR
|
|
125
|
+
CFG
|
|
126
|
+
|
|
127
|
+
File.chmod(0o700, ssh_dir)
|
|
128
|
+
File.chmod(0o600, key_path)
|
|
129
|
+
File.chmod(0o600, "#{key_path}.pub")
|
|
130
|
+
File.chmod(0o600, File.join(ssh_dir, 'config'))
|
|
131
|
+
|
|
132
|
+
File.read("#{key_path}.pub").strip
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def install_pubkey!(machine, pubkey, timeout: 300)
|
|
136
|
+
wait_for_block(name: "install SSH key on #{machine.name}", timeout:) do
|
|
137
|
+
machine.succeeds('mkdir -p /root/.ssh && chmod 700 /root/.ssh')
|
|
138
|
+
machine.succeeds("cat > /root/.ssh/authorized_keys <<'EOF'\n#{pubkey}\nEOF\nchmod 600 /root/.ssh/authorized_keys")
|
|
139
|
+
true
|
|
140
|
+
rescue EOFError, OsVm::MachineShellClosed, OsVm::CommandFailed
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def confctl_env
|
|
146
|
+
ssh_config = File.join(home_dir, '.ssh', 'config')
|
|
147
|
+
ssh_key = File.join(home_dir, '.ssh', 'id_ed25519')
|
|
148
|
+
ssh_opts = [
|
|
149
|
+
"-F #{ssh_config}",
|
|
150
|
+
"-i #{ssh_key}",
|
|
151
|
+
'-o IdentitiesOnly=yes',
|
|
152
|
+
'-o StrictHostKeyChecking=no',
|
|
153
|
+
'-o UserKnownHostsFile=/dev/null',
|
|
154
|
+
'-o LogLevel=ERROR',
|
|
155
|
+
'-o ConnectTimeout=10'
|
|
156
|
+
].join(' ')
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
'HOME' => home_dir,
|
|
160
|
+
'CONFCTL_TTY' => '0',
|
|
161
|
+
'CONFCTL_SSH_CONFIG' => ssh_config,
|
|
162
|
+
'NIX_SSHOPTS' => ssh_opts,
|
|
163
|
+
'NIX_PATH' => [nix_path, ENV.fetch('NIX_PATH', nil)].compact.join(':'),
|
|
164
|
+
'NIX_CONFIG' => [ENV.fetch('NIX_CONFIG', nil), 'sandbox = false'].compact.join("\n"),
|
|
165
|
+
'NO_COLOR' => '1',
|
|
166
|
+
'PAGER' => ''
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def confctl(*args)
|
|
171
|
+
run_local([confctl_bin, *args], chdir: conf_dir, env: confctl_env)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def confctl!(*args)
|
|
175
|
+
run_local!([confctl_bin, *args], chdir: conf_dir, env: confctl_env)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def confctl_ssh(machine, *remote_argv)
|
|
179
|
+
confctl('ssh', '--yes', machine, *remote_argv.map(&:to_s))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def confctl_ssh!(machine, *remote_argv)
|
|
183
|
+
confctl!('ssh', '--yes', machine, *remote_argv.map(&:to_s))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def extract_store_path(text)
|
|
187
|
+
text.to_s.scan(STORE_PATH_RE).last
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def confctl_remote_realpath(machine, path)
|
|
191
|
+
out, = confctl_ssh!(machine, 'realpath', path)
|
|
192
|
+
store_path = extract_store_path(out)
|
|
193
|
+
return store_path unless store_path.nil?
|
|
194
|
+
|
|
195
|
+
raise "unable to parse store path from confctl ssh output for #{machine}:#{path}: #{out.inspect}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def confctl_store_path_exists!(machine, store_path)
|
|
199
|
+
confctl_ssh!(machine, 'test', '-e', store_path)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def confctl_machine_state(machine)
|
|
203
|
+
{
|
|
204
|
+
profile: confctl_remote_realpath(machine, '/nix/var/nix/profiles/system'),
|
|
205
|
+
current: confctl_remote_realpath(machine, '/run/current-system')
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def wait_for_confctl_connectivity!(expected_successes:, timeout: 120)
|
|
210
|
+
deadline = Time.now + timeout
|
|
211
|
+
|
|
212
|
+
loop do
|
|
213
|
+
out, _err, status = confctl('test-connection', '--yes')
|
|
214
|
+
return out if status.success? && out.include?("#{expected_successes} successful")
|
|
215
|
+
|
|
216
|
+
raise "confctl test-connection did not report #{expected_successes} successful hosts in #{timeout}s" if Time.now >= deadline
|
|
217
|
+
|
|
218
|
+
sleep(2)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def confctl_generation_info(host, selector = 'current')
|
|
223
|
+
host_dir = File.join(conf_dir, '.confctl', 'generations', host.tr('/', ':'))
|
|
224
|
+
|
|
225
|
+
generation_name =
|
|
226
|
+
if selector == 'current'
|
|
227
|
+
current_link = File.join(host_dir, 'current')
|
|
228
|
+
File.basename(File.readlink(current_link))
|
|
229
|
+
else
|
|
230
|
+
selector
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
json_path = File.join(host_dir, generation_name, 'generation.json')
|
|
234
|
+
JSON.parse(File.read(json_path)).merge('name' => generation_name)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
TestRunner::TestEvaluator.include(ConfctlTestHelpers)
|