rootless_gitlab_runner: Declaratively manage a rootless GitLab Runner on a single host.
rootless_gitlab_runner::apt_repos: Adds the apt repositories thepackages.installlist installs from.rootless_gitlab_runner::config: Renders the runner configuration, the no-detach-netns drop-in and the secret-store directory.rootless_gitlab_runner::packages: Installs the packages listed underpackages.install.rootless_gitlab_runner::rootless_docker: Brings up the rootless docker user daemon behind a fail-loud preflight.rootless_gitlab_runner::self_update: Installs the standalone apply script and liveness healthcheck, plus the optional self-update loop.rootless_gitlab_runner::service: Manages the runner system service and its privilege-drop drop-in.rootless_gitlab_runner::user: Owns the runner group, user and home.
rootless_gitlab_runner::toml_escape: Escape a string for embedding inside a TOML basic (double-quoted) string.
Rootless_gitlab_runner::Apt_source: An apt source's location and signing-key endpoint.Rootless_gitlab_runner::Username: A strict POSIX-portable Linux username.
Renders the runner configuration file (config.toml) from a Hiera-driven list
of runners, manages the rootless-docker "no-detach-netns" systemd user
drop-in and the secret-store directory, installs the apt packages listed in
packages.install, and optionally: owns the runner account, brings up the
rootless docker user daemon (with the subordinate ID ranges it needs) behind
a fail-loud preflight, manages the runner service together with its
privilege-drop drop-in, and installs the standalone apply script with the
optional self-update loop. Designed to run standalone via puppet apply
with an isolated confdir/vardir so it never collides with a central Puppet
agent.
Runner tokens are never stored in this module or in version control. They
are looked up at apply time from an off-repository secret store via the
runner_tokens parameter, keyed by each runner's token_key.
Every parameter default lives in the module data layer (data/common.yaml),
and each struct parameter carries a deep-merge lookup rule, so consumer data
holds only deviations: a partial hash is merged over the module defaults on
lookup. The documented consumption pattern is Hiera plus
include rootless_gitlab_runner; a resource-style declaration bypasses
Hiera merging and must pass complete struct hashes.
The manage keys are persistent ownership switches, not one-shot bootstrap
flags: set once per host and left on, so every apply keeps owning and
drift-correcting that concern. false means hands-off, not ensure-off. A
manage key decides whether the module creates and enforces its concern's
resources; most struct keys are consumed only by their own concern (so they
are inert while its manage is false), while runner_account's identity
keys are shared inputs every concern reads.
rootless_gitlab_runner::runners:
- name: 'docker-rootless'
url: 'https://gitlab.example.org/'
executor: 'docker'
image: 'ubuntu:22.04'
token_key: 'runner_a'
rootless_gitlab_runner::runner_account:
manage: true
uid: 2000
rootless_gitlab_runner::packages:
install: ['uidmap', 'dbus-user-session', 'docker-ce', 'docker-ce-cli',
'docker-ce-rootless-extras', 'containerd.io', 'gitlab-runner']
sources:
manage: true
rootless_gitlab_runner::rootless_docker:
manage: true
rootless_gitlab_runner::runner_service:
manage: true
rootless_gitlab_runner::standalone:
manage: trueThe following parameters are available in the rootless_gitlab_runner class:
concurrentrunnersrunner_defaultsrunner_tokenssecret_store_pathconfiguration_filerunner_accountpackagesrootless_dockerrunner_servicestandalone
Data type: Integer[1]
Global concurrent value written to the runner configuration file: the
name is GitLab Runner's own config.toml global key, written unchanged.
Default 1.
Data type: Array[Hash]
Ordered list of runner definitions, rendered as the [[runners]] sections
of config.toml. Each entry is a hash; recognised keys:
name, url, id (Integer), executor, token_key, host, image,
privileged (Boolean), tls_verify (Boolean), socket_mount (Boolean),
volumes (Array[String]), security_opt (Array[String]), environment
(Array[String]), comment (String), helper_image (String),
disable_entrypoint_overwrite (Boolean), oom_kill_disable (Boolean),
disable_cache (Boolean), shm_size (Integer), network_mtu (Integer),
cache (Hash — its presence renders the [runners.cache] tables; optional
key MaxUploadedArchiveSize (Integer, default 0)),
allowed_images (Array[String]), allowed_pull_policies (Array[String]).
Tokens are merged in from runner_tokens[token_key] and must not appear
here. host is a passthrough for GitLab Runner's own [runners.docker]
host setting, pointing one runner's jobs at a specific daemon socket; the
managed runner service's own DOCKER_HOST is derived from the uid and set
independently of it.
Data type: Hash
Hash merged under every runners entry ($runner_defaults + $runner,
keys set on the entry win), so multi-runner data does not repeat url,
image, executor and friends. Recognised keys as in runners.
Data type: Sensitive[Hash[String, String]]
Sensitive map of token_key => runner token, supplied by the
off-repository secret store. Typed Sensitive so the value is redacted in
the catalog and reports; the module ships lookup_options
(convert_to: Sensitive) so a plain-YAML or hiera-eyaml secret store is
wrapped automatically on lookup — consumers write ordinary Hiera data.
Empty by default so a checkout without secrets renders blank tokens. When
non-empty, every runner must carry a token_key that resolves here or the
apply fails (typo/missed-provisioning guard). The rendered configuration
content is Sensitive too, so tokens never reach the compiled catalog,
reports, or --show_diff output.
Data type: Stdlib::Absolutepath
Path of the off-repository secret store directory. The directory itself is
managed (root-owned, 0700); the secret file inside it is host-provisioned
and never managed. Default /etc/gitlab-runner-infra.
Data type:
Struct[{
path => Stdlib::Absolutepath,
}]The rendered runner configuration file (GitLab Runner's config.toml).
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:
- :path
Stdlib::Absolutepath: Where the file is written. Default/etc/gitlab-runner/config.toml.
Data type:
Struct[{
manage => Boolean,
name => Rootless_gitlab_runner::Username,
group => Optional[Rootless_gitlab_runner::Username],
uid => Optional[Integer[1]],
home => Stdlib::Absolutepath,
}]The OS account the runner manager and the rootless docker daemon run as.
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.
Options:
- :manage
Boolean: Whether to manage the runner group, user, and home. Keep off where another configuration-management system owns the account. The subordinate UID/GID ranges rootless docker needs are owned byrootless_docker.manage. Default false. - :name
Rootless_gitlab_runner::Username: Username of the runner account. Defaultgitlab-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 tonamein code). Set it for an externally provisioned account whose primary group is named differently (e.g. accountci-worker, groupci): it feeds every group ownership the module manages — the group resource and the user's primary group wheremanageis 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/<uid>, the docker socket) and is enforced on the user whenrunner_account.manageis on; the apply fails loud when it is unset but needed — withrunner_account.manage,rootless_docker.manage,runner_service.manageorstandalone.self_update.manageon, or to derive the docker socket path for asocket_mountrunner. - :home
Stdlib::Absolutepath: Home directory of the runner account. Default/home/gitlab-runner.
Data type:
Struct[{
install => Array[String[1]],
sources => Struct[{
manage => Boolean,
docker => Rootless_gitlab_runner::Apt_source,
gitlab_runner => Rootless_gitlab_runner::Apt_source,
}],
}]The apt-packages concern: what to install, and the apt sources serving it.
Options:
- :install
Array[String[1]]: Packages to ensure installed. The empty default installs nothing. Install only: the module never removes packages absent from the list and never pins or upgrades. - :sources
Struct[{ manage => Boolean, docker => Apt_source, gitlab_runner => Apt_source }]: The apt sources theinstalllist installs from (Docker's and GitLab Runner's, viapuppetlabs/apt).sources.manage(default false) decides ownership: keep it off where apt sources are owned elsewhere (central configuration management, e.g. Foreman/Katello, or a mirror). Thedockerandgitlab_runnersub-hashes each carrylocationandkey_source— verbatimapt::sourceparameter names — overridden only for a mirror; the defaults point at the vendors' repositories and their rolling signing-key endpoints.
Data type:
Struct[{
manage => Boolean,
subid_start => Integer[1],
subid_count => Integer[65536],
}]The rootless docker runtime: "rootless mode" is Docker's own name for running the daemon unprivileged.
Options:
- :manage
Boolean: Whether to bring up the rootless docker user daemon: provision the subordinate UID/GID ranges and enforcesubid_countas a grow-only minimum width (a module-owned range narrower than declared is widened in place and the rootless daemon restarted; a foreign or already-wider range is never rewritten, and the runner account may be owned elsewhere), enable lingering and rundockerd-rootless-setuptool.sh install(guarded, as the runner user), behind a fail-loud preflight that asserts the prerequisites instead of half-installing. Also stops and masks the rootful systemdocker.service/docker.socketandcontainerd.service, which a freshdocker-ceinstall starts as root, so the only Docker daemon on the host is the unprivileged one. cgroup-v2 controller delegation is deliberately not managed (it would be the module's only system-wide write); without it CPU/IO job limits are silently unenforced — see the README Limitations. Default false. - :subid_start
Integer[1]: First subordinate UID/GID allocated to the runner account (field two of the/etc/subuidentry format, persubid(5)). Default 231072. shadow's own auto-allocation hands out 65536-wide ranges from 100000, so the third auto-allocated user on a host starts at 231072; a range overlapping another user's is warned about at compile time. - :subid_count
Integer[65536]: Number of subordinate UIDs/GIDs allocated (field three of the entry; rootless docker needs at least 65536), enforced as a grow-only minimum width. Default 165536, which also covers BuildKit's rootless image layout.
Data type:
Struct[{
manage => Boolean,
environment => Optional[Array[Pattern[/\A[^\r\n]+\z/]]],
timeout_stop_sec => Optional[Variant[Integer[0], String[1]]],
}]The systemd system service running the runner manager, privilege-dropped to the runner account.
Options:
- :manage
Boolean: Whether to manage the runner system service, its privilege-drop systemd drop-in, and the configuration directory's mode so a privilege-dropped manager can traverse to its configuration file. Default false. - :environment
Optional[Array[Pattern[/\A[^\r\n]+\z/]]]: AdditionalEnvironment=lines (KEY=value) rendered into the service drop-in, alongside the module-ownedDOCKER_HOSTline.DOCKER_HOSTitself is derived fromrunner_account.uidand cannot be set here: aDOCKER_HOSTline fails the compile, and a runner whose jobs need a different daemon uses the per-runnerhostkey. ThePatternenforces the type: each line must be non-empty and single-line, so a value containing a newline is rejected and cannot inject an extra systemd directive into the drop-in. - :timeout_stop_sec
Optional[Variant[Integer[0], String[1]]]:TimeoutStopSec=written into the manager service drop-in — how long systemd waits for a graceful drain before escalating toSIGKILL. Unset by default, so systemd'sDefaultTimeoutStopSec(typically 90s) applies; set it to the longest job a drain should wait for (GitLab's documented example is7200). Accepts a seconds integer or a systemd time span (e.g.2h).
Data type:
Struct[{
manage => Boolean,
control_repository_path => Stdlib::Absolutepath,
control_repository_branch => String[1],
puppet_confdir => Stdlib::Absolutepath,
puppet_vardir => Stdlib::Absolutepath,
puppet_bindir => Stdlib::Absolutepath,
healthcheck_interval => String[1],
self_update => Struct[{
manage => Boolean,
apply_interval => String[1],
apply_timeout => String[1],
}],
}]The standalone topology: a host that applies itself via puppet apply,
as opposed to a fleet host deployed by a Puppet server — with the
optional self-update loop nested inside.
Options:
- :manage
Boolean: Declares the host standalone: installs the apply script (/usr/local/sbin/rootless-gitlab-runner-apply, the single definition of the apply command for timer-driven and manual runs) and the liveness healthcheck (manager service, rootless docker daemon), whether or not the self-update loop is enabled. Default false. - :control_repository_path
Stdlib::Absolutepath: Root-owned checkout of the control repository on the host (the apply and self-update target). The apply's manifest, module directory, Hiera configuration and optionalPuppetfilederive strictly from the documented layout beneath it (puppet/manifests/site.pp,puppet/modules,puppet/hiera.yaml, andPuppetfileat the root — r10k installs it each tick when present). Default/opt/gitlab-runner-infra. - :control_repository_branch
String[1]: Branch the self-update loop follows (protected, signed). Defaultmain. - :puppet_confdir
Stdlib::Absolutepath: Isolated Puppet confdir for the apply — never the central agent's. Default/etc/gitlab-runner-infra/puppet. - :puppet_vardir
Stdlib::Absolutepath: Isolated Puppet vardir for the apply — never the central agent's. Default/var/lib/grunner-puppet. - :puppet_bindir
Stdlib::Absolutepath: Directory holding thepuppet(and typicallyr10k) executables, prepended to the self-update service's PATH so the timer-driven, non-login apply can find them. Default/opt/puppetlabs/bin; the AIO-reuse topology sets/opt/puppetlabs/puppet/bin(gem executables live there, not in the symlink farm). - :healthcheck_interval
String[1]: systemd time span between liveness healthcheck runs. Default15min. - :self_update
Struct[{ manage => Boolean, apply_interval => String[1], apply_timeout => String[1] }]: The self-update loop:manage(default false) installs a oneshot service + timer that fetch the control repository, verify the commit signature, reset to the remote branch, runr10k puppetfile installand re-apply. It also layers the loop-supervision checks (apply-timer state, checkout staleness, bootstrap-gem presence) into the healthcheck script, which itself installs withstandalone.manage. Only valid on a standalone host: enabling it withstandalone.manageoff fails at compile time. Never enable it where a Puppet server or r10k already deploys the host.apply_interval(default5min) is the timer's cadence;apply_timeout(default15min) the apply service'sTimeoutStartSec=(a oneshot unit has no default start timeout).
Type: Puppet Language
Every field the runner config interpolates comes from review-gated Hiera data,
but a stray double-quote, backslash or newline would otherwise produce invalid
TOML — or, with a crafted value, inject an unintended key (a newline in a
comment could open privileged = true). Escaping every interpolated field
closes that gap regardless of the data's provenance, per the TOML basic-string
rules (https://toml.io/en/v1.0.0#string): backslash and double-quote are
backslash-escaped, and the control characters that must not appear literally
(newline, carriage return, tab) become their \n / \r / \t escapes.
Backslash is escaped first so the backslashes introduced by the later substitutions are not doubled again.
Every field the runner config interpolates comes from review-gated Hiera data,
but a stray double-quote, backslash or newline would otherwise produce invalid
TOML — or, with a crafted value, inject an unintended key (a newline in a
comment could open privileged = true). Escaping every interpolated field
closes that gap regardless of the data's provenance, per the TOML basic-string
rules (https://toml.io/en/v1.0.0#string): backslash and double-quote are
backslash-escaped, and the control characters that must not appear literally
(newline, carriage return, tab) become their \n / \r / \t escapes.
Backslash is escaped first so the backslashes introduced by the later substitutions are not doubled again.
Returns: String the escaped string, safe to place between double quotes in TOML
Data type: String
the raw string to escape
location and key_source are verbatim apt::source parameter names: the
repository URL (suite = OS codename) and the URL of its armored signing key,
stored as an apt keyring. Both the Docker and GitLab Runner sources under
packages.sources share this shape, pointing at a vendor repository or a
mirror.
Alias of
Struct[{
location => Stdlib::HTTPUrl,
key_source => Stdlib::HTTPUrl,
}]The runner user is interpolated raw into shell execs (the subuid/subgid
usermod execs and the preflight awk in rootless_docker.pp) and into
systemd directives. Constraining it to the shadow-utils NAME_REGEX shape —
a lowercase or underscore start, then lowercase letters, digits, underscore or
hyphen, up to 32 characters — excludes shell-hostile characters (quotes,
spaces, $, ;, newlines) by construction, so those interpolations cannot be
broken out of. $ (the Samba machine-account suffix) is deliberately excluded
as shell-hostile.
Alias of Pattern[/\A[a-z_][a-z0-9_-]{0,31}\z/]