Configure OIDC, make idempotent, fix bugs. Claude.ai

This commit is contained in:
2026-02-25 13:20:12 -05:00
parent 995b7c4070
commit d981b69669
23 changed files with 2269 additions and 760 deletions

View File

@@ -1,321 +0,0 @@
---
# Configure OIDC authentication on SNO OpenShift using Keycloak as identity provider.
#
# This playbook creates a Keycloak client for OpenShift and configures the
# cluster's OAuth resource to use Keycloak as an OpenID Connect identity provider.
#
# Prerequisites:
# - SNO cluster is installed and accessible (see deploy_openshift.yml)
# - Keycloak server is running and accessible
# - oc binary available in the EE PATH (override with oc_binary if running outside EE)
# - middleware_automation.keycloak collection installed (see collections/requirements.yml)
#
# Inventory requirements:
# openshift group (e.g. sno.openshift.toal.ca)
# host_vars: ocp_cluster_name, ocp_base_domain, sno_install_dir
# secrets: vault_keycloak_admin_password
# vault_oidc_client_secret (optional — auto-generated if omitted; save the output!)
#
# Key variables:
# keycloak_url - Keycloak base URL (e.g. https://keycloak.toal.ca)
# keycloak_realm - Keycloak realm name (e.g. toallab)
# keycloak_admin_user - Keycloak admin username (default: admin)
# keycloak_context - URL prefix for Keycloak API:
# "" for Quarkus/modern Keycloak (default)
# "/auth" for legacy JBoss/WildFly Keycloak
# vault_keycloak_admin_password - Keycloak admin password (vaulted)
# vault_oidc_client_secret - OIDC client secret (vaulted, optional — auto-generated if omitted)
# oidc_provider_name - IdP name shown in OpenShift login (default: keycloak)
# oidc_client_id - Client ID in Keycloak (default: openshift)
#
# Optional variables:
# oidc_admin_groups - List of Keycloak groups to grant cluster-admin (default: [])
# oidc_ca_cert_file - Local path to CA cert PEM for Keycloak TLS (private CA only)
# oc_binary - Path to oc binary (default: oc from EE PATH)
# oc_kubeconfig - Path to kubeconfig (default: sno_install_dir/auth/kubeconfig)
#
# Usage:
# op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml
# op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml --tags keycloak
# op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml --tags openshift
# ---------------------------------------------------------------------------
# Play 1: Configure Keycloak OIDC client for OpenShift
# ---------------------------------------------------------------------------
- name: Configure Keycloak OIDC client for OpenShift
hosts: openshift
gather_facts: false
connection: local
tags: keycloak
vars:
# Set to "/auth" for legacy JBoss/WildFly-based Keycloak; leave empty for Quarkus (v17+)
keycloak_context: ""
oidc_provider_name: keycloak
oidc_client_id: openshift
oidc_redirect_uri: "https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}/oauth2callback/{{ oidc_provider_name }}"
__oidc_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context }}"
module_defaults:
middleware_automation.keycloak.keycloak_realm:
auth_client_id: admin-cli
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
auth_realm: master
auth_username: "{{ keycloak_admin_user }}"
auth_password: "{{ vault_keycloak_admin_password }}"
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
middleware_automation.keycloak.keycloak_client:
auth_client_id: admin-cli
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
auth_realm: master
auth_username: "{{ keycloak_admin_user }}"
auth_password: "{{ vault_keycloak_admin_password }}"
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
tasks:
# Generate a random 32-char alphanumeric secret if vault_oidc_client_secret is not supplied.
# The generated value is stored in __oidc_client_secret and displayed after the play so it
# can be vaulted and re-used on subsequent runs.
- name: Set OIDC client secret (use vault value or generate random)
ansible.builtin.set_fact:
__oidc_client_secret: "{{ vault_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}"
__oidc_secret_generated: "{{ vault_oidc_client_secret is not defined }}"
no_log: true
- name: Ensure Keycloak realm exists
middleware_automation.keycloak.keycloak_realm:
realm: "{{ keycloak_realm }}"
id: "{{ keycloak_realm }}"
display_name: "{{ keycloak_realm_display_name | default(keycloak_realm | title) }}"
enabled: true
state: present
no_log: "{{ keycloak_no_log | default(true) }}"
- name: Create OpenShift OIDC client in Keycloak
middleware_automation.keycloak.keycloak_client:
realm: "{{ keycloak_realm }}"
client_id: "{{ oidc_client_id }}"
name: "OpenShift - {{ ocp_cluster_name }}"
description: "OIDC client for OpenShift cluster {{ ocp_cluster_name }}.{{ ocp_base_domain }}"
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
secret: "{{ __oidc_client_secret }}"
redirect_uris:
- "{{ oidc_redirect_uri }}"
web_origins:
- "+"
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: "{{ keycloak_no_log | default(true) }}"
- name: Display generated client secret (save this to vault!)
ansible.builtin.debug:
msg:
- "*** GENERATED OIDC CLIENT SECRET — SAVE THIS TO VAULT ***"
- "vault_oidc_client_secret: {{ __oidc_client_secret }}"
- ""
- "Set this in host_vars or pass as --extra-vars on future runs."
when: __oidc_secret_generated | bool
- name: Display Keycloak configuration summary
ansible.builtin.debug:
msg:
- "Keycloak OIDC client configured:"
- " Realm : {{ keycloak_realm }}"
- " Client : {{ oidc_client_id }}"
- " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}"
- " Redirect: {{ oidc_redirect_uri }}"
verbosity: 1
# ---------------------------------------------------------------------------
# Play 2: Configure OpenShift OAuth with Keycloak OIDC
# ---------------------------------------------------------------------------
- name: Configure OpenShift OAuth with Keycloak OIDC
hosts: sno.openshift.toal.ca
gather_facts: false
connection: local
tags: openshift
vars:
# Set to "/auth" for legacy JBoss/WildFly-based Keycloak; leave empty for Quarkus (v17+)
keycloak_context: ""
oidc_provider_name: keycloak
oidc_client_id: openshift
oidc_admin_groups: []
__oidc_secret_name: keycloak-oidc-client-secret
__oidc_ca_configmap_name: keycloak-oidc-ca-bundle
__oidc_oc: "{{ oc_binary | default('oc') }}"
# Prefer the fact set by Play 1; fall back to vault var when running --tags openshift alone
__oidc_client_secret_value: "{{ hostvars[inventory_hostname]['__oidc_client_secret'] | default(vault_oidc_client_secret) }}"
tasks:
- name: Create temp directory for manifests
ansible.builtin.tempfile:
state: directory
suffix: .oidc
register: __oidc_tmpdir
# ------------------------------------------------------------------
# Secret: Keycloak client secret in openshift-config namespace
# ------------------------------------------------------------------
- name: Write Keycloak client secret manifest
ansible.builtin.copy:
dest: "{{ __oidc_tmpdir.path }}/keycloak-secret.yaml"
mode: "0600"
content: |
apiVersion: v1
kind: Secret
metadata:
name: {{ __oidc_secret_name }}
namespace: openshift-config
type: Opaque
stringData:
clientSecret: {{ __oidc_client_secret_value }}
no_log: true
- name: Apply Keycloak client secret
ansible.builtin.command:
cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/keycloak-secret.yaml"
register: __oidc_secret_apply
changed_when: "'configured' in __oidc_secret_apply.stdout or 'created' in __oidc_secret_apply.stdout"
# ------------------------------------------------------------------
# CA bundle: only needed when Keycloak uses a private/internal CA
# ------------------------------------------------------------------
- name: Configure CA bundle ConfigMap for Keycloak TLS
when: oidc_ca_cert_file is defined
block:
- name: Write CA bundle ConfigMap manifest
ansible.builtin.copy:
dest: "{{ __oidc_tmpdir.path }}/keycloak-ca.yaml"
mode: "0644"
content: |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ __oidc_ca_configmap_name }}
namespace: openshift-config
data:
ca.crt: |
{{ lookup('ansible.builtin.file', oidc_ca_cert_file) | indent(4) }}
- name: Apply CA bundle ConfigMap
ansible.builtin.command:
cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/keycloak-ca.yaml"
register: __oidc_ca_apply
changed_when: "'configured' in __oidc_ca_apply.stdout or 'created' in __oidc_ca_apply.stdout"
# ------------------------------------------------------------------
# OAuth cluster resource: add/replace Keycloak IdP entry
# Reads the current config, replaces any existing entry with the same
# name, and applies the merged result — preserving other IdPs.
# ------------------------------------------------------------------
- name: Get current OAuth cluster configuration
ansible.builtin.command:
cmd: "{{ __oidc_oc }} get oauth cluster -o json"
register: __oidc_current_oauth
changed_when: false
- name: Parse current OAuth configuration
ansible.builtin.set_fact:
__oidc__oidc_current_oauth_obj: "{{ __oidc_current_oauth.stdout | from_json }}"
- name: Build Keycloak OIDC identity provider definition
ansible.builtin.set_fact:
__oidc_new_idp: >-
{{
{
'name': oidc_provider_name,
'mappingMethod': 'claim',
'type': 'OpenID',
'openID': (
{
'clientID': oidc_client_id,
'clientSecret': {'name': __oidc_secret_name},
'issuer': keycloak_url ~ keycloak_context ~ '/realms/' ~ keycloak_realm,
'claims': {
'preferredUsername': ['preferred_username'],
'name': ['name'],
'email': ['email'],
'groups': ['groups']
}
} | combine(
oidc_ca_cert_file is defined | ternary(
{'ca': {'name': __oidc_ca_configmap_name}}, {}
)
)
)
}
}}
- name: Build updated identity providers list
ansible.builtin.set_fact:
__oidc_updated_idps: >-
{{
(__oidc__oidc_current_oauth_obj.spec.identityProviders | default([])
| selectattr('name', '!=', oidc_provider_name) | list)
+ [__oidc_new_idp]
}}
- name: Write updated OAuth cluster manifest
ansible.builtin.copy:
dest: "{{ __oidc_tmpdir.path }}/oauth-cluster.yaml"
mode: "0644"
content: "{{ __oidc__oidc_current_oauth_obj | combine({'spec': {'identityProviders': __oidc_updated_idps}}, recursive=true) | to_nice_yaml }}"
- name: Apply updated OAuth cluster configuration
ansible.builtin.command:
cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/oauth-cluster.yaml"
register: __oidc_oauth_apply
changed_when: "'configured' in __oidc_oauth_apply.stdout or 'created' in __oidc_oauth_apply.stdout"
- name: Wait for OAuth deployment to roll out
ansible.builtin.command:
cmd: "{{ __oidc_oc }} rollout status deployment/oauth-openshift -n openshift-authentication --timeout=300s"
changed_when: false
# ------------------------------------------------------------------
# Optional: grant cluster-admin to specified Keycloak groups
# ------------------------------------------------------------------
- name: Grant cluster-admin to OIDC admin groups
ansible.builtin.command:
cmd: "{{ __oidc_oc }} adm policy add-cluster-role-to-group cluster-admin {{ item }}"
loop: "{{ oidc_admin_groups }}"
changed_when: true
when: oidc_admin_groups | length > 0
- name: Remove temp directory
ansible.builtin.file:
path: "{{ __oidc_tmpdir.path }}"
state: absent
- name: Display post-configuration summary
ansible.builtin.debug:
msg:
- "OpenShift OIDC configuration complete!"
- " Provider : {{ oidc_provider_name }}"
- " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}"
- " Console : https://console-openshift-console.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}"
- " Login : https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}"
- ""
- "Note: OAuth pods are restarting — login may be unavailable for ~2 minutes."
verbosity: 1

