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..e004bd4 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 (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 +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..26c2ece 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 (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. * **: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..643bf1c 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 (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. # @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