present.yml 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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 resource 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: Wait for the Keycloak pod to become ready.
  252. ansible.builtin.uri:
  253. return_content: yes
  254. validate_certs: no
  255. url: "https://{{ rhbk_fqdn }}/realms/master"
  256. register: rhbk_is_ready
  257. until: rhbk_is_ready.status == 200
  258. retries: 24
  259. delay: 5
  260. - name: Get a fresh bearer token.
  261. ansible.builtin.include_tasks:
  262. file: tasks/token.yml
  263. - name: Get a list of existing realms.
  264. ansible.builtin.uri:
  265. method: GET
  266. return_content: true
  267. validate_certs: false
  268. url: "https://{{ rhbk_fqdn }}/admin/realms"
  269. headers:
  270. Authorization: Bearer {{ admin_token }}
  271. Accept: application/json
  272. register: rhbk_realms
  273. - name: Store the list of realm names/ids as a fact
  274. ansible.builtin.set_fact:
  275. realms: "{{ rhbk_realms.json | items2dict(key_name='realm', value_name='id') }}"
  276. - name: Import the realm if not present yet
  277. block:
  278. - name: Check whether there is already a realm import CR
  279. kubernetes.core.k8s_info:
  280. kubeconfig: tmp/kubeconfig-ocp4
  281. validate_certs: no
  282. api_version: k8s.keycloak.org/v2alpha1
  283. kind: keycloakrealmimport
  284. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  285. register: realm_imports
  286. - name: Remove a previous realm import if it happens to be there.
  287. kubernetes.core.k8s:
  288. kubeconfig: tmp/kubeconfig-ocp4
  289. validate_certs: no
  290. api_version: k8s.keycloak.org/v2alpha1
  291. kind: keycloakrealmimport
  292. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  293. name: "{{ rhbk.name | default('sso') }}-{{ rhbk.realm | default('sample-realm') }}-import"
  294. state: absent
  295. when:
  296. - realm_imports.resources is defined
  297. - (realm_imports.resources | length) > 0
  298. - (realm_imports | community.general.json_query('resources[*].metadata.name')) is contains((rhbk.name | default('sso')) + '-' + (rhbk.realm | default('sample-realm')) + '-import')
  299. # TODO: finish templating this one, introduce more settings as needed
  300. - name: Apply a template realm import.
  301. kubernetes.core.k8s:
  302. kubeconfig: tmp/kubeconfig-ocp4
  303. validate_certs: no
  304. api_version: k8s.keycloak.org/v2alpha1
  305. kind: keycloakrealmimport
  306. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  307. name: "{{ rhbk.name | default('sso') }}-{{ rhbk.realm | default('sample-realm') }}-import"
  308. template: templates/realm-import-template.yaml.j2
  309. register: created_import
  310. - name: Wait for the import to rollout.
  311. kubernetes.core.k8s_info:
  312. kubeconfig: tmp/kubeconfig-ocp4
  313. validate_certs: no
  314. api_version: v1
  315. kind: pod
  316. namespace: "{{ rhbk.namespace | default('keycloak') }}"
  317. label_selectors:
  318. - app=keycloak-realm-import
  319. register: import_running
  320. until:
  321. - import_running.resources is defined
  322. - import_running.resources | length > 0
  323. - import_running.resources[0].status.phase == "Succeeded"
  324. retries: 12
  325. delay: 5
  326. when: created_import.changed
  327. - name: Wait for the Keycloak pod to become ready.
  328. ansible.builtin.uri:
  329. return_content: yes
  330. validate_certs: no
  331. url: "https://{{ rhbk_fqdn }}/realms/{{ rhbk.realm | default('sample-realm') }}"
  332. register: rhbk_is_back
  333. until: rhbk_is_back.status == 200
  334. retries: 24
  335. delay: 5
  336. when: created_import.changed
  337. when:
  338. - realms[rhbk.realm | default('sample-realm')] is not defined
  339. - name: Get a fresh bearer token.
  340. ansible.builtin.include_tasks:
  341. file: tasks/token.yml
  342. - name: Get a list of existing groups in the realm.
  343. ansible.builtin.uri:
  344. method: GET
  345. return_content: true
  346. validate_certs: false
  347. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/groups"
  348. headers:
  349. Authorization: Bearer {{ admin_token }}
  350. Accept: application/json
  351. register: rhbk_realm_groups
  352. - name: Show what groups were found at verbosity 2+.
  353. ansible.builtin.debug:
  354. var: rhbk_realm_groups
  355. verbosity: 2
  356. - name: Create the groups if necessary.
  357. ansible.builtin.uri:
  358. method: POST
  359. return_content: true
  360. validate_certs: false
  361. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/groups"
  362. headers:
  363. Authorization: Bearer {{ admin_token }}
  364. Accept: application/json
  365. Content-Type: application/json
  366. body_format: json
  367. body: |
  368. {
  369. "name": "{{ item }}"
  370. }
  371. status_code:
  372. - 200
  373. - 201
  374. register: created_groups
  375. loop: "{{ rhbk.groups }}"
  376. when:
  377. - (rhbk_realm_groups.json | items2dict(key_name='name', value_name='id')).keys() is not contains(item)
  378. - name: Show what groups were created at verbosity 2+.
  379. ansible.builtin.debug:
  380. var: created_groups
  381. verbosity: 2
  382. - name: Get a fresh bearer token.
  383. ansible.builtin.include_tasks:
  384. file: tasks/token.yml
  385. - name: Get a list of existing users in the realm.
  386. ansible.builtin.uri:
  387. method: GET
  388. return_content: true
  389. validate_certs: false
  390. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/users"
  391. headers:
  392. Authorization: Bearer {{ admin_token }}
  393. Accept: application/json
  394. register: rhbk_realm_users
  395. - name: Show what users were found at verbosity 2+.
  396. ansible.builtin.debug:
  397. var: rhbk_realm_users
  398. verbosity: 2
  399. - name: Create the users if necessary.
  400. ansible.builtin.uri:
  401. method: POST
  402. return_content: true
  403. validate_certs: false
  404. url: "https://{{ rhbk_fqdn }}/admin/realms/{{ rhbk.realm | default('sample-realm') }}/users"
  405. headers:
  406. Authorization: Bearer {{ admin_token }}
  407. Accept: application/json
  408. Content-Type: application/json
  409. body_format: json
  410. body: |
  411. {
  412. "username": "{{ item.username }}",
  413. "email": "{{ item.email | default(item.username + '@example.com') }}",
  414. "firstName": "{{ item.firstname | default('') }}",
  415. "lastName": "{{ item.lastname | default('') }}",
  416. "credentials": [
  417. {
  418. "type": "password",
  419. "temporary": false,
  420. "value": "{{ item.password | default('secret') }}"
  421. }
  422. ],
  423. "enabled": true,
  424. "emailVerified": true,
  425. {% if item.groups is defined and (item.groups | length) > 0 %}
  426. "groups": [ "{{ item.groups | join('", "') }}" ]
  427. {% endif %}
  428. }
  429. status_code:
  430. - 200
  431. - 201
  432. register: created_users
  433. loop: "{{ rhbk.users }}"
  434. when:
  435. - (rhbk_realm_users.json | items2dict(key_name='username', value_name='id')).keys() is not contains(item.username)
  436. - name: Show what users were created at verbosity 2+.
  437. ansible.builtin.debug:
  438. var: created_users
  439. verbosity: 2
  440. ...