View File

@@ -1,37 +1,40 @@
---
# Deploy Single Node OpenShift (SNO) on Proxmox
# Deploy and configure Single Node OpenShift (SNO) on Proxmox
#
# Prerequisites:
# ansible-galaxy collection install -r collections/requirements.yml
# openshift-install is downloaded automatically during the sno play
# openshift-install is downloaded automatically during the sno_deploy_install play
#
# Inventory requirements:
# sno.openshift.toal.ca - in 'openshift' group
# host_vars: ocp_cluster_name, ocp_base_domain, ocp_version, sno_ip,
# sno_gateway, sno_nameserver, sno_prefix_length, sno_vm_name,
# sno_bridge, sno_vlan, proxmox_node, ...
# secrets: vault_ocp_pull_secret (Red Hat pull secret JSON string)
# proxmox_api - inventory host (ansible_host: proxmox.lab.toal.ca, ansible_port: 443)
# Used as api_host / api_port source for community.proxmox modules
# proxmox_host - inventory host (ansible_host: pve1.lab.toal.ca, ansible_connection: ssh)
# delegate_to target for qm and file operations
# sno_bridge, sno_vlan, proxmox_node, keycloak_url, keycloak_realm,
# oidc_admin_groups, sno_deploy_letsencrypt_email, ...
# secrets: vault_ocp_pull_secret, vault_keycloak_admin_password,
# vault_oidc_client_secret (optional)
# proxmox_api - inventory host (ansible_host, ansible_port)
# proxmox_host - inventory host (ansible_host, ansible_connection: ssh)
# gate.toal.ca - in 'opnsense' group
# host_vars: opnsense_host, opnsense_api_key, opnsense_api_secret,
# opnsense_api_port, haproxy_public_ip
# group_vars/all: dme_account_key, dme_account_secret
#
# Play order (intentional — DNS must precede VM boot):
# Play 1: proxmox — Create SNO VM
# Play 2: opnsense — Configure OPNsense local DNS overrides (api/api-int/apps)
# Play 3: dns — Configure public DNS records in DNS Made Easy
# Play 4: sno — Generate ISO, boot VM, wait for install
# Play 1: sno_deploy_vm — Create SNO VM
# Play 2: opnsense — Configure OPNsense local DNS overrides
# Play 3: dns — Configure public DNS records in DNS Made Easy
# Play 4: sno_deploy_install — Generate ISO, boot VM, wait for install
# Play 5: keycloak — Configure Keycloak OIDC client
# Play 6: sno_deploy_oidc / sno_deploy_certmanager / sno_deploy_delete_kubeadmin
#
# Usage:
# ansible-playbook playbooks/deploy_openshift.yml
# ansible-playbook playbooks/deploy_openshift.yml --tags proxmox
# ansible-playbook playbooks/deploy_openshift.yml --tags sno
# ansible-playbook playbooks/deploy_openshift.yml --tags dns,opnsense
# ansible-playbook playbooks/deploy_openshift.yml --tags opnsense,sno
# ansible-navigator run playbooks/deploy_openshift.yml
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_vm
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_install
# ansible-navigator run playbooks/deploy_openshift.yml --tags opnsense,dns
# ansible-navigator run playbooks/deploy_openshift.yml --tags keycloak,sno_deploy_oidc
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_certmanager
# ---------------------------------------------------------------------------
# Play 1: Create SNO VM in Proxmox
@@ -40,10 +43,13 @@
hosts: sno.openshift.toal.ca
gather_facts: false
connection: local
tags: sno_deploy_vm
roles:
- role: proxmox_sno_vm
tags: proxmox
tasks:
- name: Create VM
ansible.builtin.include_role:
name: sno_deploy
tasks_from: create_vm.yml
# ---------------------------------------------------------------------------
# Play 2: Configure OPNsense - Local DNS Overrides
@@ -115,261 +121,173 @@
record_ttl: "{{ ocp_dns_ttl }}"
# ---------------------------------------------------------------------------
# Play 4: Generate Agent ISO and deploy SNO (agent-based installer)
#
# Uses `openshift-install agent create image` — no SaaS API, no SSO required.
# The pull secret is the only Red Hat credential needed.
# Credentials (kubeconfig, kubeadmin-password) are generated locally under
# sno_install_dir/auth/ by openshift-install itself.
# Play 4: Generate Agent ISO and Deploy SNO
# ---------------------------------------------------------------------------
- name: Generate Agent ISO and Deploy SNO
hosts: sno.openshift.toal.ca
gather_facts: false
connection: local
tags: sno_deploy_install
tasks:
- name: Install SNO
ansible.builtin.include_role:
name: sno_deploy
tasks_from: install.yml
# ---------------------------------------------------------------------------
# Play 5: Configure Keycloak OIDC client for OpenShift
# ---------------------------------------------------------------------------
- name: Configure Keycloak OIDC client for OpenShift
hosts: openshift
gather_facts: false
connection: local
tags: keycloak
vars:
ocp_pull_secret: "{{ vault_ocp_pull_secret }}"
keycloak_context: ""
oidc_client_id: openshift
oidc_redirect_uri: "https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}/oauth2callback/{{ oidc_provider_name }}"
__oidc_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context }}"
tags: sno
module_defaults:
middleware_automation.keycloak.keycloak_realm:
auth_client_id: admin-cli
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
auth_realm: master
auth_username: "{{ keycloak_admin_user }}"
auth_password: "{{ vault_keycloak_admin_password }}"
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
middleware_automation.keycloak.keycloak_client:
auth_client_id: admin-cli
auth_keycloak_url: "{{ __oidc_keycloak_api_url }}"
auth_realm: master
auth_username: "{{ keycloak_admin_user }}"
auth_password: "{{ vault_keycloak_admin_password }}"
validate_certs: "{{ keycloak_validate_certs | default(true) }}"
tasks:
# ------------------------------------------------------------------
# Step 0: Ensure sno_vm_id and sno_mac are populated.
# These are set as cacheable facts by the proxmox_sno_vm role, but
# in ephemeral EEs or when running --tags sno alone the cache is
# empty. Re-query Proxmox whenever either value is missing.
# ------------------------------------------------------------------
- name: Retrieve VM info from Proxmox (needed when fact cache is empty)
community.proxmox.proxmox_vm_info:
api_host: "{{ hostvars['proxmox_api']['ansible_host'] }}"
api_user: "{{ proxmox_api_user }}"
api_port: "{{ hostvars['proxmox_api']['ansible_port'] }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_validate_certs }}"
node: "{{ proxmox_node }}"
name: "{{ sno_vm_name }}"
type: qemu
config: current
register: __sno_vm_info
when: (sno_vm_id | default('')) == '' or (sno_mac | default('')) == ''
- name: Set sno_vm_id and sno_mac from live Proxmox query
- name: Set OIDC client secret (use vault value or generate random)
ansible.builtin.set_fact:
sno_vm_id: "{{ __sno_vm_info.proxmox_vms[0].vmid }}"
sno_mac: >-
{{ __sno_vm_info.proxmox_vms[0].config.net0
| regex_search('([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})', '\1')
| first }}
cacheable: true
when: __sno_vm_info is not skipped
- name: Ensure local install directories exist
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0750"
loop:
- "{{ sno_install_dir }}"
- "{{ sno_install_dir }}/auth"
# ------------------------------------------------------------------
# Step 1: Check whether a fresh ISO already exists on Proxmox
# AND the local openshift-install state dir is intact.
# If the state dir is missing (e.g. /tmp was cleared),
# we must regenerate the ISO so wait-for has valid state.
# ------------------------------------------------------------------
- name: Check if ISO already exists on Proxmox and is less than 24 hours old
ansible.builtin.stat:
path: "{{ proxmox_iso_dir }}/{{ sno_iso_filename }}"
get_checksum: false
delegate_to: proxmox_host
register: __proxmox_iso_stat
- name: Check if local openshift-install state directory exists
ansible.builtin.stat:
path: "{{ sno_install_dir }}/.openshift_install_state"
get_checksum: false
register: __install_state_stat
- name: Set fact - skip ISO build if recent ISO exists on Proxmox and local state is intact
ansible.builtin.set_fact:
__sno_iso_fresh: >-
{{
__proxmox_iso_stat.stat.exists and
(now(utc=true).timestamp() | int - __proxmox_iso_stat.stat.mtime | int) < 86400 and
__install_state_stat.stat.exists
}}
# ------------------------------------------------------------------
# Step 2: Get openshift-install binary
# Always ensure the binary is present — needed for both ISO generation
# and wait-for-install-complete regardless of __sno_iso_fresh.
# Binaries are stored in sno_install_dir so they survive across runs
# when sno_install_dir is a mounted volume in an EE.
# ------------------------------------------------------------------
- name: Download openshift-install tarball
ansible.builtin.get_url:
url: "https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-{{ ocp_version }}/openshift-install-linux.tar.gz"
dest: "{{ sno_install_dir }}/openshift-install-{{ ocp_version }}.tar.gz"
mode: "0644"
checksum: "{{ ocp_install_checksum | default(omit) }}"
register: __ocp_install_tarball
- name: Extract openshift-install binary
ansible.builtin.unarchive:
src: "{{ sno_install_dir }}/openshift-install-{{ ocp_version }}.tar.gz"
dest: "{{ sno_install_dir }}"
remote_src: false
include:
- openshift-install
when: __ocp_install_tarball.changed or not (sno_install_dir ~ '/openshift-install') is file
- name: Download openshift-client tarball
ansible.builtin.get_url:
url: "https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-{{ ocp_version }}/openshift-client-linux.tar.gz"
dest: "{{ sno_install_dir }}/openshift-client-{{ ocp_version }}.tar.gz"
mode: "0644"
checksum: "{{ ocp_client_checksum | default(omit) }}"
register: __ocp_client_tarball
- name: Extract oc binary
ansible.builtin.unarchive:
src: "{{ sno_install_dir }}/openshift-client-{{ ocp_version }}.tar.gz"
dest: "{{ sno_install_dir }}"
remote_src: false
include:
- oc
when: __ocp_client_tarball.changed or not (sno_install_dir ~ '/oc') is file
# ------------------------------------------------------------------
# Step 3: Template agent installer config files (skipped if ISO is fresh)
# ------------------------------------------------------------------
- name: Template install-config.yaml
ansible.builtin.template:
src: templates/install-config.yaml.j2
dest: "{{ sno_install_dir }}/install-config.yaml"
mode: "0640"
when: not __sno_iso_fresh
__oidc_client_secret: "{{ vault_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}"
__oidc_secret_generated: "{{ vault_oidc_client_secret is not defined }}"
no_log: true
- name: Template agent-config.yaml
ansible.builtin.template:
src: templates/agent-config.yaml.j2
dest: "{{ sno_install_dir }}/agent-config.yaml"
mode: "0640"
when: not __sno_iso_fresh
- name: Ensure Keycloak realm exists
middleware_automation.keycloak.keycloak_realm:
realm: "{{ keycloak_realm }}"
id: "{{ keycloak_realm }}"
display_name: "{{ keycloak_realm_display_name | default(keycloak_realm | title) }}"
enabled: true
state: present
no_log: "{{ keycloak_no_log | default(true) }}"
# ------------------------------------------------------------------
# Step 4: Generate discovery ISO (skipped if ISO is fresh)
# Note: openshift-install consumes (moves) the config files into
# openshift-install-state/ — this is expected behaviour.
# ------------------------------------------------------------------
- name: Generate agent-based installer ISO
ansible.builtin.command:
cmd: "{{ sno_install_dir }}/openshift-install agent create image --dir {{ sno_install_dir }}"
when: not __sno_iso_fresh
- name: Create OpenShift OIDC client in Keycloak
middleware_automation.keycloak.keycloak_client:
realm: "{{ keycloak_realm }}"
client_id: "{{ oidc_client_id }}"
name: "OpenShift - {{ ocp_cluster_name }}"
description: "OIDC client for OpenShift cluster {{ ocp_cluster_name }}.{{ ocp_base_domain }}"
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
secret: "{{ __oidc_client_secret }}"
redirect_uris:
- "{{ oidc_redirect_uri }}"
web_origins:
- "+"
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: "{{ keycloak_no_log | default(true) }}"
# ------------------------------------------------------------------
# Step 5: Upload ISO to Proxmox and attach to VM
# ------------------------------------------------------------------
- name: Copy discovery ISO to Proxmox ISO storage
ansible.builtin.copy:
src: "{{ sno_install_dir }}/{{ sno_iso_filename }}"
dest: "{{ proxmox_iso_dir }}/{{ sno_iso_filename }}"
mode: "0644"
delegate_to: proxmox_host
when: not __sno_iso_fresh
- name: Attach ISO to VM as CDROM
ansible.builtin.command:
cmd: "qm set {{ sno_vm_id }} --ide2 {{ proxmox_iso_storage }}:iso/{{ sno_iso_filename }},media=cdrom"
delegate_to: proxmox_host
changed_when: true
- name: Ensure boot order prefers disk, falls back to CDROM
# order=scsi0;ide2: OVMF tries scsi0 first; on first boot the disk has
# no EFI application so OVMF falls through to ide2 (the agent ISO).
# After RHCOS writes its EFI entry to the disk, subsequent reboots boot
# directly from scsi0 — the CDROM is never tried again, breaking the loop.
ansible.builtin.command:
cmd: "qm set {{ sno_vm_id }} --boot order=scsi0;ide2"
delegate_to: proxmox_host
changed_when: true
# ------------------------------------------------------------------
# Step 6: Boot the VM
# ------------------------------------------------------------------
- name: Start SNO VM
community.proxmox.proxmox_kvm:
api_host: "{{ hostvars['proxmox_api']['ansible_host'] }}"
api_user: "{{ proxmox_api_user }}"
api_port: "{{ hostvars['proxmox_api']['ansible_port'] }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_validate_certs }}"
node: "{{ proxmox_node }}"
name: "{{ sno_vm_name }}"
state: started
# ------------------------------------------------------------------
# Step 7: Persist credentials to Proxmox host
# The EE is ephemeral — copy auth files to a durable location before
# the container exits. sno_credentials_dir defaults to
# /root/sno-<cluster_name> on proxmox_host.
# ------------------------------------------------------------------
- name: Create credentials directory on Proxmox host
ansible.builtin.file:
path: "{{ sno_credentials_dir }}"
state: directory
mode: "0700"
delegate_to: proxmox_host
- name: Copy kubeconfig to Proxmox host
ansible.builtin.copy:
src: "{{ sno_install_dir }}/auth/kubeconfig"
dest: "{{ sno_credentials_dir }}/kubeconfig"
mode: "0600"
delegate_to: proxmox_host
- name: Copy kubeadmin-password to Proxmox host
ansible.builtin.copy:
src: "{{ sno_install_dir }}/auth/kubeadmin-password"
dest: "{{ sno_credentials_dir }}/kubeadmin-password"
mode: "0600"
delegate_to: proxmox_host
# ------------------------------------------------------------------
# Step 8: Wait for installation to complete (~60-90 min)
# Credentials land in sno_install_dir/auth/ automatically.
# Inline poll (poll: 30) is used rather than fire-and-forget async
# because the connection is local — no SSH timeout risk — and the
# poll: 0 + async_status pattern stores job state in ~/.ansible_async
# inside the EE container, which is lost if the EE is restarted.
# Ensure your job/EE timeout is set to at least 6000 s (100 min).
# ------------------------------------------------------------------
- name: Wait for SNO installation to complete
ansible.builtin.command:
cmd: "{{ sno_install_dir }}/openshift-install agent wait-for install-complete --dir {{ sno_install_dir }} --log-level=info"
async: 5400
poll: 30
# ------------------------------------------------------------------
# Step 9: Eject CDROM so the VM never boots the agent ISO again
# ------------------------------------------------------------------
- name: Eject CDROM after successful installation
ansible.builtin.command:
cmd: "qm set {{ sno_vm_id }} --ide2 none,media=cdrom"
delegate_to: proxmox_host
changed_when: true
- name: Display post-install info
- name: Display generated client secret (save this to vault!)
ansible.builtin.debug:
msg:
- "SNO installation complete!"
- "API URL : https://api.{{ ocp_cluster_name }}.{{ ocp_base_domain }}:6443"
- "Console : https://console-openshift-console.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}"
- "Kubeconfig : {{ sno_credentials_dir }}/kubeconfig (on proxmox_host)"
- "kubeadmin pass : {{ sno_credentials_dir }}/kubeadmin-password (on proxmox_host)"
- "*** GENERATED OIDC CLIENT SECRET — SAVE THIS TO VAULT ***"
- "vault_oidc_client_secret: {{ __oidc_client_secret }}"
- ""
- "Set this in host_vars or pass as --extra-vars on future runs."
when: __oidc_secret_generated | bool
- name: Display Keycloak configuration summary
ansible.builtin.debug:
msg:
- "Keycloak OIDC client configured:"
- " Realm : {{ keycloak_realm }}"
- " Client : {{ oidc_client_id }}"
- " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}"
- " Redirect: {{ oidc_redirect_uri }}"
verbosity: 1
# ---------------------------------------------------------------------------
# Play 6: Post-install OpenShift configuration
# Configure OIDC OAuth, cert-manager, and delete kubeadmin
# ---------------------------------------------------------------------------
- name: Configure OpenShift post-install
hosts: sno.openshift.toal.ca
gather_facts: false
connection: local
environment:
KUBECONFIG: "{{ sno_install_dir }}/auth/kubeconfig"
K8S_AUTH_VERIFY_SSL: "false"
tags:
- sno_deploy_oidc
- sno_deploy_certmanager
- sno_deploy_delete_kubeadmin
tasks:
- name: Configure OpenShift OAuth with OIDC
ansible.builtin.include_role:
name: sno_deploy
tasks_from: configure_oidc.yml
tags: sno_deploy_oidc
- name: Configure cert-manager and LetsEncrypt certificates
ansible.builtin.include_role:
name: sno_deploy
tasks_from: configure_certmanager.yml
tags: sno_deploy_certmanager
- name: Delete kubeadmin user
ansible.builtin.include_role:
name: sno_deploy
tasks_from: delete_kubeadmin.yml
tags:
- never
- sno_deploy_delete_kubeadmin
# ---------------------------------------------------------------------------
# Play 7: Install Ansible Automation Platform (opt-in via --tags aap)
# ---------------------------------------------------------------------------
- name: Install Ansible Automation Platform
hosts: sno.openshift.toal.ca
gather_facts: false
connection: local
environment:
KUBECONFIG: "{{ sno_install_dir }}/auth/kubeconfig"
K8S_AUTH_VERIFY_SSL: "false"
tags:
- never
- aap
roles:
- role: aap_operator

