diff --git a/README.md b/README.md index 254fc40..2bc09e3 100644 --- a/README.md +++ b/README.md @@ -327,10 +327,12 @@ by construction where the module creates the account. With `rootless_docker.manage` on, the module brings up the rootless-Docker user daemon: provisions the subordinate UID/GID ranges the daemon needs (`subid_start`/`subid_count`, default `231072`/`165536`) and enforces `subid_count` as a grow-only minimum width. A -module-owned range at the declared start narrower than `subid_count` is widened in place (a -single locked `usermod` del-before-add) and the rootless daemon restarted so its user namespace -picks up the new width; a foreign or already-wider range is never rewritten (a too-narrow -foreign range fails the preflight, a wide-enough one warns). Widening an in-service host +module-owned range at the declared start narrower than `subid_count` is widened in place by +adding the missing tail as a further contiguous range: a pure `usermod --add`, which grows the +range even while the runner user's session is live and leaves the existing mappings as an +untouched prefix. The rootless daemon is then restarted so its user namespace picks up the new +width; a foreign or already-wider range is never rewritten (a too-narrow foreign range fails the +preflight, a wide-enough one warns). Widening an in-service host interrupts running jobs (rootless mode has no live-restore), so it is best scheduled around them; CI-side job `retry` covers a job caught by the restart. It then enables lingering and runs `dockerd-rootless-setuptool.sh install` [\[4\]](#ref-4) as the runner user (guarded on installed state, so it diff --git a/manifests/rootless_docker.pp b/manifests/rootless_docker.pp index 6b3999b..5514a28 100644 --- a/manifests/rootless_docker.pp +++ b/manifests/rootless_docker.pp @@ -57,19 +57,22 @@ # --noop cannot see, and drives every outcome from that one source: # create — no entry yet: write start:count (the greenfield path). Always # declared and grep-guarded, so it is correct even without the fact. - # widen — one entry, at the declared start, narrower than declared: rewrite - # it wider. The old width comes from the fact, so the command is - # fully literal — no shell arithmetic — and the guard re-asserts - # that exact line still exists, so a host changed since fact - # collection is skipped, not mis-edited. shadow 4.8.1 deletes - # before adding in the one locked invocation (never momentarily - # absent), and widening at an unchanged start preserves every - # existing inner->outer mapping (a prefix superset), so container - # and image ownership is untouched. + # widen — a contiguous block anchored at the declared start but narrower + # than declared: add only the missing tail as a further contiguous + # range. The add is pure (no --del), so it succeeds even while the + # runner user has a live session; --del would trip usermod's + # running-process guard on the lingering systemd --user manager and + # fail on exactly the converged hosts that need a widen. The tail + # offset comes from the fact, so the command is literal, and the + # guard re-asserts the covered width still matches, so a host + # changed since fact collection is skipped, not double-added. The + # existing lower ranges are left untouched (a prefix superset) and + # rootlesskit maps every range, so the effective width is their sum + # and container/image ownership is untouched. # advise — everything else is neither created nor widened: a wider-than- - # declared entry, or a foreign/multi-entry range wide enough in - # sum, warns (declared data must mirror the host); a too-narrow - # foreign range fails loud at the preflight below. + # declared entry, or a foreign-start, gapped, or overlapping range + # wide enough in sum, warns (declared data must mirror the host); a + # too-narrow foreign range fails loud at the preflight below. # warning() is a log line, not a resource, so the apply stays change-free and # --noop clean (an exec/notify warning would report a change every apply); # only the exact-match state is silent. Guards test host state and exit 0/1 — @@ -79,8 +82,8 @@ $subid_last = $subid_first + $subid_count - 1 $subid_report = $facts['rootless_gitlab_runner_subids'] $subid_files = { - 'subuid' => { 'add' => '--add-subuids', 'del' => '--del-subuids' }, - 'subgid' => { 'add' => '--add-subgids', 'del' => '--del-subgids' }, + 'subuid' => { 'add' => '--add-subuids' }, + 'subgid' => { 'add' => '--add-subgids' }, } $subid_files.each |$f, $flag| { # This file's entries, from the fact: all owners, and the runner user's own @@ -97,14 +100,36 @@ before => Exec['rootless_gitlab_runner preflight'], } - # widen / advise on the runner user's own entry. - if $ranges.length == 1 and $ranges[0]['start'] == $subid_first { - $have = $ranges[0]['count'] + # widen / advise on the runner user's own entries. The create path and an + # additive widen both leave the runner's ranges as one contiguous block + # anchored at the declared start; detect that shape and its covered width by + # folding the ranges (ascending) from subid_first, breaking on the first + # gap, overlap, or foreign start. + $sorted = $ranges.sort |$a, $b| { $a['start'] - $b['start'] } + $fold = $sorted.reduce([$subid_first, true]) |$acc, $r| { + ($acc[1] and $r['start'] == $acc[0]) ? { + true => [$acc[0] + $r['count'], true], + default => [$acc[0], false], + } + } + $anchored = $ranges.length > 0 and $fold[1] + $have = $fold[0] - $subid_first # covered width, valid when anchored + + if $anchored { if $have < $subid_count { - $old_last = $subid_first + $have - 1 + # Grow-only, additive: add only the missing tail as a contiguous range. + # A pure --add succeeds even while the runner user has a live session; + # a --del would trip usermod's running-process guard on the lingering + # systemd --user manager, so delete-and-re-add fails on exactly the + # converged hosts that need a widen. The existing lower ranges are an + # untouched prefix, and rootlesskit maps every range, so the effective + # width is their sum. onlyif re-asserts the covered width the fact saw, + # so a host changed since fact collection is skipped (not double-added), + # and the widen no-ops once the sum reaches the count. + $tail_first = $subid_first + $have exec { "rootless_gitlab_runner ${f} widen": - command => "usermod ${flag['del']} ${subid_first}-${old_last} ${flag['add']} ${subid_first}-${subid_last} ${runner_user}", - onlyif => "grep -qxF '${runner_user}:${subid_first}:${have}' /etc/${f}", + command => "usermod ${flag['add']} ${tail_first}-${subid_last} ${runner_user}", + onlyif => "awk -F: -v u='${runner_user}' '\$1==u{s+=\$3} END{exit !(s==${have})}' /etc/${f}", path => ['/usr/sbin', '/usr/bin', '/bin'], provider => 'shell', before => Exec['rootless_gitlab_runner preflight'], @@ -118,8 +143,9 @@ ], ' ')) } } elsif $ranges.length > 0 { - # Foreign start, or multiple entries: never rewritten. Wide enough in sum - # warns; too narrow is caught loud by the preflight, not here. + # Not an anchored contiguous block: a foreign start, a gap, or an internal + # overlap. Never rewritten. Wide enough in sum warns; too narrow is caught + # loud by the preflight, not here. $summed = $ranges.reduce(0) |$m, $r| { $m + $r['count'] } if $summed >= $subid_count { warning(join([ diff --git a/spec/classes/rootless_gitlab_runner_spec.rb b/spec/classes/rootless_gitlab_runner_spec.rb index ce54bec..42a4494 100644 --- a/spec/classes/rootless_gitlab_runner_spec.rb +++ b/spec/classes/rootless_gitlab_runner_spec.rb @@ -677,12 +677,11 @@ def module_warnings let(:params) { base_params } let(:facts) { subid_facts([{ 'start' => 231_072, 'count' => 65_536 }]) } - { 'subuid' => ['--del-subuids', '--add-subuids'], - 'subgid' => ['--del-subgids', '--add-subgids'] }.each do |f, (del, add)| - it "widens #{f} in place with a literal usermod, guarded on the exact current line" do + { 'subuid' => '--add-subuids', 'subgid' => '--add-subgids' }.each do |f, add| + it "widens #{f} by adding only the missing tail, guarded on the covered width" do is_expected.to contain_exec("rootless_gitlab_runner #{f} widen").with( - 'command' => "usermod #{del} 231072-296607 #{add} 231072-396607 gitlab-runner", - 'onlyif' => "grep -qxF 'gitlab-runner:231072:65536' /etc/#{f}", + 'command' => "usermod #{add} 296608-396607 gitlab-runner", + 'onlyif' => "awk -F: -v u='gitlab-runner' '$1==u{s+=$3} END{exit !(s==65536)}' /etc/#{f}", 'provider' => 'shell', ).that_comes_before('Exec[rootless_gitlab_runner preflight]') end @@ -777,6 +776,55 @@ def module_warnings ) end end + + context 'an additive-widened range: two contiguous entries summing to the count' do + let(:params) { base_params } + let(:facts) do + subid_facts([{ 'start' => 231_072, 'count' => 65_536 }, + { 'start' => 296_608, 'count' => 100_000 }]) + end + + it 'is satisfied: no widen and no advisory' do + expect(module_warnings).to be_empty + %w[subuid subgid].each do |f| + is_expected.not_to contain_exec("rootless_gitlab_runner #{f} widen") + end + end + end + + context 'an additive-widened range that a raised subid_count outgrows' do + let(:params) do + { 'rootless_docker' => struct_param('rootless_docker', 'manage' => true, 'subid_count' => 262_144), + 'runner_account' => account_with_uid(4242) } + end + let(:facts) do + subid_facts([{ 'start' => 231_072, 'count' => 65_536 }, + { 'start' => 296_608, 'count' => 100_000 }]) + end + + it 'adds a further tail from the covered width to the raised width' do + is_expected.to contain_exec('rootless_gitlab_runner subuid widen') + .with_command('usermod --add-subuids 396608-493215 gitlab-runner') + .with_onlyif("awk -F: -v u='gitlab-runner' '$1==u{s+=$3} END{exit !(s==165536)}' /etc/subuid") + end + end + + context 'a gapped set of entries anchored at the declared start' do + let(:params) { base_params } + let(:facts) do + subid_facts([{ 'start' => 231_072, 'count' => 65_536 }, + { 'start' => 300_000, 'count' => 100_000 }]) + end + + it 'is not treated as contiguous: no widen, and a does-not-mirror advisory' do + expect(module_warnings).to include( + match(%r{/etc/subuid for gitlab-runner does not mirror the declared range 231072:165536}), + ) + %w[subuid subgid].each do |f| + is_expected.not_to contain_exec("rootless_gitlab_runner #{f} widen") + end + end + end end context 'with rootless_docker.manage, a custom declared range and a narrower host range' do @@ -787,10 +835,10 @@ def module_warnings end let(:facts) { subid_facts([{ 'start' => 300_000, 'count' => 70_000 }]) } - it 'computes both inclusive bounds from the fact width and the declared width' do + it 'adds only the missing tail from the fact width to the declared width' do is_expected.to contain_exec('rootless_gitlab_runner subuid widen') - .with_command('usermod --del-subuids 300000-369999 --add-subuids 300000-499999 gitlab-runner') - .with_onlyif("grep -qxF 'gitlab-runner:300000:70000' /etc/subuid") + .with_command('usermod --add-subuids 370000-499999 gitlab-runner') + .with_onlyif("awk -F: -v u='gitlab-runner' '$1==u{s+=$3} END{exit !(s==70000)}' /etc/subuid") end it 'carries the declared non-default count into the summed preflight' do