From 93bc2964f06501aceb439440c0424e715532b562 Mon Sep 17 00:00:00 2001 From: jcardozo-eth <131674798+jcardozo-eth@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:04:10 +0200 Subject: [PATCH 1/2] feat(runner_account): support a differently named primary group The runner_account struct gains an optional `group` key naming the account's primary group. It defaults to the account name, so existing hosts render and apply identically; set it for an externally provisioned account whose primary group is named differently (account ci-worker, group ci), where the account name is not itself a resolvable group and the first apply would otherwise fail to find it. The group is derived once (falling back to the name) and threaded into every group ownership the module manages: the managed group and the user's gid where the account is owned here, and the group of the runner configuration file, its directory, the .runner_system_id file, and the account's systemd user tree. Owners stay the account name throughout. The module data layer cannot express "same as name", so the default is an absent key with the fallback resolved in code. --- CHANGELOG.md | 1 + README.md | 18 +++++--- REFERENCE.md | 11 ++++- data/common.yaml | 1 + manifests/config.pp | 13 +++--- manifests/init.pp | 21 ++++++++- manifests/user.pp | 10 +++-- spec/classes/rootless_gitlab_runner_spec.rb | 48 ++++++++++++++++++++- 8 files changed, 105 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf3768..206e532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ module follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- An externally provisioned runner account whose primary group is named differently from the account now converges on the first apply: the new optional `runner_account.group` names the account's primary group and feeds every group ownership the module manages. It defaults to the account name, so existing hosts are unchanged. - A Hiera data check ships with the module: it fails on any data key that no deployed class declares, instead of Hiera silently ignoring it, and flags subkeys set under a disabled `manage` toggle with a non-failing advisory. ### Changed diff --git a/README.md b/README.md index bd30dee..85bffe0 100644 --- a/README.md +++ b/README.md @@ -283,16 +283,24 @@ dependencies; the example skeleton carries the lines). #### `runner_account` -The OS account the runner manager and the rootless daemon run as: `name`, `uid`, and `home`, -with `manage` deciding ownership. The identity keys are read by every concern even when -`manage: false` (socket derivation, file ownership, service `ExecStart`); the toggle only -decides whether the module creates and enforces the group, user, and home. Keep `manage` off -where another configuration-management system owns the account; two owners would fight over +The OS account the runner manager and the rootless daemon run as: `name`, an optional `group`, +`uid`, and `home`, with `manage` deciding ownership. The identity keys are read by every concern +even when `manage: false` (socket derivation, file ownership, service `ExecStart`); the toggle +only decides whether the module creates and enforces the group, user, and home. Keep `manage` +off where another configuration-management system owns the account; two owners would fight over it. The subordinate UID/GID ranges rootless Docker needs are owned by [`rootless_docker`](#rootless_docker), not this toggle. Home internals (`.ssh`, `.config`) are never managed, beyond the no-detach-netns drop-in the module places under `~/.config/systemd/user/`. +`group` names the account's primary group and defaults to `name`. Set it for an externally +provisioned account whose primary group is named differently (account `ci-worker`, group `ci`): +it feeds every group ownership the module manages — the group resource and the user's primary +group where `manage` is on, and the group of the runner configuration file, its directory, and +the account's systemd user tree — so the first apply converges instead of failing to resolve a +group that does not exist. Left unset, the account name doubles as the group, which is correct +by construction where the module creates the account. + #### `rootless_docker` With `rootless_docker.manage` on, the module brings up the rootless-Docker user daemon: diff --git a/REFERENCE.md b/REFERENCE.md index a6a341a..3067e6f 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -175,7 +175,8 @@ Struct[{ ``` The rendered runner configuration file (GitLab Runner's `config.toml`). -Owner and group derive from `runner_account.name`; the mode is fixed 0600 +Owner derives from `runner_account.name` and group from +`runner_account.group` (which defaults to the name); the mode is fixed 0600 (the file carries the runner tokens). Options: @@ -190,6 +191,7 @@ Data type: Struct[{ manage => Boolean, name => Rootless_gitlab_runner::Username, + group => Optional[Rootless_gitlab_runner::Username], uid => Optional[Integer[1]], home => Stdlib::Absolutepath, }] @@ -207,6 +209,13 @@ configuration-management system owns the account. The subordinate UID/GID ranges rootless docker needs are owned by `rootless_docker.manage`. Default false. * **:name** `Rootless_gitlab_runner::Username`: Username of the runner account. Default `gitlab-runner`. +* **:group** `Optional[Rootless_gitlab_runner::Username]`: Name of the account's primary group. Defaults to the account name (the +data layer cannot express that default, so an unset key falls back to +`name` in code). Set it for an externally provisioned account whose primary +group is named differently (account `ci-worker`, group `ci`): it feeds +every group ownership the module manages — the group resource and the +user's primary group where `manage` is on, and the group of the runner +configuration file, its directory, and the account's systemd user tree. * **:uid** `Optional[Integer[1]]`: Numeric uid of the runner account. No default: the uid is host data, not something a module can sensibly invent. It derives the rootless runtime paths (`/run/user/`, the docker socket) and is enforced on the user diff --git a/data/common.yaml b/data/common.yaml index 8d5035a..1a51c41 100644 --- a/data/common.yaml +++ b/data/common.yaml @@ -60,6 +60,7 @@ rootless_gitlab_runner::configuration_file: rootless_gitlab_runner::runner_account: manage: false name: 'gitlab-runner' + group: ~ uid: ~ home: '/home/gitlab-runner' diff --git a/manifests/config.pp b/manifests/config.pp index 6742a70..8029fff 100644 --- a/manifests/config.pp +++ b/manifests/config.pp @@ -4,6 +4,9 @@ assert_private() $runner_name = $rootless_gitlab_runner::runner_account['name'] + # Primary group, defaulting to the account name (derived in init.pp). Owners + # stay the account name; only the group ownerships derive from it. + $runner_group = $rootless_gitlab_runner::runner_group $runner_home = $rootless_gitlab_runner::runner_account['home'] $configuration_file_path = $rootless_gitlab_runner::configuration_file['path'] @@ -87,7 +90,7 @@ file { $config_dir: ensure => directory, owner => 'root', - group => $runner_name, + group => $runner_group, mode => '0770', } @@ -104,7 +107,7 @@ file { "${config_dir}/.runner_system_id": ensure => file, owner => $runner_name, - group => $runner_name, + group => $runner_group, mode => '0600', } } @@ -118,7 +121,7 @@ file { $configuration_file_path: ensure => file, owner => $runner_name, - group => $runner_name, + group => $runner_group, mode => '0600', content => Sensitive(epp('rootless_gitlab_runner/config.toml.epp', { 'concurrent' => $rootless_gitlab_runner::concurrent, @@ -145,7 +148,7 @@ file { [$user_config_dir, $user_systemd_dir, $user_units_dir, $dropin_dir]: ensure => directory, owner => $runner_name, - group => $runner_name, + group => $runner_group, mode => '0755', } @@ -155,7 +158,7 @@ file { $dropin_path: ensure => file, owner => $runner_name, - group => $runner_name, + group => $runner_group, mode => '0644', source => 'puppet:///modules/rootless_gitlab_runner/no-detach-netns.conf', require => File[$dropin_dir], diff --git a/manifests/init.pp b/manifests/init.pp index 09184d4..af4fd39 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -93,7 +93,8 @@ # and never managed. Default `/etc/gitlab-runner-infra`. # @param configuration_file # The rendered runner configuration file (GitLab Runner's `config.toml`). -# Owner and group derive from `runner_account.name`; the mode is fixed 0600 +# Owner derives from `runner_account.name` and group from +# `runner_account.group` (which defaults to the name); the mode is fixed 0600 # (the file carries the runner tokens). # @option configuration_file [Stdlib::Absolutepath] :path # Where the file is written. Default `/etc/gitlab-runner/config.toml`. @@ -109,6 +110,14 @@ # Default false. # @option runner_account [Rootless_gitlab_runner::Username] :name # Username of the runner account. Default `gitlab-runner`. +# @option runner_account [Optional[Rootless_gitlab_runner::Username]] :group +# Name of the account's primary group. Defaults to the account name (the +# data layer cannot express that default, so an unset key falls back to +# `name` in code). Set it for an externally provisioned account whose primary +# group is named differently (account `ci-worker`, group `ci`): it feeds +# every group ownership the module manages — the group resource and the +# user's primary group where `manage` is on, and the group of the runner +# configuration file, its directory, and the account's systemd user tree. # @option runner_account [Optional[Integer[1]]] :uid # Numeric uid of the runner account. No default: the uid is host data, not # something a module can sensibly invent. It derives the rootless runtime @@ -229,6 +238,7 @@ Struct[{ manage => Boolean, name => Rootless_gitlab_runner::Username, + group => Optional[Rootless_gitlab_runner::Username], uid => Optional[Integer[1]], home => Stdlib::Absolutepath, }] $runner_account, @@ -291,6 +301,15 @@ ])) } + # The account's primary group, derived once and read as a group by every + # concern (the group resource and the user's gid in user.pp, and every + # managed file's group in config.pp). The data layer cannot express "same as + # the account name", so the default is an absent key and the fallback lives + # here: an unset group follows the account name — correct by construction + # where the module creates the account — while a set group names the + # differently named primary group of an externally provisioned account. + $runner_group = pick($runner_account['group'], $runner_account['name']) + # Defaults merged under every runner entry; keys set on the entry win. $effective_runners = $runners.map |$r| { $runner_defaults + $r } diff --git a/manifests/user.pp b/manifests/user.pp index b026dad..7aaab10 100644 --- a/manifests/user.pp +++ b/manifests/user.pp @@ -6,9 +6,11 @@ $account = $rootless_gitlab_runner::runner_account if $account['manage'] { - $runner_name = $account['name'] + $runner_name = $account['name'] + # Primary group, defaulting to the account name (derived in init.pp). + $runner_group = $rootless_gitlab_runner::runner_group - group { $runner_name: + group { $runner_group: ensure => present, system => true, } @@ -19,11 +21,11 @@ ensure => present, system => true, uid => $account['uid'], - gid => $runner_name, + gid => $runner_group, home => $account['home'], managehome => true, shell => '/bin/bash', - require => Group[$runner_name], + require => Group[$runner_group], } } } diff --git a/spec/classes/rootless_gitlab_runner_spec.rb b/spec/classes/rootless_gitlab_runner_spec.rb index 26bceb8..17c88a4 100644 --- a/spec/classes/rootless_gitlab_runner_spec.rb +++ b/spec/classes/rootless_gitlab_runner_spec.rb @@ -438,14 +438,15 @@ def struct_param(name, overrides = {}) it { is_expected.to compile.with_all_deps } it { is_expected.to contain_group('gitlab-runner').with('ensure' => 'present', 'system' => true) } - it 'owns the user with the declared uid and home' do + it 'owns the user with the declared uid and home, primary group defaulting to the name' do is_expected.to contain_user('gitlab-runner').with( 'ensure' => 'present', 'system' => true, 'uid' => 4242, + 'gid' => 'gitlab-runner', 'home' => '/home/gitlab-runner', 'managehome' => true, - ) + ).that_requires('Group[gitlab-runner]') end %w[subuid subgid].each do |f| @@ -455,6 +456,23 @@ def struct_param(name, overrides = {}) end end + # runner_account.group names the account's primary group; unset it defaults + # to the account name. With manage on, the module creates that group and sets + # it as the user's primary group instead of a same-named group. + context 'with runner_account.manage and a differently named primary group' do + let(:params) do + { 'runner_account' => struct_param('runner_account', 'manage' => true, 'uid' => 4242, 'group' => 'ci') } + end + + it { is_expected.to compile.with_all_deps } + + it 'creates the named group and sets it as the user primary group, not the account name' do + is_expected.to contain_group('ci').with('ensure' => 'present', 'system' => true) + is_expected.not_to contain_group('gitlab-runner') + is_expected.to contain_user('gitlab-runner').with('gid' => 'ci').that_requires('Group[ci]') + end + end + context 'with rootless_docker.manage' do let(:params) do { 'rootless_docker' => struct_param('rootless_docker', 'manage' => true), @@ -708,6 +726,32 @@ def struct_param(name, overrides = {}) .with_content(%r{^ExecStart=/usr/bin/gitlab-runner run --working-directory /srv/ci-worker --config /etc/gitlab-runner/config\.toml --service gitlab-runner$}) .with_content(%r{^Environment=DOCKER_HOST=unix:///run/user/5000/docker\.sock$}) end + + # An externally provisioned account (manage off) whose primary group is + # named differently. Every managed group ownership must derive from the + # group, while owners stay the account name — otherwise the first apply + # fails to resolve a group that does not exist. + context 'with a differently named primary group' do + let(:params) do + super().merge('runner_account' => struct_param('runner_account', + 'name' => 'ci-worker', 'uid' => 5000, + 'home' => '/srv/ci-worker', 'group' => 'ci')) + end + + it { is_expected.to compile.with_all_deps } + + it 'derives every managed group ownership from the group, owners staying the account name' do + is_expected.to contain_file('/etc/gitlab-runner/config.toml') + .with('owner' => 'ci-worker', 'group' => 'ci') + is_expected.to contain_file('/etc/gitlab-runner').with('group' => 'ci') + is_expected.to contain_file('/etc/gitlab-runner/.runner_system_id') + .with('owner' => 'ci-worker', 'group' => 'ci') + is_expected.to contain_file('/srv/ci-worker/.config/systemd/user') + .with('owner' => 'ci-worker', 'group' => 'ci') + is_expected.to contain_file('/srv/ci-worker/.config/systemd/user/docker.service.d/no-detach-netns.conf') + .with('owner' => 'ci-worker', 'group' => 'ci') + end + end end context 'with standalone.manage alone (no self-update loop)' do From d3edd3e4c4cb9e2047b1b0dee1f7a94e93165b05 Mon Sep 17 00:00:00 2001 From: jcardozo-eth <131674798+jcardozo-eth@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:52:42 +0200 Subject: [PATCH 2/2] docs: mark the runner_account.group example as illustrative The `ci-worker`/`ci` pair in the runner_account.group option doc and the README is an example, not a default or a shipped name. Prefix it with "e.g." so it does not read as a fixed value. --- README.md | 2 +- REFERENCE.md | 2 +- manifests/init.pp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 85bffe0..e004bd4 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ never managed, beyond the no-detach-netns drop-in the module places under `~/.config/systemd/user/`. `group` names the account's primary group and defaults to `name`. Set it for an externally -provisioned account whose primary group is named differently (account `ci-worker`, group `ci`): +provisioned account whose primary group is named differently (e.g. account `ci-worker`, group `ci`): it feeds every group ownership the module manages — the group resource and the user's primary group where `manage` is on, and the group of the runner configuration file, its directory, and the account's systemd user tree — so the first apply converges instead of failing to resolve a diff --git a/REFERENCE.md b/REFERENCE.md index 3067e6f..26c2ece 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -212,7 +212,7 @@ Default false. * **:group** `Optional[Rootless_gitlab_runner::Username]`: Name of the account's primary group. Defaults to the account name (the data layer cannot express that default, so an unset key falls back to `name` in code). Set it for an externally provisioned account whose primary -group is named differently (account `ci-worker`, group `ci`): it feeds +group is named differently (e.g. account `ci-worker`, group `ci`): it feeds every group ownership the module manages — the group resource and the user's primary group where `manage` is on, and the group of the runner configuration file, its directory, and the account's systemd user tree. diff --git a/manifests/init.pp b/manifests/init.pp index af4fd39..643bf1c 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -114,7 +114,7 @@ # Name of the account's primary group. Defaults to the account name (the # data layer cannot express that default, so an unset key falls back to # `name` in code). Set it for an externally provisioned account whose primary -# group is named differently (account `ci-worker`, group `ci`): it feeds +# group is named differently (e.g. account `ci-worker`, group `ci`): it feeds # every group ownership the module manages — the group resource and the # user's primary group where `manage` is on, and the group of the runner # configuration file, its directory, and the account's systemd user tree.