present.yml 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. ---
  2. # Deploys RHBK in a namespace with the Keycloak operator CSV.
  3. - name: Check if there is a namespace.
  4. kubernetes.core.k8s_info:
  5. kubeconfig: tmp/kubeconfig-ocp4
  6. validate_certs: no
  7. api_version: v1
  8. kind: namespace
  9. name: "{{ rhbk.namespace | default('keycloak') }}"
  10. register: prereq_ns
  11. - name: Fail if not so.
  12. ansible.builtin.assert:
  13. that:
  14. - prereq_ns.resources is defined
  15. - prereq_ns.resources | length == 1
  16. success_msg: "OK, namespace found."
  17. fail_msg: "FATAL: namespace to deploy ({{ rhbk.namespace | default('keycloak') }}) not found. Ensure there is an operator already present."
  18. # TODO: figure this out. probably look at subscription's CSV status attribute for name or something.
  19. - name: Check if there is a CSV in the namespace.
  20. kubernetes.core.k8s_info:
  21. kubeconfig: tmp/kubeconfig-ocp4
  22. validate_certs: no
  23. api_version: operators.coreos.com/v1alpha1
  24. kind: clusterserviceversion
  25. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  26. register: prereq_csv
  27. # TODO: figure this out - what is the CSV name, how do you find the operator manifest?
  28. - name: Fail if not so.
  29. ansible.builtin.assert:
  30. that:
  31. - prereq_csv.resources is defined
  32. - prereq_csv.resources | length > 0
  33. success_msg: "OK, operator CSV found."
  34. fail_msg: "FATAL: Operator is not deployed in the namespace: {{ rhbk.namespace | default('keycloak') }}. Ensure there is an operator already present."
  35. - name: Tech hack. Prevent anything from blowing up because rhbk is defined somewhere, but not its structured contents.
  36. ansible.builtin.set_fact:
  37. rhbk: "{{ rhbk | default({}) | combine({ 'db': {} }) }}"
  38. when:
  39. - rhbk.db is not defined
  40. - name: Ensure there is a secret containing DB credentials in the project.
  41. kubernetes.core.k8s:
  42. kubeconfig: tmp/kubeconfig-ocp4
  43. validate_certs: no
  44. api_version: v1
  45. kind: secret
  46. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  47. name: "{{ rhbk.name | default('sso') }}-db-auth"
  48. resource_definition:
  49. data:
  50. username: "{{ rhbk.db.username | default('rhbk') | b64encode }}"
  51. password: "{{ rhbk.db.password | default('secret') | b64encode }}"
  52. # TODO: ensure that there is no STS to begin with, or if there is, verify that
  53. # only the allowed fields would change, if anything. Otherwise:
  54. #
  55. # Forbidden: updates to statefulset spec for fields other than
  56. # 'replicas', 'ordinals', 'template', 'updateStrategy',
  57. # 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are
  58. # forbidden
  59. #
  60. - name: Ensure there is a database sts in the project
  61. kubernetes.core.k8s:
  62. kubeconfig: tmp/kubeconfig-ocp4
  63. validate_certs: no
  64. api_version: apps/v1
  65. kind: statefulset
  66. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  67. name: "{{ rhbk.name | default('sso') }}-db"
  68. resource_definition:
  69. spec:
  70. # TODO: implement rhbk.db.replicas at some point?
  71. replicas: 1
  72. serviceName: "{{ rhbk.name | default('sso') }}-db"
  73. selector:
  74. matchLabels:
  75. app: "{{ rhbk.name | default('sso') }}-db"
  76. template:
  77. metadata:
  78. labels:
  79. app: "{{ rhbk.name | default('sso') }}-db"
  80. spec:
  81. containers:
  82. - name: "{{ rhbk.name | default('sso') }}-db"
  83. image: "{{ rhbk.db.image | default('registry.redhat.io/rhel9/postgresql-15:latest') }}"
  84. volumeMounts:
  85. - mountPath: /var/lib/pgsql/data
  86. name: "{{ rhbk.name | default('sso') }}-db-data"
  87. env:
  88. - name: POSTGRESQL_USER
  89. valueFrom:
  90. secretKeyRef:
  91. name: "{{ rhbk.name | default('sso') }}-db-auth"
  92. key: username
  93. - name: POSTGRESQL_PASSWORD
  94. valueFrom:
  95. secretKeyRef:
  96. name: "{{ rhbk.name | default('sso') }}-db-auth"
  97. key: password
  98. - name: POSTGRESQL_DATABASE
  99. value: "{{ rhbk.db.name | default('rhbk') }}"
  100. volumeClaimTemplates:
  101. - metadata:
  102. name: "{{ rhbk.name | default('sso') }}-db-data"
  103. spec:
  104. accessModes: "{{ rhbk.db.claim_modes | default(['ReadWriteOnce']) }}"
  105. storageClassName: "{{ rhbk.db.storage_class | default(omit) }}"
  106. resources:
  107. requests:
  108. storage: "{{ rhbk.db.size | default('1Gi') }}"
  109. - name: Ensure there is a service for the database as well
  110. kubernetes.core.k8s:
  111. kubeconfig: tmp/kubeconfig-ocp4
  112. validate_certs: no
  113. api_version: v1
  114. kind: service
  115. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  116. name: "{{ rhbk.name | default('sso') }}-db"
  117. resource_definition:
  118. spec:
  119. selector:
  120. app: "{{ rhbk.name | default('sso') }}-db"
  121. type: LoadBalancer
  122. ports:
  123. - port: 5432
  124. targetPort: 5432
  125. protocol: TCP
  126. - name: Ensure there is a secret containing Keycloak bootstrap credentials
  127. kubernetes.core.k8s:
  128. kubeconfig: tmp/kubeconfig-ocp4
  129. validate_certs: no
  130. api_version: v1
  131. kind: secret
  132. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  133. name: "{{ rhbk.name | default('sso') }}-auth"
  134. resource_definition:
  135. data:
  136. username: "{{ rhbk.admin.username | default('rhbk') | b64encode }}"
  137. password: "{{ rhbk.admin.password | default('secret') | b64encode }}"
  138. - name: If there is no FQDN, check what the default domain is.
  139. kubernetes.core.k8s_info:
  140. kubeconfig: tmp/kubeconfig-ocp4
  141. validate_certs: no
  142. api_version: operator.openshift.io/v1
  143. kind: ingresscontroller
  144. namespace: openshift-ingress-operator
  145. name: default
  146. register: default_ingress
  147. when: rhbk.fqdn is not defined
  148. - name: Set a fact that reflects either the FQDN as set, or a composition of vars and default ingress info.
  149. ansible.builtin.set_fact:
  150. rhbk_fqdn: "{{ rhbk.fqdn | default((rhbk.name | default('sso')) + '-' + (rhbk.namespace | default('keycloak')) + '.' + default_ingress.resources[0].status.domain) }}"
  151. - name: Announce what hostname would be used.
  152. ansible.builtin.debug:
  153. msg: Using "https://{{ rhbk_fqdn }}" as the hostname.
  154. # TODO: remember if there were changes, and force delete any non-ready pod?
  155. - name: Lastly, make sure there is a Keycloak
  156. kubernetes.core.k8s:
  157. kubeconfig: tmp/kubeconfig-ocp4
  158. validate_certs: no
  159. api_version: k8s.keycloak.org/v2alpha1
  160. kind: keycloak
  161. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  162. name: "{{ rhbk.name | default('sso') }}"
  163. resource_definition:
  164. spec:
  165. instances: "{{ rhbk.replicas | default(1) }}"
  166. db:
  167. vendor: postgres
  168. host: "{{ rhbk.name | default('sso') }}-db"
  169. database: "{{ rhbk.db.name | default('rhbk') }}"
  170. usernameSecret:
  171. name: "{{ rhbk.name | default('sso') }}-db-auth"
  172. key: username
  173. passwordSecret:
  174. name: "{{ rhbk.name | default('sso') }}-db-auth"
  175. key: password
  176. hostname:
  177. hostname: "https://{{ rhbk_fqdn }}"
  178. strict: false
  179. backchannelDynamic: true
  180. http:
  181. httpEnabled: true
  182. httpPort: 8080
  183. httpsPort: 8443
  184. tlsSecret: "{{ rhbk.name | default('sso') }}-tls"
  185. bootstrapAdmin:
  186. user:
  187. secret: "{{ rhbk.name | default('sso') }}-auth"
  188. ingress:
  189. enabled: false
  190. - name: Wait for the service to show up.
  191. kubernetes.core.k8s_info:
  192. kubeconfig: tmp/kubeconfig-ocp4
  193. validate_certs: no
  194. api_version: v1
  195. kind: service
  196. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  197. name: "{{ rhbk.name | default('sso') }}-service"
  198. register: rhbk_svc
  199. until:
  200. - rhbk_svc.resources is defined
  201. - (rhbk_svc.resources | length) > 0
  202. retries: 24
  203. delay: 5
  204. - name: Ensure the service is correctly annotated.
  205. kubernetes.core.k8s:
  206. kubeconfig: tmp/kubeconfig-ocp4
  207. validate_certs: no
  208. api_version: v1
  209. kind: service
  210. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  211. name: "{{ rhbk.name | default('sso') }}-service"
  212. state: patched
  213. resource_definition:
  214. metadata:
  215. annotations:
  216. service.beta.openshift.io/serving-cert-secret-name: "{{ rhbk.name | default('sso') }}-tls"
  217. - name: Make sure there is a re-encrypt route.
  218. kubernetes.core.k8s:
  219. kubeconfig: tmp/kubeconfig-ocp4
  220. validate_certs: no
  221. api_version: route.openshift.io/v1
  222. kind: route
  223. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  224. name: "{{ rhbk.name | default('sso') }}"
  225. resource_definition:
  226. spec:
  227. to:
  228. kind: Service
  229. name: "{{ rhbk.name | default('sso') }}-service"
  230. port:
  231. targetPort: 8443
  232. tls:
  233. termination: reencrypt
  234. insecureEdgeTerminationPolicy: Redirect
  235. - name: Wait for the Keycloak service to report ready.
  236. kubernetes.core.k8s_info:
  237. kubeconfig: tmp/kubeconfig-ocp4
  238. validate_certs: no
  239. api_version: k8s.keycloak.org/v2alpha1
  240. kind: keycloak
  241. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  242. name: "{{ rhbk.name | default('sso') }}"
  243. register: rhbk_ready
  244. until:
  245. - rhbk_ready.resources is defined
  246. - rhbk_ready.resources | length == 1
  247. - rhbk_ready.resources[0].status is defined
  248. - (rhbk_ready.resources[0].status | community.general.json_query('conditions[?type==`Ready`].status'))[0]
  249. retries: 24
  250. delay: 5
  251. - name: Get a fresh bearer token.
  252. ansible.builtin.include_tasks:
  253. file: tasks/token.yml
  254. - name: Get a list of existing realms.
  255. ansible.builtin.uri:
  256. method: GET
  257. return_content: true
  258. validate_certs: false
  259. url: "https://{{ rhbk_fqdn }}/admin/realms"
  260. headers:
  261. Authorization: Bearer {{ admin_token }}
  262. Accept: application/json
  263. register: rhbk_realms
  264. - name: Store the list of realm names/ids as a fact
  265. ansible.builtin.set_fact:
  266. realms: "{{ rhbk_realms.json | items2dict(key_name='realm', value_name='id') }}"
  267. - name: Import the realm if not present yet
  268. block:
  269. - name: Check whether there is already a realm import CR
  270. kubernetes.core.k8s_info:
  271. kubeconfig: tmp/kubeconfig-ocp4
  272. validate_certs: no
  273. api_version: k8s.keycloak.org/v2alpha1
  274. kind: keycloakrealmimport
  275. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  276. register: realm_imports
  277. - name: Remove a previous realm import if it happens to be there.
  278. kubernetes.core.k8s:
  279. kubeconfig: tmp/kubeconfig-ocp4
  280. validate_certs: no
  281. api_version: k8s.keycloak.org/v2alpha1
  282. kind: keycloakrealmimport
  283. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  284. name: "{{ rhbk.name | default('sso') }}-{{ rhbk.realm | default('sample-realm') }}-import"
  285. state: absent
  286. when:
  287. - realm_imports.resources is defined
  288. - (realm_imports.resources | length) > 0
  289. - (realm_imports | community.general.json_query('resources[*].metadata.name')) is contains((rhbk.name | default('sso')) + '-' + (rhbk.realm | default('sample-realm')) + '-import')
  290. # TODO: finish templating this one, introduce more settings as needed
  291. - name: Apply a template realm import.
  292. kubernetes.core.k8s:
  293. kubeconfig: tmp/kubeconfig-ocp4
  294. validate_certs: no
  295. api_version: k8s.keycloak.org/v2alpha1
  296. kind: keycloakrealmimport
  297. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  298. name: "{{ rhbk.name | default('sso') }}-{{ rhbk.realm | default('sample-realm') }}-import"
  299. template: templates/realm-import-template.yaml.j2
  300. register: created_import
  301. - name: Wait for the import to rollout.
  302. kubernetes.core.k8s_info:
  303. kubeconfig: tmp/kubeconfig-ocp4
  304. validate_certs: no
  305. api_version: v1
  306. kind: pod
  307. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  308. label_selectors:
  309. - app=keycloak-realm-import
  310. register: import_running
  311. until:
  312. - import_running.resources is defined
  313. - import_running.resources | length > 0
  314. - import_running.resources[0].status.phase == "Succeeded"
  315. retries: 12
  316. delay: 5
  317. when: created_import.changed
  318. - name: Wait for the Keycloak pod to restart.
  319. kubernetes.core.k8s_info:
  320. kubeconfig: tmp/kubeconfig-ocp4
  321. validate_certs: no
  322. api_version: v1
  323. kind: pod
  324. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  325. label_selectors:
  326. - app=keycloak
  327. register: rhbk_running
  328. until:
  329. - rhbk_running.resources is defined
  330. - rhbk_running.resources | length > 0
  331. - rhbk_running.resources[0].status.phase == "Running"
  332. - (rhbk_running.resources[0].status | community.general.json_query('conditions[?type=="Ready"].status'))[0]
  333. when: created_import.changed
  334. when:
  335. - realms[rhbk.realm | default('sample-realm')] is not defined
  336. - name: Get a fresh bearer token.
  337. ansible.builtin.include_tasks:
  338. file: tasks/token.yml
  339. - name: Get a list of existing groups in the realm.
  340. ansible.builtin.uri:
  341. method: GET
  342. return_content: true
  343. validate_certs: false
  344. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/groups"
  345. headers:
  346. Authorization: Bearer {{ admin_token }}
  347. Accept: application/json
  348. register: rhbk_realm_groups
  349. - name: Show what groups were found at verbosity 2+.
  350. ansible.builtin.debug:
  351. var: rhbk_realm_groups
  352. verbosity: 2
  353. - name: Create the groups if necessary.
  354. ansible.builtin.uri:
  355. method: POST
  356. return_content: true
  357. validate_certs: false
  358. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/groups"
  359. headers:
  360. Authorization: Bearer {{ admin_token }}
  361. Accept: application/json
  362. Content-Type: application/json
  363. body_format: json
  364. body: |
  365. {
  366. "name": "{{ item }}"
  367. }
  368. status_code:
  369. - 200
  370. - 201
  371. register: created_groups
  372. loop: "{{ rhbk.groups }}"
  373. when:
  374. - (rhbk_realm_groups.json | items2dict(key_name='name', value_name='id')).keys() is not contains(item)
  375. - name: Show what groups were created at verbosity 2+.
  376. ansible.builtin.debug:
  377. var: created_groups
  378. verbosity: 2
  379. - name: Get a fresh bearer token.
  380. ansible.builtin.include_tasks:
  381. file: tasks/token.yml
  382. - name: Get a list of existing users in the realm.
  383. ansible.builtin.uri:
  384. method: GET
  385. return_content: true
  386. validate_certs: false
  387. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/users"
  388. headers:
  389. Authorization: Bearer {{ admin_token }}
  390. Accept: application/json
  391. register: rhbk_realm_users
  392. - name: Show what users were found at verbosity 2+.
  393. ansible.builtin.debug:
  394. var: rhbk_realm_users
  395. verbosity: 2
  396. - name: Create the users if necessary.
  397. ansible.builtin.uri:
  398. method: POST
  399. return_content: true
  400. validate_certs: false
  401. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/users"
  402. headers:
  403. Authorization: Bearer {{ admin_token }}
  404. Accept: application/json
  405. Content-Type: application/json
  406. body_format: json
  407. body: |
  408. {
  409. "username": "{{ item.username }}",
  410. "email": "{{ item.email | default(item.username + '@example.com') }}",
  411. "firstName": "{{ item.firstname | default('') }}",
  412. "lastName": "{{ item.lastname | default('') }}",
  413. "credentials": [
  414. {
  415. "type": "password",
  416. "temporary": false,
  417. "value": "{{ item.password | default('secret') }}"
  418. }
  419. ],
  420. "enabled": true,
  421. "emailVerified": true,
  422. {% if item.groups is defined and (item.groups | length) > 0 %}
  423. "groups": [ "{{ item.groups | join('", "') }}" ]
  424. {% endif %}
  425. }
  426. status_code:
  427. - 200
  428. - 201
  429. register: created_users
  430. loop: "{{ rhbk.users }}"
  431. when:
  432. - (rhbk_realm_users.json | items2dict(key_name='username', value_name='id')).keys() is not contains(item.username)
  433. - name: Show what users were created at verbosity 2+.
  434. ansible.builtin.debug:
  435. var: created_users
  436. verbosity: 2
  437. ...