Skip to content

Public Task Interface

The companion primetheus.gitlab_settings collection provides four ordinary Ansible actions:

  • primetheus.gitlab_settings.gitlab_settings_discover lists normalized semantic resources.
  • primetheus.gitlab_settings.gitlab_settings reconciles one semantic resource.
  • primetheus.gitlab_settings.gitlab_credential_check verifies a credential once, before the tasks that depend on it.
  • primetheus.gitlab_settings.gitlab_oauth_session maintains a managed OAuth session deliberately.

Credential declaration, verification verdicts, managed OAuth sessions, and every state those two credential tasks can report are documented in Credential operations.

Use these actions when the resource set is discovered at runtime and an ordinary Ansible loop is the clearest composition. Use the standalone role's tasks_from: apply entry point for efficient bulk convergence of static or preassembled desired-state documents.

Install from this source tree

Build and install the supported companion artifact with standard collection tooling:

COLLECTION_ARCHIVE="$(python3 tools/build_public_collection.py --output-dir dist)"
ansible-galaxy collection install "$COLLECTION_ARCHIVE"

The installed FQCNs need no ANSIBLE_LIBRARY, role pre-inclusion, copied plugins, or symlinks. The packaging decision is recorded in ADR-0010.

Discover, register, and loop

Discovery records intentionally carry the exact identifier accepted by the reconciliation action:

- name: Discover all groups
  primetheus.gitlab_settings.gitlab_settings_discover:
    target: "{{ gitlab_settings_target }}"
    resource: group
    scope: all
  register: discovered_groups

- name: Enable Duo for every discovered group
  primetheus.gitlab_settings.gitlab_settings:
    target: "{{ gitlab_settings_target }}"
    resource: group
    identifier: "{{ item['identifier'] }}"
    settings:
      duo:
        duo_features_enabled: true
        duo_availability: default_on
  loop: "{{ discovered_groups['resources'] }}"
  loop_control:
    label: "{{ item['identifier'] }}"

No fact accumulator or result-shaping filter connects the tasks. A looped reconciliation task produces Ansible's normal registered results list. when, failed_when, changed_when, task tags, delegation, check mode, and diff mode retain their ordinary Ansible meanings.

Nested resources

Nested objects take a semantic parent. The module owns identifier encoding and transport paths:

- name: Discover unprotected branches
  primetheus.gitlab_settings.gitlab_settings_discover:
    target: "{{ gitlab_settings_target }}"
    resource: branch
    parent:
      resource: project
      identifier: platform/service
    filters:
      protected: false
  register: discovered_branches

Scalar identities are passed directly. Composite identities use a mapping:

- name: Remove one freeze period
  primetheus.gitlab_settings.gitlab_settings:
    target: "{{ gitlab_settings_target }}"
    resource: freeze_period
    parent:
      resource: project
      identifier: platform/service
    identifier:
      freeze_start: "0 0 * * 5"
      freeze_end: "0 0 * * 1"
    state: absent

One desired-state schema

settings is exactly the equivalent bulk entry after its path identity and lifecycle state are lifted into module arguments:

# Bulk role
gitlab_settings_groups:
  - path: platform
    duo:
      duo_features_enabled: true
      duo_availability: default_on

# Public task
- primetheus.gitlab_settings.gitlab_settings:
    target: "{{ gitlab_settings_target }}"
    resource: group
    identifier: platform
    settings:
      duo:
        duo_features_enabled: true
        duo_availability: default_on

Both forms pass through the same normalizer, planner, policy metadata, secret separation, and transport router.

Discovery behavior

Singular resource names are required. Semantic scopes, filters, parent rules, normalized attributes, and transport capabilities come from the generated registry reference.

Full pagination is the default. limit returns at most that many matching records and sets truncated: true only after the module proves another matching record exists. Discovery is always read-only and reports changed: false, including in check mode.

Successful records contain:

resource: group
identifier: platform
id: 42
name: Platform
parent: null
attributes:
  full_path: platform

