diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 8ede742..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "ansible-dev-container-codespaces", - "image": "ghcr.io/ansible/community-ansible-dev-tools:latest", - "containerUser": "root", - "runArgs": [ - "--security-opt", - "seccomp=unconfined", - "--security-opt", - "label=disable", - "--cap-add=SYS_ADMIN", - "--cap-add=SYS_RESOURCE", - "--device", - "/dev/fuse", - "--security-opt", - "apparmor=unconfined", - "--hostname=ansible-dev-container" - ], - "updateRemoteUserUID": true, - "customizations": { - "vscode": { - "extensions": ["redhat.ansible","redhat.vscode-redhat-account"] - } - } -} diff --git a/.devcontainer/docker/devcontainer.json b/.devcontainer/docker/devcontainer.json deleted file mode 100644 index 391f02d..0000000 --- a/.devcontainer/docker/devcontainer.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "ansible-dev-container-docker", - "image": "ghcr.io/ansible/community-ansible-dev-tools:latest", - "containerUser": "root", - "runArgs": [ - "--security-opt", - "seccomp=unconfined", - "--security-opt", - "label=disable", - "--cap-add=SYS_ADMIN", - "--cap-add=SYS_RESOURCE", - "--device", - "/dev/fuse", - "--security-opt", - "apparmor=unconfined", - "--hostname=ansible-dev-container" - ], - "updateRemoteUserUID": true, - "customizations": { - "vscode": { - "extensions": ["redhat.ansible","redhat.vscode-redhat-account"] - } - } -} diff --git a/.devcontainer/podman/devcontainer.json b/.devcontainer/podman/devcontainer.json deleted file mode 100644 index e52a86b..0000000 --- a/.devcontainer/podman/devcontainer.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "ansible-dev-container-podman", - "image": "ghcr.io/ansible/community-ansible-dev-tools:latest", - "containerUser": "root", - "containerEnv": { - "REGISTRY_AUTH_FILE": "/container-auth.json" - }, - "runArgs": [ - "--cap-add=CAP_MKNOD", - "--cap-add=NET_ADMIN", - "--cap-add=SYS_ADMIN", - "--cap-add=SYS_RESOURCE", - "--device", - "/dev/fuse", - "--security-opt", - "seccomp=unconfined", - "--security-opt", - "label=disable", - "--security-opt", - "apparmor=unconfined", - "--security-opt", - "unmask=/sys/fs/cgroup", - "--userns=host", - "--hostname=ansible-dev-container", - "--env-file", - ".env" - ], - "customizations": { - "vscode": { - "extensions": ["redhat.ansible","redhat.vscode-redhat-account"] - } - }, - "mounts": [ - "source=${localEnv:XDG_RUNTIME_DIR}/containers/auth.json,target=/container-auth.json,type=bind,consistency=cached", - "source=${localEnv:HOME}/Dev/inventories/toallab-inventory,target=/workspaces/inventory,type=bind,consistency=cached", - "source=${localEnv:HOME}/Dev/ansible_collections/,target=/workspaces/collections/,type=bind,consistency=cached", - ] -} diff --git a/playbooks/bootstrap.yml b/playbooks/bootstrap.yml index 05163d4..5cfffd3 100644 --- a/playbooks/bootstrap.yml +++ b/playbooks/bootstrap.yml @@ -2,7 +2,7 @@ - name: Set up IPA Client hosts: tag_ipa_client - become: yes + become: true collections: - freeipa.ansible_freeipa pre_tasks: @@ -27,7 +27,7 @@ - name: Packages hosts: all - become: yes + become: true tasks: - name: Host Packages diff --git a/playbooks/deploy_automation_orchestrator.yml b/playbooks/deploy_automation_orchestrator.yml new file mode 100644 index 0000000..0a84c89 --- /dev/null +++ b/playbooks/deploy_automation_orchestrator.yml @@ -0,0 +1,84 @@ +--- +# Deploy Automation Orchestrator on OpenShift via its OLM operator, backed by a +# CloudNativePG-provisioned PostgreSQL cluster. +# +# WARNING: CloudNativePG here is a single-instance, operator-provisioned PostgreSQL +# cluster intended for demo/dev use — not production. For production, disable the +# cloudnativepg_operator role below and point automation_orchestrator_operator's +# postgres_* variables at a customer-managed PostgreSQL instance instead. +# +# Authenticates via the same aap-deployer ServiceAccount token used by deploy_aap.yml +# (same cluster). The token is stored in 1Password as vault_aap_deployer_token. +# +# Modeled on https://github.com/ansible/aapctl (aap-plus install path). +# +# Prerequisites: +# - OpenShift cluster deployed (deploy_openshift.yml) +# - aap-deployer ServiceAccount provisioned (re-run after RBAC changes): +# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts +# - CloudNativePG uses Manual install-plan approval; the role approves its +# InstallPlan. Automation Orchestrator installs from redhat-operators and +# adopts an existing operator install when its namespace is set in inventory. +# - Pre-release AO builds additionally need a custom CatalogSource and an +# ImageDigestMirrorSet (see automation_orchestrator_operator defaults). +# +# Keycloak OIDC (--tags ao_configure_oidc, run after the install): +# - Keycloak settings come from the openshift host (keycloak_url, keycloak_realm, +# keycloak_admin_user, vault_keycloak_admin_password). +# - automation_orchestrator_oidc_group_mappings in host_vars for the aap host. +# - The client secret is generated by Keycloak and read back each run (not vaulted). +# +# Usage: +# ansible-navigator run playbooks/deploy_automation_orchestrator.yml --mode stdout +# ansible-navigator run playbooks/deploy_automation_orchestrator.yml --mode stdout --tags ao_configure_oidc + +- name: Install Automation Orchestrator + hosts: aap + gather_facts: false + connection: local + + environment: + K8S_AUTH_HOST: "{{ aap_k8s_api_url }}" + K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}" + + pre_tasks: + - name: Verify aap-deployer token is available + ansible.builtin.assert: + that: + - vault_aap_deployer_token is defined + - vault_aap_deployer_token | length > 0 + fail_msg: >- + vault_aap_deployer_token is not set. Provision the ServiceAccount with: + ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts + Then save the displayed token to 1Password as vault_aap_deployer_token. + + roles: + - role: cloudnativepg_operator + - role: automation_orchestrator_operator + +# --------------------------------------------------------------------------- +# Configure Keycloak OIDC login (optional, run after the install) +# --------------------------------------------------------------------------- +- name: Configure Keycloak OIDC for Automation Orchestrator + hosts: aap + gather_facts: false + connection: local + + tags: + - never + - ao_configure_oidc + + vars: + __ao_keycloak_host: "{{ hostvars[groups['openshift'][0]] }}" + automation_orchestrator_oidc_keycloak_url: "{{ __ao_keycloak_host.keycloak_url }}{{ __ao_keycloak_host.keycloak_context | default('') }}" + automation_orchestrator_oidc_keycloak_realm: "{{ __ao_keycloak_host.keycloak_realm }}" + automation_orchestrator_oidc_keycloak_admin_user: "{{ __ao_keycloak_host.keycloak_admin_user }}" + automation_orchestrator_oidc_keycloak_admin_password: "{{ __ao_keycloak_host.vault_keycloak_admin_password }}" + automation_orchestrator_oidc_keycloak_validate_certs: "{{ __ao_keycloak_host.keycloak_validate_certs | default(true) }}" + + environment: + K8S_AUTH_HOST: "{{ aap_k8s_api_url }}" + K8S_AUTH_API_KEY: "{{ vault_aap_deployer_token }}" + + roles: + - role: automation_orchestrator_oidc diff --git a/playbooks/deploy_openshift.yml b/playbooks/deploy_openshift.yml index 33ffd38..edadde2 100644 --- a/playbooks/deploy_openshift.yml +++ b/playbooks/deploy_openshift.yml @@ -396,18 +396,40 @@ - apiGroups: [""] resources: ["serviceaccounts"] verbs: ["get", "list", "watch"] + # patch: Automation Orchestrator backend env (APP_OIDC_ALLOW_PRIVATE_NETWORKS) - apiGroups: ["apps"] resources: ["deployments"] + verbs: ["get", "list", "watch", "patch"] + - apiGroups: ["route.openshift.io"] + resources: ["routes"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["endpoints"] verbs: ["get", "list", "watch"] - apiGroups: ["operators.coreos.com"] - resources: ["operatorgroups", "subscriptions", "clusterserviceversions"] + resources: ["operatorgroups", "subscriptions", "clusterserviceversions", + "catalogsources"] verbs: ["get", "list", "create", "patch", "watch"] + - apiGroups: ["operators.coreos.com"] + resources: ["installplans"] + verbs: ["get", "list", "patch", "watch"] - apiGroups: ["apiextensions.k8s.io"] resources: ["customresourcedefinitions"] verbs: ["get", "list", "watch"] - apiGroups: ["aap.ansible.com"] - resources: ["ansibleautomationplatforms"] + resources: ["ansibleautomationplatforms", "automationorchestrators"] verbs: ["get", "list", "create", "patch", "watch"] + # CloudNativePG Cluster/Database CRs backing Automation Orchestrator + - apiGroups: ["postgresql.cnpg.io"] + resources: ["clusters", "databases"] + verbs: ["get", "list", "create", "patch", "watch"] + # Automation Orchestrator pre-release image mirror (IDMS) + MCP rollout wait + - apiGroups: ["config.openshift.io"] + resources: ["imagedigestmirrorsets"] + verbs: ["get", "list", "create", "patch"] + - apiGroups: ["machineconfiguration.openshift.io"] + resources: ["machineconfigpools"] + verbs: ["get", "list", "watch"] - name: Store aap-deployer token in Vault community.hashi_vault.vault_kv2_write: diff --git a/roles/automation_orchestrator_oidc/defaults/main.yml b/roles/automation_orchestrator_oidc/defaults/main.yml new file mode 100644 index 0000000..6f17575 --- /dev/null +++ b/roles/automation_orchestrator_oidc/defaults/main.yml @@ -0,0 +1,38 @@ +--- +# --- Automation Orchestrator instance --- +# Defaults track the automation_orchestrator_operator role's variables when set. +automation_orchestrator_oidc_namespace: "{{ automation_orchestrator_operator_app_namespace | default('automation-orchestrator') }}" +automation_orchestrator_oidc_instance_name: "{{ automation_orchestrator_operator_instance_name | default('automation-orchestrator') }}" +# Public base URL of the AO UI/API; "" derives it from the instance's Route. +automation_orchestrator_oidc_url: "" +automation_orchestrator_oidc_admin_user: admin +automation_orchestrator_oidc_validate_certs: true + +# --- Keycloak --- +automation_orchestrator_oidc_keycloak_url: "" +automation_orchestrator_oidc_keycloak_realm: "" +automation_orchestrator_oidc_keycloak_admin_user: "" +automation_orchestrator_oidc_keycloak_admin_password: "" +automation_orchestrator_oidc_keycloak_validate_certs: true +automation_orchestrator_oidc_client_id: automation-orchestrator +# Create the Keycloak groups named in group_mappings (membership is managed in Keycloak). +automation_orchestrator_oidc_create_keycloak_groups: true + +# --- AO identity provider --- +automation_orchestrator_oidc_provider_name: Keycloak +# Keycloak group (from the "groups" claim) -> AO built-in group (admins, auditors, users). +# Users in no mapped group are refused login. +automation_orchestrator_oidc_group_mappings: [] +# - idp_group: ao-admins +# group: admins +automation_orchestrator_oidc_rp_initiated_logout: true +# PATCH the provider even when its config is unchanged (e.g. to push a rotated client secret). +automation_orchestrator_oidc_force_update: false + +# The backend rejects OIDC issuers that resolve to private IPs unless this is set, and +# the AutomationOrchestrator CR has no field for it — so the role sets the env var on the +# backend Deployment directly. Re-run the role if the operator ever reverts it. +automation_orchestrator_oidc_allow_private_networks: true + +# --- Debugging --- +automation_orchestrator_oidc_no_log: true diff --git a/roles/automation_orchestrator_oidc/meta/argument_specs.yml b/roles/automation_orchestrator_oidc/meta/argument_specs.yml new file mode 100644 index 0000000..b746b72 --- /dev/null +++ b/roles/automation_orchestrator_oidc/meta/argument_specs.yml @@ -0,0 +1,99 @@ +--- +argument_specs: + main: + short_description: Configure Keycloak OIDC login for Automation Orchestrator + description: + - Creates a confidential OIDC client (with a "groups" claim mapper) and the mapped groups + in Keycloak, then registers Keycloak as an identity provider in Automation Orchestrator + via its REST API, mapping Keycloak groups to AO built-in groups. + - The client secret is read back from Keycloak on each run, so it is not stored elsewhere. + - >- + Sets APP_OIDC_ALLOW_PRIVATE_NETWORKS on the backend Deployment, since the + AutomationOrchestrator CR has no field for it and the backend otherwise rejects + issuers resolving to private IPs. + options: + automation_orchestrator_oidc_namespace: + description: Namespace of the AutomationOrchestrator instance. + type: str + default: automation-orchestrator + automation_orchestrator_oidc_instance_name: + description: Name of the AutomationOrchestrator CR (prefix of its Route, Deployments and Secrets). + type: str + default: automation-orchestrator + automation_orchestrator_oidc_url: + description: Public base URL of Automation Orchestrator. Empty derives it from the instance Route. + type: str + default: "" + automation_orchestrator_oidc_admin_user: + description: Local AO admin used to call the API (password read from the initial-admin-password Secret). + type: str + default: admin + automation_orchestrator_oidc_validate_certs: + description: Verify TLS when calling the AO API. + type: bool + default: true + automation_orchestrator_oidc_keycloak_url: + description: Keycloak base URL (e.g. https://auth.example.com). + type: str + required: true + automation_orchestrator_oidc_keycloak_realm: + description: Keycloak realm holding the client and groups. + type: str + required: true + automation_orchestrator_oidc_keycloak_admin_user: + description: Keycloak master-realm admin username. + type: str + required: true + automation_orchestrator_oidc_keycloak_admin_password: + description: Keycloak master-realm admin password. + type: str + required: true + no_log: true + automation_orchestrator_oidc_keycloak_validate_certs: + description: Verify TLS when calling the Keycloak admin API. + type: bool + default: true + automation_orchestrator_oidc_client_id: + description: Keycloak client ID for Automation Orchestrator. + type: str + default: automation-orchestrator + automation_orchestrator_oidc_create_keycloak_groups: + description: Create the Keycloak groups named in group_mappings. Group membership is managed in Keycloak. + type: bool + default: true + automation_orchestrator_oidc_provider_name: + description: Name of the identity provider in Automation Orchestrator (shown on the login page). + type: str + default: Keycloak + automation_orchestrator_oidc_group_mappings: + description: >- + Keycloak group to AO built-in group mappings. Users in no mapped group are refused login. + type: list + elements: dict + required: true + options: + idp_group: + description: Keycloak group name, as emitted in the "groups" claim. + type: str + required: true + group: + description: AO built-in group. + type: str + required: true + choices: [admins, auditors, users] + automation_orchestrator_oidc_rp_initiated_logout: + description: Log users out of Keycloak when they log out of Automation Orchestrator. + type: bool + default: true + automation_orchestrator_oidc_force_update: + description: PATCH the identity provider even when unchanged (e.g. to push a rotated client secret). + type: bool + default: false + automation_orchestrator_oidc_allow_private_networks: + description: Set APP_OIDC_ALLOW_PRIVATE_NETWORKS=true on the backend Deployment. + type: bool + default: true + automation_orchestrator_oidc_no_log: + description: Set to false to expose secret values in task output. Never set false in shared environments. + type: bool + default: true diff --git a/roles/automation_orchestrator_oidc/meta/main.yml b/roles/automation_orchestrator_oidc/meta/main.yml new file mode 100644 index 0000000..0ecd26f --- /dev/null +++ b/roles/automation_orchestrator_oidc/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + author: ptoal + description: Configure Keycloak OIDC login for Automation Orchestrator + license: MIT + min_ansible_version: "2.16" + platforms: + - name: GenericLinux + versions: + - all + galaxy_tags: + - openshift + - aap + - keycloak + - oidc + +dependencies: [] diff --git a/roles/automation_orchestrator_oidc/tasks/main.yml b/roles/automation_orchestrator_oidc/tasks/main.yml new file mode 100644 index 0000000..9411ba9 --- /dev/null +++ b/roles/automation_orchestrator_oidc/tasks/main.yml @@ -0,0 +1,286 @@ +--- +# Configure Keycloak OIDC login for Automation Orchestrator. +# +# 1. Keycloak: mapped groups + confidential client with a "groups" claim mapper; +# the client secret is read back rather than stored elsewhere. +# 2. Backend Deployment: APP_OIDC_ALLOW_PRIVATE_NETWORKS (no CR field exists for it). +# 3. AO REST API: create/update the identity provider with group mappings. +# +# AO identity providers are runtime configuration in the AO database — the +# AutomationOrchestrator CR has no auth fields. + +- name: Validate group mappings + ansible.builtin.assert: + that: + - automation_orchestrator_oidc_group_mappings | length > 0 + - >- + automation_orchestrator_oidc_group_mappings | map(attribute='group') + | difference(__automation_orchestrator_oidc_valid_groups) | length == 0 + fail_msg: >- + automation_orchestrator_oidc_group_mappings must map at least one Keycloak group to an + AO built-in group ({{ __automation_orchestrator_oidc_valid_groups | join(', ') }}); + otherwise no Keycloak user can log in. + +# ------------------------------------------------------------------ +# Step 1: Resolve the Automation Orchestrator public URL +# ------------------------------------------------------------------ +- name: Read Automation Orchestrator Route + kubernetes.core.k8s_info: + api_version: route.openshift.io/v1 + kind: Route + namespace: "{{ automation_orchestrator_oidc_namespace }}" + name: "{{ automation_orchestrator_oidc_instance_name }}" + register: __automation_orchestrator_oidc_route + when: automation_orchestrator_oidc_url | length == 0 + failed_when: __automation_orchestrator_oidc_route.resources | length == 0 + +- name: Set Automation Orchestrator URL + ansible.builtin.set_fact: + __automation_orchestrator_oidc_url: >- + {{ (automation_orchestrator_oidc_url + if automation_orchestrator_oidc_url | length > 0 + else 'https://' + __automation_orchestrator_oidc_route.resources[0].spec.host) + | regex_replace('/$', '') }} + +# ------------------------------------------------------------------ +# Step 2: Keycloak client and groups +# ------------------------------------------------------------------ +- name: Configure Keycloak + module_defaults: + community.general.keycloak_group: &keycloak_auth + auth_client_id: admin-cli + auth_keycloak_url: "{{ automation_orchestrator_oidc_keycloak_url }}" + auth_realm: master + auth_username: "{{ automation_orchestrator_oidc_keycloak_admin_user }}" + auth_password: "{{ automation_orchestrator_oidc_keycloak_admin_password }}" + validate_certs: "{{ automation_orchestrator_oidc_keycloak_validate_certs }}" + community.general.keycloak_clientsecret_info: *keycloak_auth + middleware_automation.keycloak.keycloak_client: *keycloak_auth + block: + - name: Create mapped Keycloak groups + community.general.keycloak_group: + realm: "{{ automation_orchestrator_oidc_keycloak_realm }}" + name: "{{ item }}" + state: present + loop: "{{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group') | unique }}" + when: automation_orchestrator_oidc_create_keycloak_groups | bool + + # No secret is passed: Keycloak generates one on create and keeps it on update. + - name: Create Automation Orchestrator OIDC client in Keycloak + middleware_automation.keycloak.keycloak_client: + realm: "{{ automation_orchestrator_oidc_keycloak_realm }}" + client_id: "{{ automation_orchestrator_oidc_client_id }}" + name: Automation Orchestrator + description: OIDC client for Automation Orchestrator at {{ __automation_orchestrator_oidc_url }} + enabled: true + protocol: openid-connect + public_client: false + standard_flow_enabled: true + implicit_flow_enabled: false + direct_access_grants_enabled: false + service_accounts_enabled: false + root_url: "{{ __automation_orchestrator_oidc_url }}" + redirect_uris: + - "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback" + web_origins: + - "+" + attributes: + post.logout.redirect.uris: "{{ __automation_orchestrator_oidc_url }}##{{ __automation_orchestrator_oidc_url }}/*" + protocol_mappers: + - name: groups + protocol: openid-connect + protocolMapper: oidc-group-membership-mapper + config: + full.path: "false" + id.token.claim: "true" + access.token.claim: "true" + userinfo.token.claim: "true" + claim.name: groups + state: present + no_log: "{{ automation_orchestrator_oidc_no_log }}" + + - name: Read Automation Orchestrator client secret from Keycloak + community.general.keycloak_clientsecret_info: + realm: "{{ automation_orchestrator_oidc_keycloak_realm }}" + client_id: "{{ automation_orchestrator_oidc_client_id }}" + register: __automation_orchestrator_oidc_clientsecret + no_log: "{{ automation_orchestrator_oidc_no_log }}" + +# ------------------------------------------------------------------ +# Step 3: Allow private-network issuers on the backend +# ------------------------------------------------------------------ +- name: Set APP_OIDC_ALLOW_PRIVATE_NETWORKS on backend Deployment + kubernetes.core.k8s: + state: patched + merge_type: strategic-merge + api_version: apps/v1 + kind: Deployment + namespace: "{{ automation_orchestrator_oidc_namespace }}" + name: "{{ automation_orchestrator_oidc_instance_name }}-backend" + definition: + spec: + template: + spec: + containers: + - name: backend + env: + - name: APP_OIDC_ALLOW_PRIVATE_NETWORKS + value: "{{ automation_orchestrator_oidc_allow_private_networks | string | lower }}" + register: __automation_orchestrator_oidc_backend_patch + +- name: Wait for backend rollout + kubernetes.core.k8s_info: + api_version: apps/v1 + kind: Deployment + namespace: "{{ automation_orchestrator_oidc_namespace }}" + name: "{{ automation_orchestrator_oidc_instance_name }}-backend" + register: __automation_orchestrator_oidc_backend + until: >- + __automation_orchestrator_oidc_backend.resources | length > 0 and + (__automation_orchestrator_oidc_backend.resources[0].status.observedGeneration | default(0)) >= + __automation_orchestrator_oidc_backend.resources[0].metadata.generation and + (__automation_orchestrator_oidc_backend.resources[0].status.updatedReplicas | default(0)) == + __automation_orchestrator_oidc_backend.resources[0].spec.replicas and + (__automation_orchestrator_oidc_backend.resources[0].status.availableReplicas | default(0)) == + __automation_orchestrator_oidc_backend.resources[0].spec.replicas + retries: 30 + delay: 10 + when: __automation_orchestrator_oidc_backend_patch is changed + +# ------------------------------------------------------------------ +# Step 4: Register Keycloak as an AO identity provider +# ------------------------------------------------------------------ +- name: Read AO initial admin password + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + namespace: "{{ automation_orchestrator_oidc_namespace }}" + name: "{{ automation_orchestrator_oidc_instance_name }}-initial-admin-password" + register: __automation_orchestrator_oidc_admin_secret + no_log: "{{ automation_orchestrator_oidc_no_log }}" + +- name: Log in to the AO API + ansible.builtin.uri: + url: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/login" + method: POST + body_format: json + body: + username: "{{ automation_orchestrator_oidc_admin_user }}" + password: "{{ __automation_orchestrator_oidc_admin_secret.resources[0].data.password | b64decode }}" + validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}" + register: __automation_orchestrator_oidc_login + no_log: "{{ automation_orchestrator_oidc_no_log }}" + +- name: Call the AO API + module_defaults: + ansible.builtin.uri: + headers: + Authorization: "Bearer {{ __automation_orchestrator_oidc_login.json.access_token }}" + validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}" + block: + - name: List AO groups + ansible.builtin.uri: + url: "{{ __automation_orchestrator_oidc_url }}/api/v1/groups?limit=100" + register: __automation_orchestrator_oidc_groups + + - name: List AO identity providers + ansible.builtin.uri: + url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers?limit=100" + register: __automation_orchestrator_oidc_idps + + - name: Build desired identity provider configuration + ansible.builtin.set_fact: + __automation_orchestrator_oidc_existing: >- + {{ __automation_orchestrator_oidc_idps.json.resources + | selectattr('name', '==', automation_orchestrator_oidc_provider_name) | first | default({}) }} + __automation_orchestrator_oidc_config: + provider_type: oidc + idp_type: custom + auto_discovery: true + issuer_url: "{{ __automation_orchestrator_oidc_issuer }}" + client_id: "{{ automation_orchestrator_oidc_client_id }}" + redirect_uri: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback" + scopes: openid profile email + group_jmespath_expression: groups + allow_all_authenticated: false + enable_rp_initiated_logout: "{{ automation_orchestrator_oidc_rp_initiated_logout | bool }}" + __automation_orchestrator_oidc_group_ids: >- + {{ __automation_orchestrator_oidc_groups.json.resources | items2dict(key_name='name', value_name='id') }} + __automation_orchestrator_oidc_entries: [] + + - name: Build group mapping entries + ansible.builtin.set_fact: + __automation_orchestrator_oidc_entries: >- + {{ __automation_orchestrator_oidc_entries + [{ + 'idp_group_value': item.idp_group, + 'mapped_group_id': __automation_orchestrator_oidc_group_ids[item.group]}] }} + loop: "{{ automation_orchestrator_oidc_group_mappings }}" + loop_control: + label: "{{ item.idp_group }} -> {{ item.group }}" + + # Compare everything except client_secret (never returned by the API). + - name: Determine whether the identity provider needs updating + ansible.builtin.set_fact: + __automation_orchestrator_oidc_needs_update: >- + {{ automation_orchestrator_oidc_force_update | bool + or not (__automation_orchestrator_oidc_existing.enabled | default(false)) + or (__automation_orchestrator_oidc_existing.configuration | default({})) + != ((__automation_orchestrator_oidc_existing.configuration | default({})) + | combine(__automation_orchestrator_oidc_config)) + or ((__automation_orchestrator_oidc_existing.configuration.group_mapping_entries | default([]) + | map(attribute='idp_group_value') | zip(__automation_orchestrator_oidc_existing.configuration.group_mapping_entries + | default([]) | map(attribute='mapped_group_id')) | map('join', '=') | sort) + != (__automation_orchestrator_oidc_entries | map(attribute='idp_group_value') + | zip(__automation_orchestrator_oidc_entries | map(attribute='mapped_group_id')) + | map('join', '=') | sort)) }} + when: __automation_orchestrator_oidc_existing | length > 0 + + - name: Create Keycloak identity provider in AO + ansible.builtin.uri: + url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers" + method: POST + body_format: json + body: + name: "{{ automation_orchestrator_oidc_provider_name }}" + description: Keycloak realm {{ automation_orchestrator_oidc_keycloak_realm }} + configuration: >- + {{ __automation_orchestrator_oidc_config | combine({ + 'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value, + 'group_mapping_entries': __automation_orchestrator_oidc_entries}) }} + status_code: 201 + changed_when: true + when: __automation_orchestrator_oidc_existing | length == 0 + no_log: "{{ automation_orchestrator_oidc_no_log }}" + + - name: Update Keycloak identity provider in AO + ansible.builtin.uri: + url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers/{{ __automation_orchestrator_oidc_existing.id }}" + method: PATCH + body_format: json + body: + enabled: true + configuration: >- + {{ __automation_orchestrator_oidc_config | combine({ + 'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value, + 'group_mapping_entries': __automation_orchestrator_oidc_entries}) }} + changed_when: true + when: + - __automation_orchestrator_oidc_existing | length > 0 + - __automation_orchestrator_oidc_needs_update | bool + no_log: "{{ automation_orchestrator_oidc_no_log }}" + +# ------------------------------------------------------------------ +# Step 5: Summary +# ------------------------------------------------------------------ +- name: Display Automation Orchestrator OIDC summary + ansible.builtin.debug: + msg: + - "Keycloak OIDC configured for Automation Orchestrator:" + - " Login URL : {{ __automation_orchestrator_oidc_url }}" + - " Issuer : {{ __automation_orchestrator_oidc_issuer }}" + - " Client : {{ automation_orchestrator_oidc_client_id }}" + - " Mappings : {{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group') + | zip(automation_orchestrator_oidc_group_mappings | map(attribute='group')) + | map('join', ' -> ') | join(', ') }}" + - "" + - "Add users to the mapped Keycloak groups to grant access." diff --git a/roles/automation_orchestrator_oidc/vars/main.yml b/roles/automation_orchestrator_oidc/vars/main.yml new file mode 100644 index 0000000..cc2e108 --- /dev/null +++ b/roles/automation_orchestrator_oidc/vars/main.yml @@ -0,0 +1,8 @@ +--- +# Computed internal variables - do not override +__automation_orchestrator_oidc_valid_groups: + - admins + - auditors + - users +__automation_orchestrator_oidc_issuer: >- + {{ automation_orchestrator_oidc_keycloak_url | regex_replace('/$', '') }}/realms/{{ automation_orchestrator_oidc_keycloak_realm }} diff --git a/roles/automation_orchestrator_operator/defaults/main.yml b/roles/automation_orchestrator_operator/defaults/main.yml new file mode 100644 index 0000000..0d8fb5e --- /dev/null +++ b/roles/automation_orchestrator_operator/defaults/main.yml @@ -0,0 +1,81 @@ +--- +# --- OLM subscription --- +# Automation Orchestrator is published in redhat-operators. To install a pre-release +# build instead (as aapctl's scripts/create-catalogsources.sh does), set: +# automation_orchestrator_operator_catalog_source_name: cs-automation-orchestrator +# automation_orchestrator_operator_catalog_image: quay.io/aap/ansible-automation-platform/automation-orchestrator-operator-index@sha256:... +# plus automation_orchestrator_operator_image_digest_mirrors below. +# +# To adopt an existing install (e.g. from OperatorHub), point operator_namespace at +# it: an existing OperatorGroup in that namespace is reused, not duplicated, and an +# already-installed CSV skips InstallPlan approval. +automation_orchestrator_operator_operator_namespace: automation-orchestrator +automation_orchestrator_operator_channel: stable +automation_orchestrator_operator_name: automation-orchestrator-operator +automation_orchestrator_operator_catalog_source_name: redhat-operators +# Index image for a custom CatalogSource; "" uses the existing catalog_source_name as-is. +automation_orchestrator_operator_catalog_image: "" +# Manual: the role approves the initial InstallPlan only (upgrades stay pending). +automation_orchestrator_operator_install_plan_approval: Manual +automation_orchestrator_operator_wait_timeout: 600 + +# --- Pre-release image mirrors (ImageDigestMirrorSet) --- +# Only needed for pre-release builds whose images are not on registry.redhat.io. +# Cluster-wide; applying it triggers a MachineConfigPool rollout. Pre-release example: +# - source: registry.redhat.io/ansible-automation-platform-27-next +# mirrors: [quay.io/aap/ansible-automation-platform-27-next] +# - source: registry.redhat.io/ansible-automation-platform-27 +# mirrors: [quay.io/aap/ansible-automation-platform-27-next] +automation_orchestrator_operator_idms_name: idms-aap-27-next +automation_orchestrator_operator_image_digest_mirrors: [] +automation_orchestrator_operator_mcp_wait_timeout: 1800 + +# --- AutomationOrchestrator CR --- +# Same as the operator namespace by default (aapctl runs OLM + CR in one namespace); +# override only if you need to split the cluster-scoped operator from the app instance. +automation_orchestrator_operator_app_namespace: "{{ automation_orchestrator_operator_operator_namespace }}" +automation_orchestrator_operator_instance_name: automation-orchestrator + +# --- PostgreSQL (bring-your-own) --- +# Defaults match the Secrets/Service created by the cloudnativepg_operator role +# (run it first, or point these at a customer-managed PostgreSQL instance instead). +automation_orchestrator_operator_postgres_host: orchestrator-postgres-rw +automation_orchestrator_operator_postgres_port: 5432 +automation_orchestrator_operator_postgres_ssl_mode: verify-ca +# Name of a pre-existing Secret (key ca.crt). CloudNativePG auto-generates +# "-ca" for its Cluster CR. Only used with sslMode verify-ca/verify-full. +automation_orchestrator_operator_postgres_ca_cert_secret: orchestrator-postgres-ca + +# Names of pre-existing Secrets (keys: database, username, password) — created by +# cloudnativepg_operator, or your own Secrets when using a customer-managed PostgreSQL. +automation_orchestrator_operator_backend_db_secret_name: orchestrator-postgres-secret +automation_orchestrator_operator_temporal_db_secret_name: temporal-postgres-secret + +# --- Ingress --- +# Route (OpenShift) or Ingress (AKS); the operator manages the resource itself. +automation_orchestrator_operator_ingress_type: Route +automation_orchestrator_operator_ingress_host: "" +automation_orchestrator_operator_ingress_annotations: {} +# Only used when ingress_type is Ingress — name of a pre-existing kubernetes.io/tls Secret. +automation_orchestrator_operator_ingress_tls_secret_name: "" + +# --- Component workload images (only needed for private registries) --- +automation_orchestrator_operator_image_pull_secrets: [] + +# --- Per-component overrides (replicas, resources, affinity, tolerations, nodeSelector) --- +# Passed through as-is into the matching spec. block; leave {} for operator defaults. +automation_orchestrator_operator_backend: {} +automation_orchestrator_operator_ui: {} +automation_orchestrator_operator_worker: {} +automation_orchestrator_operator_background_worker: {} +automation_orchestrator_operator_redis: {} +automation_orchestrator_operator_temporal_server: {} + +# Escape hatch: recursively merged into the generated spec last, for fields +# (fileStorage, otel, tls, secrets, workflowHttpRequestAllowedHosts, ...) not +# otherwise exposed as dedicated variables above. +automation_orchestrator_operator_extra_spec: {} + +# --- Debugging --- +# Set to false to disable no_log and expose secret values in task output. +automation_orchestrator_operator_no_log: true diff --git a/roles/automation_orchestrator_operator/meta/argument_specs.yml b/roles/automation_orchestrator_operator/meta/argument_specs.yml new file mode 100644 index 0000000..7379421 --- /dev/null +++ b/roles/automation_orchestrator_operator/meta/argument_specs.yml @@ -0,0 +1,158 @@ +--- +argument_specs: + main: + short_description: Install Automation Orchestrator via its OLM operator (AutomationOrchestrator CR) + description: + - Installs the Automation Orchestrator operator via OLM (redhat-operators by default, or a + custom CatalogSource for pre-release builds) and creates a single + AutomationOrchestrator CR. Adopts an existing install (OperatorGroup, installed CSV) + when operator_namespace points at one. + - >- + Automation Orchestrator is bring-your-own-PostgreSQL: this role only references + existing K8s Secrets (keys: database, username, password) — it does not create the + database/user or their credential Secret. Defaults match the cloudnativepg_operator + role's output; run that role first, or point postgres_* variables and the secret-name + variables at a customer-managed PostgreSQL instance. + - Modeled on https://github.com/ansible/aapctl and https://github.com/automation-nexus/automation-orchestrator-operator. + options: + automation_orchestrator_operator_operator_namespace: + description: Namespace for the Automation Orchestrator operator and (by default) its CR. + type: str + default: automation-orchestrator + automation_orchestrator_operator_channel: + description: OLM subscription channel. + type: str + default: stable + automation_orchestrator_operator_name: + description: Operator package name in the catalog. + type: str + default: automation-orchestrator-operator + automation_orchestrator_operator_catalog_source_name: + description: CatalogSource to subscribe from (created by this role only when catalog_image is set). + type: str + default: redhat-operators + automation_orchestrator_operator_catalog_image: + description: >- + Index image for a custom CatalogSource (e.g. a pre-release build). Empty string + uses the existing catalog_source_name without creating a CatalogSource. + type: str + default: "" + automation_orchestrator_operator_install_plan_approval: + description: >- + Subscription installPlanApproval (Manual or Automatic). With Manual, the role approves + only the initial InstallPlan; once a CSV is installed, upgrades are left pending. + type: str + choices: [Manual, Automatic] + default: Manual + automation_orchestrator_operator_wait_timeout: + description: Seconds to wait for the operator and instance to become ready. + type: int + default: 600 + automation_orchestrator_operator_idms_name: + description: Name of the cluster-wide ImageDigestMirrorSet for pre-release images. + type: str + default: idms-aap-27-next + automation_orchestrator_operator_image_digest_mirrors: + description: >- + spec.imageDigestMirrors entries ({source, mirrors}) for the IDMS. Applying a change + triggers a MachineConfigPool rollout. Only needed for pre-release builds; [] skips it. + type: list + elements: dict + default: [] + automation_orchestrator_operator_mcp_wait_timeout: + description: Seconds to wait for the MachineConfigPool rollout after the IDMS changes. + type: int + default: 1800 + automation_orchestrator_operator_app_namespace: + description: >- + Namespace for the AutomationOrchestrator instance. Exactly one CR is allowed per namespace. + Defaults to the same namespace as the operator. + type: str + default: "{operator_namespace}" + automation_orchestrator_operator_instance_name: + description: Name of the AutomationOrchestrator CR. + type: str + default: automation-orchestrator + automation_orchestrator_operator_postgres_host: + description: PostgreSQL host shared by the backend and Temporal databases. + type: str + default: orchestrator-postgres-rw + automation_orchestrator_operator_postgres_port: + description: PostgreSQL port. + type: int + default: 5432 + automation_orchestrator_operator_postgres_ssl_mode: + description: PostgreSQL SSL mode (disable, allow, prefer, require, verify-ca, verify-full). + type: str + default: verify-ca + automation_orchestrator_operator_postgres_ca_cert_secret: + description: >- + Name of a pre-existing Secret (key ca.crt) with the PostgreSQL server CA certificate. + Required when sslMode is verify-ca or verify-full. + type: str + default: orchestrator-postgres-ca + automation_orchestrator_operator_backend_db_secret_name: + description: Name of the pre-existing Secret with the backend database credentials. + type: str + default: orchestrator-postgres-secret + automation_orchestrator_operator_temporal_db_secret_name: + description: Name of the pre-existing Secret with the Temporal database credentials. + type: str + default: temporal-postgres-secret + automation_orchestrator_operator_ingress_type: + description: External access type (Route on OpenShift, Ingress on AKS, or None). + type: str + default: Route + automation_orchestrator_operator_ingress_host: + description: Custom hostname for external access. Leave unset to use the operator's auto-generated hostname. + type: str + default: "" + automation_orchestrator_operator_ingress_annotations: + description: Annotations applied to the generated Route/Ingress resource. + type: dict + default: {} + automation_orchestrator_operator_ingress_tls_secret_name: + description: Name of a pre-existing kubernetes.io/tls Secret. Only used when ingress_type is Ingress. + type: str + default: "" + automation_orchestrator_operator_image_pull_secrets: + description: List of {name} references to Secrets used to pull component workload images. + type: list + elements: dict + default: [] + automation_orchestrator_operator_backend: + description: Passthrough overrides (replicas, resources, affinity, tolerations, nodeSelector) for spec.backend. + type: dict + default: {} + automation_orchestrator_operator_ui: + description: Passthrough overrides for spec.ui. + type: dict + default: {} + automation_orchestrator_operator_worker: + description: Passthrough overrides for spec.worker. + type: dict + default: {} + automation_orchestrator_operator_background_worker: + description: >- + Passthrough overrides for spec.backgroundWorker. Note: replicas has no effect — + the operator always manages this component via an HPA. + type: dict + default: {} + automation_orchestrator_operator_redis: + description: Passthrough overrides for spec.redis. + type: dict + default: {} + automation_orchestrator_operator_temporal_server: + description: Passthrough overrides for spec.temporal.server. + type: dict + default: {} + automation_orchestrator_operator_extra_spec: + description: >- + Arbitrary dict recursively merged into the generated CR spec last — escape hatch for + fields not otherwise exposed (fileStorage, otel, tls, secrets, workflowHttpRequestAllowedHosts, ...). + type: dict + default: {} + automation_orchestrator_operator_no_log: + description: Set to false to disable no_log and expose secret values in task output. Never set false in shared environments. + type: bool + default: true diff --git a/roles/automation_orchestrator_operator/meta/main.yml b/roles/automation_orchestrator_operator/meta/main.yml new file mode 100644 index 0000000..5d8923d --- /dev/null +++ b/roles/automation_orchestrator_operator/meta/main.yml @@ -0,0 +1,18 @@ +--- +galaxy_info: + author: ptoal + description: Install Automation Orchestrator via its OpenShift/AKS OLM operator + license: MIT + min_ansible_version: "2.16" + platforms: + - name: GenericLinux + versions: + - all + galaxy_tags: + - openshift + - aap + - operator + - olm + - ansible + +dependencies: [] diff --git a/roles/automation_orchestrator_operator/tasks/image_mirrors.yml b/roles/automation_orchestrator_operator/tasks/image_mirrors.yml new file mode 100644 index 0000000..a5baf16 --- /dev/null +++ b/roles/automation_orchestrator_operator/tasks/image_mirrors.yml @@ -0,0 +1,63 @@ +--- +# Apply the ImageDigestMirrorSet that redirects pre-release AAP 2.7 images from +# registry.redhat.io to quay.io, then wait for the resulting MachineConfigPool +# rollout (matches https://github.com/ansible/aapctl scripts/create-catalogsources.sh). +# +# The IDMS is cluster-wide: it affects every digest pull from the source repos, +# not just Automation Orchestrator. The API may be briefly unavailable during the +# rollout on SNO, so the waits tolerate failed reads and keep retrying. + +- name: Read MachineConfigPools before applying IDMS + kubernetes.core.k8s_info: + api_version: machineconfiguration.openshift.io/v1 + kind: MachineConfigPool + register: __automation_orchestrator_operator_mcp_before + +- name: Create ImageDigestMirrorSet for pre-release images + kubernetes.core.k8s: + state: present + definition: + apiVersion: config.openshift.io/v1 + kind: ImageDigestMirrorSet + metadata: + name: "{{ automation_orchestrator_operator_idms_name }}" + spec: + imageDigestMirrors: "{{ automation_orchestrator_operator_image_digest_mirrors }}" + register: __automation_orchestrator_operator_idms + +# Without this, the Updated wait below could pass on the pools' pre-IDMS state +# before the MCO has rendered the new MachineConfig. +- name: Wait for MCO to render new MachineConfigs + kubernetes.core.k8s_info: + api_version: machineconfiguration.openshift.io/v1 + kind: MachineConfigPool + register: __automation_orchestrator_operator_mcp + until: >- + (__automation_orchestrator_operator_mcp.resources | default([]) | length) > 0 and + (__automation_orchestrator_operator_mcp.resources + | map(attribute='spec.configuration.name') + | intersect(__automation_orchestrator_operator_mcp_before.resources | map(attribute='spec.configuration.name')) + | length) == 0 + retries: "{{ __automation_orchestrator_operator_mcp_wait_retries }}" + delay: 10 + when: __automation_orchestrator_operator_idms is changed + +# Always runs, so a re-run also catches a rollout interrupted on a previous run. +- name: Wait for MachineConfigPool rollout to complete + kubernetes.core.k8s_info: + api_version: machineconfiguration.openshift.io/v1 + kind: MachineConfigPool + register: __automation_orchestrator_operator_mcp + until: >- + (__automation_orchestrator_operator_mcp.resources | default([]) | length) > 0 and + (__automation_orchestrator_operator_mcp.resources | map(attribute='spec.configuration.name') | list) == + (__automation_orchestrator_operator_mcp.resources | map(attribute='status.configuration.name', default='') | list) and + (__automation_orchestrator_operator_mcp.resources + | map(attribute='status.conditions', default=[]) + | map('selectattr', 'type', '==', 'Updated') + | map('selectattr', 'status', '==', 'True') + | map('list') + | reject('equalto', []) + | list | length) == (__automation_orchestrator_operator_mcp.resources | length) + retries: "{{ __automation_orchestrator_operator_mcp_wait_retries }}" + delay: 10 diff --git a/roles/automation_orchestrator_operator/tasks/main.yml b/roles/automation_orchestrator_operator/tasks/main.yml new file mode 100644 index 0000000..7716c51 --- /dev/null +++ b/roles/automation_orchestrator_operator/tasks/main.yml @@ -0,0 +1,271 @@ +--- +# Install Automation Orchestrator via its OpenShift OLM operator. +# +# Automation Orchestrator is bring-your-own-PostgreSQL: this role only creates the +# OperatorGroup/Subscription and the AutomationOrchestrator CR — it references +# existing K8s Secrets (keys: database/username/password, or host/port/database/ +# username/password for CNPG's basic-auth secrets) rather than creating them. +# Pair with the cloudnativepg_operator role (defaults match its secret names) or +# point postgres_* variables at a customer-managed PostgreSQL instance. +# +# Modeled on https://github.com/ansible/aapctl (internal/install/automationorchestrator) +# and https://github.com/automation-nexus/automation-orchestrator-operator. +# With Manual install-plan approval (the default) this role approves the initial +# InstallPlan itself; later upgrade InstallPlans are left for manual approval. +# +# All tasks are idempotent (kubernetes.core.k8s state: present). + +- name: Verify PostgreSQL host is set + ansible.builtin.assert: + that: + - automation_orchestrator_operator_postgres_host | length > 0 + fail_msg: >- + automation_orchestrator_operator_postgres_host must be set. Automation Orchestrator is + bring-your-own-PostgreSQL — either run the cloudnativepg_operator role first (its defaults + match this role's) or point this at a customer-managed PostgreSQL instance. + +# ------------------------------------------------------------------ +# Step 0: Mirror pre-release AAP 2.7 images (IDMS) — triggers an MCP rollout +# ------------------------------------------------------------------ +- name: Configure pre-release image mirrors + ansible.builtin.include_tasks: image_mirrors.yml + when: automation_orchestrator_operator_image_digest_mirrors | length > 0 + +# ------------------------------------------------------------------ +# Step 1: Install Automation Orchestrator operator via OLM +# ------------------------------------------------------------------ +- name: Create Automation Orchestrator namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ automation_orchestrator_operator_operator_namespace }}" + +- name: Create Automation Orchestrator app namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ automation_orchestrator_operator_app_namespace }}" + when: automation_orchestrator_operator_app_namespace != automation_orchestrator_operator_operator_namespace + +- name: Create CatalogSource for Automation Orchestrator + kubernetes.core.k8s: + state: present + definition: + apiVersion: operators.coreos.com/v1alpha1 + kind: CatalogSource + metadata: + name: "{{ automation_orchestrator_operator_catalog_source_name }}" + namespace: openshift-marketplace + spec: + sourceType: grpc + image: "{{ automation_orchestrator_operator_catalog_image }}" + displayName: Automation Orchestrator + when: automation_orchestrator_operator_catalog_image | length > 0 + +# OLM allows only one OperatorGroup per namespace; reuse any existing one +# (e.g. the auto-named group an OperatorHub install creates). +- name: Look up existing OperatorGroups + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1 + kind: OperatorGroup + namespace: "{{ automation_orchestrator_operator_operator_namespace }}" + register: __automation_orchestrator_operator_ogs + +- name: Create OperatorGroup for Automation Orchestrator + kubernetes.core.k8s: + state: present + definition: + apiVersion: operators.coreos.com/v1 + kind: OperatorGroup + metadata: + name: "{{ automation_orchestrator_operator_name }}" + namespace: "{{ automation_orchestrator_operator_operator_namespace }}" + spec: {} + when: __automation_orchestrator_operator_ogs.resources | length == 0 + +- name: Subscribe to Automation Orchestrator operator + kubernetes.core.k8s: + state: present + definition: + apiVersion: operators.coreos.com/v1alpha1 + kind: Subscription + metadata: + name: "{{ automation_orchestrator_operator_name }}" + namespace: "{{ automation_orchestrator_operator_operator_namespace }}" + spec: + channel: "{{ automation_orchestrator_operator_channel }}" + installPlanApproval: "{{ automation_orchestrator_operator_install_plan_approval }}" + name: "{{ automation_orchestrator_operator_name }}" + source: "{{ automation_orchestrator_operator_catalog_source_name }}" + sourceNamespace: openshift-marketplace + +# ------------------------------------------------------------------ +# Step 2: Approve the initial InstallPlan (if needed) and wait for the CSV +# ------------------------------------------------------------------ +# Once a CSV is installed, pending InstallPlans are upgrades — leave those for +# manual approval rather than approving them on every run. +- name: Wait for OLM to resolve the Subscription + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1alpha1 + kind: Subscription + namespace: "{{ automation_orchestrator_operator_operator_namespace }}" + name: "{{ automation_orchestrator_operator_name }}" + register: __automation_orchestrator_operator_sub + until: >- + __automation_orchestrator_operator_sub.resources | length > 0 and + ((__automation_orchestrator_operator_sub.resources[0].status.installedCSV | default('')) | length > 0 or + (__automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name | default('')) | length > 0) + retries: "{{ __automation_orchestrator_operator_wait_retries }}" + delay: 10 + +- name: Resolve installed CSV name + ansible.builtin.set_fact: + __automation_orchestrator_operator_csv_name: >- + {{ __automation_orchestrator_operator_sub.resources[0].status.installedCSV | default('') }} + +- name: Approve the initial InstallPlan + when: __automation_orchestrator_operator_csv_name | length == 0 + block: + - name: Read the InstallPlan + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1alpha1 + kind: InstallPlan + namespace: "{{ automation_orchestrator_operator_operator_namespace }}" + name: "{{ __automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name }}" + register: __automation_orchestrator_operator_installplan + + - name: Approve the InstallPlan + kubernetes.core.k8s_json_patch: + api_version: operators.coreos.com/v1alpha1 + kind: InstallPlan + namespace: "{{ automation_orchestrator_operator_operator_namespace }}" + name: "{{ __automation_orchestrator_operator_sub.resources[0].status.installPlanRef.name }}" + patch: + - op: add + path: /spec/approved + value: true + when: not (__automation_orchestrator_operator_installplan.resources[0].spec.approved | default(false)) + + - name: Resolve CSV name from the InstallPlan + ansible.builtin.set_fact: + __automation_orchestrator_operator_csv_name: >- + {{ __automation_orchestrator_operator_installplan.resources[0].spec.clusterServiceVersionNames[0] }} + +- name: Wait for Automation Orchestrator CSV to succeed + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1alpha1 + kind: ClusterServiceVersion + namespace: "{{ automation_orchestrator_operator_operator_namespace }}" + name: "{{ __automation_orchestrator_operator_csv_name }}" + register: __automation_orchestrator_operator_csv + until: >- + __automation_orchestrator_operator_csv.resources | length > 0 and + __automation_orchestrator_operator_csv.resources[0].status.phase | default('') == 'Succeeded' + retries: "{{ __automation_orchestrator_operator_wait_retries }}" + delay: 10 + +- name: Wait for AutomationOrchestrator CRD to be available + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: automationorchestrators.aap.ansible.com + register: __automation_orchestrator_operator_crd + until: __automation_orchestrator_operator_crd.resources | length > 0 + retries: "{{ __automation_orchestrator_operator_wait_retries }}" + delay: 10 + +# ------------------------------------------------------------------ +# Step 3: Deploy the AutomationOrchestrator instance +# ------------------------------------------------------------------ +- name: Build AutomationOrchestrator spec + ansible.builtin.set_fact: + __automation_orchestrator_operator_spec: >- + {{ + { + 'imagePullSecrets': automation_orchestrator_operator_image_pull_secrets, + 'postgres': { + 'host': automation_orchestrator_operator_postgres_host, + 'port': automation_orchestrator_operator_postgres_port, + 'sslMode': automation_orchestrator_operator_postgres_ssl_mode, + 'backendDatabase': {'secretRef': {'name': automation_orchestrator_operator_backend_db_secret_name}}, + 'temporalDatabase': {'secretRef': {'name': automation_orchestrator_operator_temporal_db_secret_name}}, + }, + 'ingress': { + 'type': automation_orchestrator_operator_ingress_type, + 'annotations': automation_orchestrator_operator_ingress_annotations, + }, + 'backend': automation_orchestrator_operator_backend, + 'ui': automation_orchestrator_operator_ui, + 'worker': automation_orchestrator_operator_worker, + 'backgroundWorker': automation_orchestrator_operator_background_worker, + 'redis': automation_orchestrator_operator_redis, + 'temporal': {'server': automation_orchestrator_operator_temporal_server}, + } + | combine( + {'postgres': {'caCertSecretRef': {'name': automation_orchestrator_operator_postgres_ca_cert_secret}}} if automation_orchestrator_operator_postgres_ca_cert_secret else {}, + recursive=True + ) + | combine( + {'ingress': {'host': automation_orchestrator_operator_ingress_host}} if automation_orchestrator_operator_ingress_host else {}, + recursive=True + ) + | combine( + {'ingress': {'tlsSecretRef': {'name': automation_orchestrator_operator_ingress_tls_secret_name}}} + if (automation_orchestrator_operator_ingress_type == 'Ingress' and automation_orchestrator_operator_ingress_tls_secret_name) + else {}, + recursive=True + ) + | combine(automation_orchestrator_operator_extra_spec, recursive=True) + }} + +- name: Create AutomationOrchestrator instance + kubernetes.core.k8s: + state: present + definition: + apiVersion: aap.ansible.com/v1alpha1 + kind: AutomationOrchestrator + metadata: + name: "{{ automation_orchestrator_operator_instance_name }}" + namespace: "{{ automation_orchestrator_operator_app_namespace }}" + spec: "{{ __automation_orchestrator_operator_spec }}" + +# ------------------------------------------------------------------ +# Step 4: Wait for the instance to be ready +# ------------------------------------------------------------------ +- name: Wait for AutomationOrchestrator to be ready + kubernetes.core.k8s_info: + api_version: aap.ansible.com/v1alpha1 + kind: AutomationOrchestrator + namespace: "{{ automation_orchestrator_operator_app_namespace }}" + name: "{{ automation_orchestrator_operator_instance_name }}" + register: __automation_orchestrator_operator_status + ignore_errors: true + until: >- + __automation_orchestrator_operator_status.resources is defined and + __automation_orchestrator_operator_status.resources | length > 0 and + (__automation_orchestrator_operator_status.resources[0].status.conditions | default([]) + | selectattr('type', '==', 'Ready') + | selectattr('status', '==', 'True') | list | length > 0) + retries: "{{ __automation_orchestrator_operator_wait_retries }}" + delay: 10 + +# ------------------------------------------------------------------ +# Step 5: Display summary +# ------------------------------------------------------------------ +- name: Display Automation Orchestrator deployment summary + ansible.builtin.debug: + msg: + - "Automation Orchestrator deployment complete!" + - " Namespace : {{ automation_orchestrator_operator_app_namespace }}" + - " Instance : {{ automation_orchestrator_operator_instance_name }}" + - " Ingress type: {{ automation_orchestrator_operator_ingress_type }}" + - "" + - "Find the UI hostname with: oc get routes -n {{ automation_orchestrator_operator_app_namespace }}" + - "The initial admin password is auto-generated unless spec.secrets.initialAdminPasswordSecretRef is set:" + - " oc get secret {{ automation_orchestrator_operator_instance_name }}-initial-admin-password -n {{ automation_orchestrator_operator_app_namespace }} -o jsonpath='{.data.password}' | base64 -d" diff --git a/roles/automation_orchestrator_operator/vars/main.yml b/roles/automation_orchestrator_operator/vars/main.yml new file mode 100644 index 0000000..81c333d --- /dev/null +++ b/roles/automation_orchestrator_operator/vars/main.yml @@ -0,0 +1,4 @@ +--- +# Computed internal variables - do not override +__automation_orchestrator_operator_wait_retries: "{{ (automation_orchestrator_operator_wait_timeout / 10) | int }}" +__automation_orchestrator_operator_mcp_wait_retries: "{{ (automation_orchestrator_operator_mcp_wait_timeout / 10) | int }}" diff --git a/roles/cloudnativepg_operator/defaults/main.yml b/roles/cloudnativepg_operator/defaults/main.yml new file mode 100644 index 0000000..ff2506a --- /dev/null +++ b/roles/cloudnativepg_operator/defaults/main.yml @@ -0,0 +1,50 @@ +--- +# --- OLM subscription --- +# CloudNativePG is in the certified-operators catalog by default (no custom +# CatalogSource needed, unlike automation_orchestrator_operator). +cloudnativepg_operator_operator_namespace: cloudnative-pg +cloudnativepg_operator_channel: stable-v1 +cloudnativepg_operator_name: cloudnative-pg +cloudnativepg_operator_catalog_source: certified-operators +cloudnativepg_operator_wait_timeout: 600 + +# --- Cluster CR --- +# Namespace for the Cluster, credential Secrets, and Database CRs. Defaults to +# the automation_orchestrator_operator app namespace — keep the two in sync if overridden. +cloudnativepg_operator_cluster_namespace: automation-orchestrator +cloudnativepg_operator_cluster_name: orchestrator-postgres +cloudnativepg_operator_owner_user: orchestrator +cloudnativepg_operator_instances: 1 +cloudnativepg_operator_image_name: ghcr.io/cloudnative-pg/postgresql:15 +cloudnativepg_operator_storage_size: 5Gi +cloudnativepg_operator_storage_class: "" +cloudnativepg_operator_max_connections: 200 + +# --- Databases (Database CRs, all owned by cloudnativepg_operator_owner_user) --- +cloudnativepg_operator_databases: + - name: orchestrator + dbname: orchestrator + - name: temporal + dbname: temporal + - name: temporal-visibility + dbname: temporal_visibility + +# --- Credential secrets (type kubernetes.io/basic-auth) --- +# All share the same generated username/password; only the referenced database differs. +# secrets[0] is used as the CNPG bootstrap.initdb secret (creates the owner role + first DB). +cloudnativepg_operator_secrets: + - name: orchestrator-postgres-secret + database: orchestrator + - name: temporal-postgres-secret + database: temporal + - name: temporal-visibility-postgres-secret + database: temporal_visibility + +# --- Vault --- +# KV2 path/key where the generated postgres password is stored and retrieved. +cloudnativepg_operator_vault_mount: kv +cloudnativepg_operator_vault_path: cloudnativepg/orchestrator-postgres + +# --- Debugging --- +# Set to false to disable no_log and expose secret values in task output. +cloudnativepg_operator_no_log: true diff --git a/roles/cloudnativepg_operator/meta/argument_specs.yml b/roles/cloudnativepg_operator/meta/argument_specs.yml new file mode 100644 index 0000000..cc424b9 --- /dev/null +++ b/roles/cloudnativepg_operator/meta/argument_specs.yml @@ -0,0 +1,89 @@ +--- +argument_specs: + main: + short_description: Install CloudNativePG via OLM and provision a PostgreSQL Cluster + Databases + description: + - Installs the CloudNativePG operator via OLM (certified-operators catalog, Manual install-plan + approval which this role approves itself) and creates a single-instance PostgreSQL Cluster CR + with Database CRs for the orchestrator, temporal, and temporal_visibility databases — all owned + by one shared PostgreSQL role. + - >- + WARNING: this is a demo/dev-oriented, non-HA PostgreSQL deployment. For production, point + automation_orchestrator_operator at a customer-managed PostgreSQL instance instead. + - The generated owner-role password is stored in Vault at cloudnativepg_operator_vault_path + and reused on subsequent runs for idempotency. + options: + cloudnativepg_operator_operator_namespace: + description: Namespace for the CloudNativePG operator. + type: str + default: cloudnative-pg + cloudnativepg_operator_channel: + description: OLM subscription channel. + type: str + default: stable-v1 + cloudnativepg_operator_name: + description: Operator package name in the catalog. + type: str + default: cloudnative-pg + cloudnativepg_operator_catalog_source: + description: Name of the CatalogSource to subscribe from. + type: str + default: certified-operators + cloudnativepg_operator_wait_timeout: + description: Seconds to wait for the operator, CSV, and Database CRs to become ready. + type: int + default: 600 + cloudnativepg_operator_cluster_namespace: + description: Namespace for the Cluster CR, credential Secrets, and Database CRs. + type: str + default: automation-orchestrator + cloudnativepg_operator_cluster_name: + description: Name of the CloudNativePG Cluster CR. + type: str + default: orchestrator-postgres + cloudnativepg_operator_owner_user: + description: PostgreSQL role that owns all provisioned databases. + type: str + default: orchestrator + cloudnativepg_operator_instances: + description: Number of Postgres instances in the Cluster (1 = no HA). + type: int + default: 1 + cloudnativepg_operator_image_name: + description: PostgreSQL container image for the Cluster. + type: str + default: ghcr.io/cloudnative-pg/postgresql:15 + cloudnativepg_operator_storage_size: + description: Size of the Cluster's PersistentVolumeClaim. + type: str + default: 5Gi + cloudnativepg_operator_storage_class: + description: StorageClass for the Cluster's PVC. Empty string uses the cluster default. + type: str + default: "" + cloudnativepg_operator_max_connections: + description: postgresql.parameters.max_connections. Set to 0 to omit and use the Postgres default. + type: int + default: 200 + cloudnativepg_operator_databases: + description: List of {name, dbname} Database CRs to create, all owned by cloudnativepg_operator_owner_user. + type: list + elements: dict + cloudnativepg_operator_secrets: + description: >- + List of {name, database} credential Secrets to create (type kubernetes.io/basic-auth), all + sharing the same generated username/password. secrets[0] is used as the CNPG bootstrap.initdb secret. + type: list + elements: dict + cloudnativepg_operator_vault_mount: + description: Vault KV2 mount point where the generated owner-role password is stored. + type: str + default: kv + cloudnativepg_operator_vault_path: + description: Vault KV2 path where the generated owner-role password is stored. + type: str + default: cloudnativepg/orchestrator-postgres + cloudnativepg_operator_no_log: + description: Set to false to disable no_log and expose secret values in task output. Never set false in shared environments. + type: bool + default: true diff --git a/roles/cloudnativepg_operator/meta/main.yml b/roles/cloudnativepg_operator/meta/main.yml new file mode 100644 index 0000000..99afe60 --- /dev/null +++ b/roles/cloudnativepg_operator/meta/main.yml @@ -0,0 +1,18 @@ +--- +galaxy_info: + author: ptoal + description: Install CloudNativePG via OLM and provision a PostgreSQL Cluster for Automation Orchestrator + license: MIT + min_ansible_version: "2.16" + platforms: + - name: GenericLinux + versions: + - all + galaxy_tags: + - openshift + - postgresql + - cnpg + - operator + - olm + +dependencies: [] diff --git a/roles/cloudnativepg_operator/tasks/main.yml b/roles/cloudnativepg_operator/tasks/main.yml new file mode 100644 index 0000000..233b208 --- /dev/null +++ b/roles/cloudnativepg_operator/tasks/main.yml @@ -0,0 +1,229 @@ +--- +# Install CloudNativePG via OLM and provision the PostgreSQL Cluster + Database CRs +# and credential Secrets that back Automation Orchestrator (backend + Temporal + +# Temporal Visibility databases, one shared owner role). +# +# WARNING: this is a single-instance (no HA), operator-provisioned PostgreSQL +# cluster intended for demo/dev use. For production, point automation_orchestrator_operator +# at a customer-managed PostgreSQL instance instead of running this role. +# +# Modeled on https://github.com/ansible/aapctl (internal/install/cloudnativepg). +# CloudNativePG's OLM subscription uses Manual install-plan approval, so this role +# approves the InstallPlan itself rather than relying on Automatic. +# +# All tasks are idempotent (kubernetes.core.k8s state: present). + +- name: Resolve postgres credentials from Vault + ansible.builtin.include_tasks: vault_secrets.yml + +# ------------------------------------------------------------------ +# Step 1: Install CloudNativePG operator via OLM +# ------------------------------------------------------------------ +- name: Create CloudNativePG operator namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ cloudnativepg_operator_operator_namespace }}" + +- name: Create CloudNativePG OperatorGroup + kubernetes.core.k8s: + state: present + definition: + apiVersion: operators.coreos.com/v1 + kind: OperatorGroup + metadata: + name: "{{ cloudnativepg_operator_name }}" + namespace: "{{ cloudnativepg_operator_operator_namespace }}" + spec: {} + +- name: Subscribe to CloudNativePG operator + kubernetes.core.k8s: + state: present + definition: + apiVersion: operators.coreos.com/v1alpha1 + kind: Subscription + metadata: + name: "{{ cloudnativepg_operator_name }}" + namespace: "{{ cloudnativepg_operator_operator_namespace }}" + spec: + channel: "{{ cloudnativepg_operator_channel }}" + installPlanApproval: Manual + name: "{{ cloudnativepg_operator_name }}" + source: "{{ cloudnativepg_operator_catalog_source }}" + sourceNamespace: openshift-marketplace + +# ------------------------------------------------------------------ +# Step 2: Approve the Manual InstallPlan and wait for the CSV to succeed +# ------------------------------------------------------------------ +- name: Wait for OLM to create an InstallPlan for the Subscription + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1alpha1 + kind: Subscription + namespace: "{{ cloudnativepg_operator_operator_namespace }}" + name: "{{ cloudnativepg_operator_name }}" + register: __cloudnativepg_operator_sub + until: >- + __cloudnativepg_operator_sub.resources | length > 0 and + (__cloudnativepg_operator_sub.resources[0].status.installPlanRef.name | default('')) | length > 0 + retries: "{{ __cloudnativepg_operator_wait_retries }}" + delay: 10 + +- name: Read the InstallPlan + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1alpha1 + kind: InstallPlan + namespace: "{{ cloudnativepg_operator_operator_namespace }}" + name: "{{ __cloudnativepg_operator_sub.resources[0].status.installPlanRef.name }}" + register: __cloudnativepg_operator_installplan + +- name: Approve the InstallPlan + kubernetes.core.k8s_json_patch: + api_version: operators.coreos.com/v1alpha1 + kind: InstallPlan + namespace: "{{ cloudnativepg_operator_operator_namespace }}" + name: "{{ __cloudnativepg_operator_sub.resources[0].status.installPlanRef.name }}" + patch: + - op: add + path: /spec/approved + value: true + +- name: Wait for CloudNativePG CSV to succeed + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1alpha1 + kind: ClusterServiceVersion + namespace: "{{ cloudnativepg_operator_operator_namespace }}" + name: "{{ __cloudnativepg_operator_installplan.resources[0].spec.clusterServiceVersionNames[0] }}" + register: __cloudnativepg_operator_csv + until: >- + __cloudnativepg_operator_csv.resources | length > 0 and + __cloudnativepg_operator_csv.resources[0].status.phase | default('') == 'Succeeded' + retries: "{{ __cloudnativepg_operator_wait_retries }}" + delay: 10 + +- name: Wait for CloudNativePG controller manager webhook to be reachable + kubernetes.core.k8s_info: + api_version: v1 + kind: Endpoints + namespace: "{{ cloudnativepg_operator_operator_namespace }}" + name: cnpg-controller-manager-service + register: __cloudnativepg_operator_endpoints + until: >- + __cloudnativepg_operator_endpoints.resources | length > 0 and + (__cloudnativepg_operator_endpoints.resources[0].subsets | default([]) + | map(attribute='addresses', default=[]) | flatten | length) > 0 + retries: "{{ __cloudnativepg_operator_wait_retries }}" + delay: 10 + +# ------------------------------------------------------------------ +# Step 3: Credential secrets (type kubernetes.io/basic-auth) +# ------------------------------------------------------------------ +- name: Create cluster namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ cloudnativepg_operator_cluster_namespace }}" + +- name: Create postgres credential secrets + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ item.name }}" + namespace: "{{ cloudnativepg_operator_cluster_namespace }}" + type: kubernetes.io/basic-auth + stringData: + username: "{{ cloudnativepg_operator_owner_user }}" + password: "{{ __cloudnativepg_operator_password }}" + host: "{{ cloudnativepg_operator_cluster_name }}-rw.{{ cloudnativepg_operator_cluster_namespace }}.svc" + port: "5432" + database: "{{ item.database }}" + loop: "{{ cloudnativepg_operator_secrets }}" + loop_control: + label: "{{ item.name }}" + no_log: "{{ cloudnativepg_operator_no_log }}" + +# ------------------------------------------------------------------ +# Step 4: PostgreSQL Cluster +# ------------------------------------------------------------------ +- name: Create PostgreSQL Cluster + kubernetes.core.k8s: + state: present + definition: + apiVersion: postgresql.cnpg.io/v1 + kind: Cluster + metadata: + name: "{{ cloudnativepg_operator_cluster_name }}" + namespace: "{{ cloudnativepg_operator_cluster_namespace }}" + spec: >- + {{ + { + 'instances': cloudnativepg_operator_instances, + 'imageName': cloudnativepg_operator_image_name, + 'bootstrap': { + 'initdb': { + 'owner': cloudnativepg_operator_owner_user, + 'secret': {'name': cloudnativepg_operator_secrets[0].name}, + }, + }, + 'storage': {'size': cloudnativepg_operator_storage_size}, + } + | combine({'storage': {'storageClass': cloudnativepg_operator_storage_class}} if cloudnativepg_operator_storage_class else {}, recursive=True) + | combine({'postgresql': {'parameters': {'max_connections': cloudnativepg_operator_max_connections | string}}} if cloudnativepg_operator_max_connections else {}, recursive=True) + }} + +# ------------------------------------------------------------------ +# Step 5: Logical databases +# ------------------------------------------------------------------ +- name: Create Database CRs + kubernetes.core.k8s: + state: present + definition: + apiVersion: postgresql.cnpg.io/v1 + kind: Database + metadata: + name: "{{ item.name }}" + namespace: "{{ cloudnativepg_operator_cluster_namespace }}" + spec: + name: "{{ item.dbname }}" + owner: "{{ cloudnativepg_operator_owner_user }}" + cluster: + name: "{{ cloudnativepg_operator_cluster_name }}" + loop: "{{ cloudnativepg_operator_databases }}" + loop_control: + label: "{{ item.name }}" + +- name: Wait for Database CRs to be applied + kubernetes.core.k8s_info: + api_version: postgresql.cnpg.io/v1 + kind: Database + namespace: "{{ cloudnativepg_operator_cluster_namespace }}" + register: __cloudnativepg_operator_databases_status + until: >- + __cloudnativepg_operator_databases_status.resources | length >= (cloudnativepg_operator_databases | length) and + (__cloudnativepg_operator_databases_status.resources + | rejectattr('status.applied', 'undefined') + | selectattr('status.applied') + | list | length) == (__cloudnativepg_operator_databases_status.resources | length) + retries: "{{ __cloudnativepg_operator_wait_retries }}" + delay: 10 + +# ------------------------------------------------------------------ +# Step 6: Summary +# ------------------------------------------------------------------ +- name: Display CloudNativePG deployment summary + ansible.builtin.debug: + msg: + - "CloudNativePG PostgreSQL cluster ready!" + - " Operator namespace : {{ cloudnativepg_operator_operator_namespace }}" + - " Cluster : {{ cloudnativepg_operator_cluster_name }} ({{ cloudnativepg_operator_cluster_namespace }})" + - " Owner role : {{ cloudnativepg_operator_owner_user }}" + - " Databases : {{ cloudnativepg_operator_databases | map(attribute='dbname') | join(', ') }}" + - " Credential secrets : {{ cloudnativepg_operator_secrets | map(attribute='name') | join(', ') }}" diff --git a/roles/cloudnativepg_operator/tasks/vault_secrets.yml b/roles/cloudnativepg_operator/tasks/vault_secrets.yml new file mode 100644 index 0000000..201f907 --- /dev/null +++ b/roles/cloudnativepg_operator/tasks/vault_secrets.yml @@ -0,0 +1,26 @@ +--- +# Read the shared postgres owner password from Vault; generate and store it if missing. +# Sets __cloudnativepg_operator_password, consumed by the K8s secret tasks in main.yml. + +- name: Read cloudnativepg postgres password from Vault + community.hashi_vault.vault_kv2_get: + path: "{{ cloudnativepg_operator_vault_path }}" + engine_mount_point: "{{ cloudnativepg_operator_vault_mount }}" + register: __cloudnativepg_operator_vault_read + failed_when: false + no_log: "{{ cloudnativepg_operator_no_log }}" + +- name: Resolve postgres password (generate if missing) + ansible.builtin.set_fact: + __cloudnativepg_operator_password: >- + {{ (__cloudnativepg_operator_vault_read.secret | default({})).password + | default(lookup('community.general.random_string', length=32, special=false)) }} + no_log: "{{ cloudnativepg_operator_no_log }}" + +- name: Write postgres password to Vault + community.hashi_vault.vault_kv2_write: + path: "{{ cloudnativepg_operator_vault_path }}" + engine_mount_point: "{{ cloudnativepg_operator_vault_mount }}" + data: + password: "{{ __cloudnativepg_operator_password }}" + no_log: "{{ cloudnativepg_operator_no_log }}" diff --git a/roles/cloudnativepg_operator/vars/main.yml b/roles/cloudnativepg_operator/vars/main.yml new file mode 100644 index 0000000..803a066 --- /dev/null +++ b/roles/cloudnativepg_operator/vars/main.yml @@ -0,0 +1,3 @@ +--- +# Computed internal variables - do not override +__cloudnativepg_operator_wait_retries: "{{ (cloudnativepg_operator_wait_timeout / 10) | int }}"