View File

@@ -1,34 +0,0 @@
---
# Generated by Ansible — do not edit by hand
# Source: playbooks/templates/agent-config.yaml.j2
apiVersion: v1alpha1
kind: AgentConfig
metadata:
name: {{ ocp_cluster_name }}
rendezvousIP: {{ sno_ip }}
hosts:
- hostname: master-0
interfaces:
- name: primary
macAddress: "{{ sno_mac }}"
networkConfig:
interfaces:
- name: primary
type: ethernet
state: up
mac-address: "{{ sno_mac }}"
ipv4:
enabled: true
address:
- ip: {{ sno_ip }}
prefix-length: {{ sno_prefix_length }}
dhcp: false
dns-resolver:
config:
server:
- {{ sno_nameserver }}
routes:
config:
- destination: 0.0.0.0/0
next-hop-address: {{ sno_gateway }}
next-hop-interface: primary

View File

@@ -1,27 +0,0 @@
---
# Generated by Ansible — do not edit by hand
# Source: playbooks/templates/install-config.yaml.j2
apiVersion: v1
baseDomain: {{ ocp_base_domain }}
metadata:
name: {{ ocp_cluster_name }}
networking:
networkType: OVNKubernetes
machineNetwork:
- cidr: {{ sno_machine_network }}
clusterNetwork:
- cidr: 10.128.0.0/14
hostPrefix: 23
serviceNetwork:
- 172.30.0.0/16
compute:
- name: worker
replicas: 0
controlPlane:
name: master
replicas: 1
platform:
none: {}
pullSecret: |
{{ ocp_pull_secret | ansible.builtin.to_json }}
sshKey: "{{ ocp_ssh_public_key }}"