The enclosing result always includes resources, count, truncated, pages_read, transport, skipped, skip_reason, notices, credential-free authentication, and the live compatibility decision. When a safe retry occurs it also includes attempts, retries used, the safe retry reason, and any rate-limit wait. Authentication, authorization, malformed input, and unsafe response ambiguity fail; they never become an empty successful discovery. compatible mode turns only an explicitly unavailable registry/transport capability into a structured skip. Failures name the semantic resource and parent while keeping endpoint paths, GraphQL documents, headers, and raw response bodies internal.

Reconciliation behavior

The module reads current state and writes only on observed drift. It reports semantic actions, safe diff, found, resource_id, the chosen transport, credential-free authentication/compatibility state, retry diagnostics when used, and structured compatibility skips. Check mode performs validation and reads, reports the planned change and diff, and suppresses mutation.

Sensitive fields remain in their established semantic locations. Module argument leaves are no_log; planner secrets are held separately from display operations; results and diffs never contain their values.

backend uses the same ordered preference contract as the role: api, all, one transport, or a list. REST and GraphQL are selected internally from resource capabilities. Public per-item Rails execution is intentionally unavailable until a controller action can preserve normal delegation and the role's single-boot Rails safety contract; selecting Rails alone therefore strict-fails or compatible-skips rather than silently using another transport.

Target and authentication

Every invocation takes an explicit target; public tasks do not read ambient role variables. Structured token and OAuth access-token forms build headers internally:

gitlab_settings_target:
  url: https://gitlab.example.com
  auth:
    type: oauth2
    access_token: "{{ vault_gitlab_oauth_access_token }}"

Project and group access tokens additionally require a caller-declared bound_to maximum authority. For example:

gitlab_settings_target:
  url: https://gitlab.example.com
  auth:
    type: group_access_token
    token: "{{ vault_gitlab_group_access_token }}"
    bound_to:
      resource: group
      identifier: platform

The registry records accepted credential types, scope alternatives, request forms, resource-boundary rules, and sudo support for every operation/backend. Each record is built from the concrete (path, method) REST endpoints, or the committed GraphQL operation documents, that the operation actually performs, and is the conjunction over them — so a record is never broader than the narrowest endpoint involved. Known incompatible or out-of-boundary uses fail before an API client builds credential headers, and an unregistered resource fails closed rather than inheriting a general capability.

CI/CD job tokens and deploy tokens are declarable and fully implemented, but are currently refused for every selectable operation: GitLab opts routes in to them individually and the role holds no pinned allowlist for its v19.2.0-ee contract. Refusing is the safe direction, because an unopted route treats such a credential as absent rather than rejecting it. Each refusal names the evidence artifact that would settle the decision.

GitLab requires a credential to read instance metadata, and the registry decides which credentials it accepts. A target whose credential cannot perform that read — currently anonymous — must declare an exact supported target.version, and compatibility is validated from that declaration instead of a live /metadata read. Every other credential discovers version and edition live.

TLS validation defaults on and ca_bundle is local to the module execution host (the controller when the task is delegated there). The public actions discover /metadata internally after semantic validation, require the same committed compatibility profile as the bulk role, and treat target version/edition fields as expected constraints rather than overrides.

OAuth access tokens are supported consistently by REST, GraphQL, discovery, and compatibility reads. Managed refresh is opt-in through auth.refresh.enabled and is performed by each action's associated action plugin on the Ansible controller, so a rotated pair is committed durably before the module runs and is shared by later loop items, later tasks, and later plays. Refresh and application material never leave the controller.

An access token with no refresh configuration still fails with a specific recovery message once it is known to be expired. Discovery and reconciliation never initialize a credential store or rotate a token in check mode; use gitlab_oauth_session with check_mode: false when you intend to permit maintenance during a --check run.

Anonymous and deploy-token targets are accepted by the same contract. Which credential an operation accepts is decided by the shared semantic resource registry before any request is built, so an unsupported combination fails with an authentication message rather than a transport one.

Snapshot discovery is separate

The role's existing tasks_from: discover entry point captures GitLab version and application-setting snapshots and may persist gitlab_settings_snapshot_file. It has not been repurposed as arbitrary resource enumeration. The public gitlab_settings_discover action lists one semantic resource and never writes a snapshot.

Internal actions such as gitlab_settings_rest_resource, gitlab_settings_graphql_resource, and compatibility/planner modules are role implementation details. Public playbooks should call only the FQCNs above or the documented bulk role entry points.