mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-03 19:58:14 +00:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			c6f49dc6e2
			...
			b6ee7b9f98
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b6ee7b9f98 | |||
| 67122800f3 | |||
| bfd1a2ee70 | |||
| 076a2058cc | |||
| 9dc55c5893 | |||
| 81ef808191 | |||
| 8161dd1b6d | |||
| ac72544b72 | |||
| 732607bbb6 | 
@@ -6,7 +6,6 @@ import os
 | 
			
		||||
import datetime
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_ansible_playbook(
 | 
			
		||||
    inventory,
 | 
			
		||||
    modes,
 | 
			
		||||
@@ -81,6 +80,24 @@ def run_ansible_playbook(
 | 
			
		||||
    duration = end_time - start_time
 | 
			
		||||
    print(f"⏱️ Total execution time: {duration}\n")
 | 
			
		||||
 | 
			
		||||
def validate_application_ids(inventory, app_ids):
 | 
			
		||||
    """
 | 
			
		||||
    Abort the script if any application IDs are invalid, with detailed reasons.
 | 
			
		||||
    """
 | 
			
		||||
    from utils.valid_deploy_id import ValidDeployId
 | 
			
		||||
    validator = ValidDeployId()
 | 
			
		||||
    invalid = validator.validate(inventory, app_ids)
 | 
			
		||||
    if invalid:
 | 
			
		||||
        print("\n❌ Detected invalid application_id(s):\n")
 | 
			
		||||
        for app_id, status in invalid.items():
 | 
			
		||||
            reasons = []
 | 
			
		||||
            if not status['in_roles']:
 | 
			
		||||
                reasons.append("not defined in roles (cymais)")
 | 
			
		||||
            if not status['in_inventory']:
 | 
			
		||||
                reasons.append("not found in inventory file")
 | 
			
		||||
            print(f"  - {app_id}: " + ", ".join(reasons))
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
@@ -150,6 +167,7 @@ def main():
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
    validate_application_ids(args.inventory, args.id)
 | 
			
		||||
 | 
			
		||||
    modes = {
 | 
			
		||||
        "mode_reset": args.reset,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
database_id:        "svc-db-{{ database_type }}"
 | 
			
		||||
database_instance:  "{{ applications | get_app_conf(database_application_id, 'hostname', false) if applications | get_app_conf(database_application_id, 'features.central_database', False) else database_application_id }}"
 | 
			
		||||
database_host:      "{{ applications | get_app_conf(database_application_id, 'hostname', false) if applications | get_app_conf(database_application_id, 'features.central_database', False) else 'database' }}"
 | 
			
		||||
database_instance:  "{{ applications | get_app_conf(database_id, 'hostname', True) if applications | get_app_conf(database_application_id, 'features.central_database', False) else database_application_id }}"
 | 
			
		||||
database_host:      "{{ applications | get_app_conf(database_id, 'hostname', True) if applications | get_app_conf(database_application_id, 'features.central_database', False) else 'database' }}"
 | 
			
		||||
database_name:      "{{ applications | get_app_conf(database_application_id, 'database.name', false, database_application_id ) }}"    # The overwritte configuration is needed by bigbluebutton
 | 
			
		||||
database_username:  "{{ applications | get_app_conf(database_application_id, 'database.username', false, database_application_id )}}" # The overwritte configuration is needed by bigbluebutton
 | 
			
		||||
database_password:  "{{ applications | get_app_conf(database_application_id, 'credentials.database_password', true) }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,11 @@ networks:
 | 
			
		||||
  %}
 | 
			
		||||
  svc-db-openldap:
 | 
			
		||||
    external: true
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% else %}
 | 
			
		||||
  default:
 | 
			
		||||
{% if
 | 
			
		||||
  application_id in networks.local and 
 | 
			
		||||
  networks.local[application_id].subnet is defined and
 | 
			
		||||
  application_id != 'svc-db-openldap'
 | 
			
		||||
  networks.local[application_id].subnet is defined
 | 
			
		||||
%}
 | 
			
		||||
    name: {{ application_id }}
 | 
			
		||||
    driver: bridge
 | 
			
		||||
@@ -25,4 +24,5 @@ networks:
 | 
			
		||||
      config:
 | 
			
		||||
        - subnet: {{networks.local[application_id].subnet}}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{{ "\n" }}
 | 
			
		||||
@@ -1,4 +1,8 @@
 | 
			
		||||
version:  "latest"
 | 
			
		||||
hostname: "svc-db-mariadb"
 | 
			
		||||
network:  "<< defaults_applications[svc-db-mariadb].hostname >>"
 | 
			
		||||
volume:   "<< defaults_applications[svc-db-mariadb].hostname >>_data"
 | 
			
		||||
docker: 
 | 
			
		||||
  services:
 | 
			
		||||
    mariadb: 
 | 
			
		||||
      version:  "latest"
 | 
			
		||||
      image:    "mariadb"
 | 
			
		||||
      volume:   "<< defaults_applications[svc-db-mariadb].hostname >>_data"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								roles/svc-db-mariadb/tasks/init.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								roles/svc-db-mariadb/tasks/init.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
- name: "Create database: {{ database_name }}"
 | 
			
		||||
  mysql_db:
 | 
			
		||||
    name:           "{{ database_name }}"
 | 
			
		||||
    state:          present
 | 
			
		||||
    login_user:     root
 | 
			
		||||
    login_password: "{{ mariadb_root_pwd }}"
 | 
			
		||||
    login_host:     127.0.0.1
 | 
			
		||||
    login_port:     "{{ database_port }}"
 | 
			
		||||
    encoding:       "{{ database_encoding }}"
 | 
			
		||||
    collation:      "{{ database_collation }}"
 | 
			
		||||
 | 
			
		||||
- name: "Create database user: {{ database_username }}"
 | 
			
		||||
  mysql_user:
 | 
			
		||||
    name:     "{{database_username}}"
 | 
			
		||||
    password: "{{database_password}}"
 | 
			
		||||
    host: "%"
 | 
			
		||||
    priv: '{{database_name}}.*:ALL'
 | 
			
		||||
    state: present
 | 
			
		||||
    login_user: root
 | 
			
		||||
    login_password: "{{mariadb_root_pwd}}"
 | 
			
		||||
    login_host: 127.0.0.1
 | 
			
		||||
    login_port: "{{database_port}}"
 | 
			
		||||
 | 
			
		||||
# Deactivated due to https://chatgpt.com/share/683ba14b-0e74-800f-9ad1-a8979bc77093
 | 
			
		||||
# @todo Remove if this works fine in the future. 
 | 
			
		||||
#- name: Grant database privileges
 | 
			
		||||
#  ansible.builtin.shell:
 | 
			
		||||
#    cmd: "docker exec {{mariadb_hostname }} mariadb -u root -p{{ mariadb_root_pwd }} -e \"GRANT ALL PRIVILEGES ON `{{database_name}}`.* TO '{{database_username}}'@'%';\""
 | 
			
		||||
#  args:
 | 
			
		||||
#    executable: /bin/bash
 | 
			
		||||
@@ -1,29 +1,29 @@
 | 
			
		||||
- name: Create Docker network for MariaDB
 | 
			
		||||
  docker_network:
 | 
			
		||||
    name: "{{ applications['svc-db-mariadb'].network }}"
 | 
			
		||||
    name: "{{ mariadb_network_name }}"
 | 
			
		||||
    state: present
 | 
			
		||||
    ipam_config:
 | 
			
		||||
      - subnet: "{{ networks.local['svc-db-mariadb'].subnet }}"
 | 
			
		||||
      - subnet: "{{ mariadb_subnet }}"
 | 
			
		||||
  when: run_once_docker_mariadb is not defined
 | 
			
		||||
 | 
			
		||||
- name: install MariaDB
 | 
			
		||||
  docker_container:
 | 
			
		||||
    name: "{{ mariadb_hostname }}"
 | 
			
		||||
    image: "mariadb:{{applications['svc-db-mariadb'].version}}"
 | 
			
		||||
    image: "{{ mariadb_image }}:{{ mariadb_version}}"
 | 
			
		||||
    detach: yes
 | 
			
		||||
    env:
 | 
			
		||||
      MARIADB_ROOT_PASSWORD:  "{{mariadb_root_pwd}}"
 | 
			
		||||
      MARIADB_ROOT_PASSWORD:  "{{ mariadb_root_pwd }}"
 | 
			
		||||
      MARIADB_AUTO_UPGRADE:   "1"
 | 
			
		||||
    networks:
 | 
			
		||||
      - name: "{{ applications['svc-db-mariadb'].network }}"
 | 
			
		||||
      - name: "{{ mariadb_network_name }}"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - "{{ applications['svc-db-mariadb'].volume }}:/var/lib/mysql"
 | 
			
		||||
      - "{{ mariadb_volume }}:/var/lib/mysql"
 | 
			
		||||
    published_ports:
 | 
			
		||||
      - "127.0.0.1:{{database_port}}:3306" # can be that this will be removed if all applications use sockets
 | 
			
		||||
      - "127.0.0.1:{{ mariadb_port }}:3306" # can be that this will be removed if all applications use sockets
 | 
			
		||||
    command: "--transaction-isolation=READ-COMMITTED --binlog-format=ROW" #for nextcloud
 | 
			
		||||
    restart_policy: "{{docker_restart_policy}}"
 | 
			
		||||
    restart_policy: "{{ docker_restart_policy }}"
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: "/usr/bin/mariadb --user=root --password={{mariadb_root_pwd}} --execute \"SHOW DATABASES;\""
 | 
			
		||||
      test: "/usr/bin/mariadb --user=root --password={{ mariadb_root_pwd }} --execute \"SHOW DATABASES;\""
 | 
			
		||||
      interval: 3s
 | 
			
		||||
      timeout: 1s
 | 
			
		||||
      retries: 5
 | 
			
		||||
@@ -51,36 +51,9 @@
 | 
			
		||||
    - setup_mariadb_container_result.changed
 | 
			
		||||
    - run_once_docker_mariadb is not defined
 | 
			
		||||
 | 
			
		||||
- name: "Create database: {{ database_name }}"
 | 
			
		||||
  mysql_db:
 | 
			
		||||
    name:           "{{ database_name }}"
 | 
			
		||||
    state:          present
 | 
			
		||||
    login_user:     root
 | 
			
		||||
    login_password: "{{ mariadb_root_pwd }}"
 | 
			
		||||
    login_host:     127.0.0.1
 | 
			
		||||
    login_port:     "{{ database_port }}"
 | 
			
		||||
    encoding:       "{{ database_encoding }}"
 | 
			
		||||
    collation:      "{{ database_collation }}"
 | 
			
		||||
 | 
			
		||||
- name: "Create database user: {{ database_username }}"
 | 
			
		||||
  mysql_user:
 | 
			
		||||
    name:     "{{database_username}}"
 | 
			
		||||
    password: "{{database_password}}"
 | 
			
		||||
    host: "%"
 | 
			
		||||
    priv: '{{database_name}}.*:ALL'
 | 
			
		||||
    state: present
 | 
			
		||||
    login_user: root
 | 
			
		||||
    login_password: "{{mariadb_root_pwd}}"
 | 
			
		||||
    login_host: 127.0.0.1
 | 
			
		||||
    login_port: "{{database_port}}"
 | 
			
		||||
 | 
			
		||||
# Deactivated due to https://chatgpt.com/share/683ba14b-0e74-800f-9ad1-a8979bc77093
 | 
			
		||||
# @todo Remove if this works fine in the future. 
 | 
			
		||||
#- name: Grant database privileges
 | 
			
		||||
#  ansible.builtin.shell:
 | 
			
		||||
#    cmd: "docker exec {{mariadb_hostname }} mariadb -u root -p{{ mariadb_root_pwd }} -e \"GRANT ALL PRIVILEGES ON `{{database_name}}`.* TO '{{database_username}}'@'%';\""
 | 
			
		||||
#  args:
 | 
			
		||||
#    executable: /bin/bash
 | 
			
		||||
- name: "Initialize database for '{{ database_name }}'"
 | 
			
		||||
  include_tasks: init.yml
 | 
			
		||||
  when: "{{ mariadb_init }}"
 | 
			
		||||
 | 
			
		||||
- name: run the docker_mariadb tasks once
 | 
			
		||||
  set_fact:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,11 @@
 | 
			
		||||
application_id:   svc-db-mariadb
 | 
			
		||||
mariadb_hostname: "{{ applications | get_app_conf(application_id, 'hostname', True) }}"
 | 
			
		||||
mariadb_root_pwd: "{{ applications['svc-db-mariadb'].credentials.root_password }}"
 | 
			
		||||
application_id:       svc-db-mariadb
 | 
			
		||||
mariadb_hostname:     "{{ applications | get_app_conf(application_id,'hostname', True) }}"
 | 
			
		||||
mariadb_root_pwd:     "{{ applications | get_app_conf(application_id,'credentials.root_password', True) }}"
 | 
			
		||||
mariadb_init:         "{{ database_username is defined and database_password is defined and database_name is defined }}"
 | 
			
		||||
mariadb_subnet:       "{{ networks.local['svc-db-mariadb'].subnet  }}"
 | 
			
		||||
mariadb_network_name: "{{ applications | get_app_conf(application_id,'network', True) }}"
 | 
			
		||||
mariadb_volume:       "{{ applications | get_app_conf(application_id,'docker.services.mariadb.volume', True) }}"
 | 
			
		||||
mariadb_image:        "{{ applications | get_app_conf(application_id,'docker.services.mariadb.image','mariadb', True) }}"
 | 
			
		||||
mariadb_version:      "{{ applications | get_app_conf(application_id,'docker.services.mariadb.version', True) }}"
 | 
			
		||||
mariadb_port:         "{{ database_port | default(ports.localhost.database[ application_id ]) }}"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,21 @@ network:
 | 
			
		||||
  local:          True                                # Activates local network. Necessary for LDIF import routines
 | 
			
		||||
  docker:         True                                # Activates docker network to allow other docker containers to connect
 | 
			
		||||
  public:         False                               # Set to true in inventory file if you want to expose the LDAP port to the internet
 | 
			
		||||
images:
 | 
			
		||||
  openldap:       "bitnami/openldap:latest"
 | 
			
		||||
docker: 
 | 
			
		||||
  services:
 | 
			
		||||
    openldap:     
 | 
			
		||||
      image:      "bitnami/openldap"
 | 
			
		||||
      version:    "latest"
 | 
			
		||||
      container:  "<< defaults_applications[svc-db-openldap].hostname >>"
 | 
			
		||||
webinterface:     "lam"                               # The webinterface which should be used. Possible: lam and phpldapadmin
 | 
			
		||||
features:
 | 
			
		||||
  ldap:           true
 | 
			
		||||
import:           
 | 
			
		||||
# Here it's possible to define what can be imported. 
 | 
			
		||||
provisioning:           
 | 
			
		||||
# Here it's possible to define what should be imported and updated.
 | 
			
		||||
# It doesn't make sense to let  the import run everytime because its very time consuming
 | 
			
		||||
  credentials:    true 
 | 
			
		||||
  schemas:        true 
 | 
			
		||||
  entries:        true 
 | 
			
		||||
  users:          true
 | 
			
		||||
  configuration:  true # E.g. MemberOf and Hashed Password Configuration
 | 
			
		||||
  credentials:    true # Administrator Password
 | 
			
		||||
  schemas:        true # E.g. Nextcloud, Openssl
 | 
			
		||||
  users:          true # E.g. User, group and role entries
 | 
			
		||||
  groups:         true # Roles and Groups import
 | 
			
		||||
  update:         true # User Class updates
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
- name: Load memberof module from file in OpenLDAP container
 | 
			
		||||
  shell: >
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapmodify -Y EXTERNAL -H ldapi:/// -f {{ldif_docker_path}}configuration/01_member_of_configuration.ldif
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapmodify -Y EXTERNAL -H ldapi:/// -f {{openldap_ldif_docker_path}}configuration/01_member_of_configuration.ldif
 | 
			
		||||
  listen: 
 | 
			
		||||
    - "Import configuration LDIF files"
 | 
			
		||||
    - "Import all LDIF files"
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
- name: Refint Module Activation for OpenLDAP
 | 
			
		||||
  shell: >
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapadd -Y EXTERNAL -H ldapi:/// -f {{ldif_docker_path}}configuration/02_member_of_configuration.ldif
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapadd -Y EXTERNAL -H ldapi:/// -f {{openldap_ldif_docker_path}}configuration/02_member_of_configuration.ldif
 | 
			
		||||
  listen: 
 | 
			
		||||
    - "Import configuration LDIF files"
 | 
			
		||||
    - "Import all LDIF files"
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
 | 
			
		||||
- name: "Import schemas"
 | 
			
		||||
  shell: >
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapadd -Y EXTERNAL -H ldapi:/// -f "{{ldif_docker_path}}schema/{{ item | basename | regex_replace('\.j2$', '') }}"
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapadd -Y EXTERNAL -H ldapi:/// -f "{{openldap_ldif_docker_path}}schema/{{ item | basename | regex_replace('\.j2$', '') }}"
 | 
			
		||||
  register: ldapadd_result
 | 
			
		||||
  changed_when: "'adding new entry' in ldapadd_result.stdout"
 | 
			
		||||
  failed_when: ldapadd_result.rc not in [0, 80]
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
 | 
			
		||||
- name: Refint Overlay Configuration for OpenLDAP
 | 
			
		||||
  shell: >
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapmodify -Y EXTERNAL -H ldapi:/// -f {{ldif_docker_path}}configuration/03_member_of_configuration.ldif
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapmodify -Y EXTERNAL -H ldapi:/// -f {{openldap_ldif_docker_path}}configuration/03_member_of_configuration.ldif
 | 
			
		||||
  listen:
 | 
			
		||||
    - "Import configuration LDIF files"
 | 
			
		||||
    - "Import all LDIF files"
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
 | 
			
		||||
- name: "Import users, groups, etc. to LDAP"
 | 
			
		||||
  shell: >
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapadd -x -D "{{ldap.dn.administrator.data}}" -w "{{ldap.bind_credential}}" -c -f "{{ldif_docker_path}}data/{{ item | basename | regex_replace('\.j2$', '') }}"
 | 
			
		||||
    docker exec -i {{ applications | get_app_conf(application_id, 'hostname', True) }} ldapadd -x -D "{{ldap.dn.administrator.data}}" -w "{{ldap.bind_credential}}" -c -f "{{openldap_ldif_docker_path}}data/{{ item | basename | regex_replace('\.j2$', '') }}"
 | 
			
		||||
  register: ldapadd_result
 | 
			
		||||
  changed_when: "'adding new entry' in ldapadd_result.stdout"
 | 
			
		||||
  failed_when: ldapadd_result.rc not in [0, 20, 68, 65]
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
- name: Ensure LDAP users exist
 | 
			
		||||
  community.general.ldap_entry:
 | 
			
		||||
    dn:           "{{ ldap.user.attributes.id }}={{ item.key }},{{ ldap.dn.ou.users }}"
 | 
			
		||||
    server_uri:   "{{ ldap_server_uri }}"
 | 
			
		||||
    server_uri:   "{{ openldap_server_uri }}"
 | 
			
		||||
    bind_dn:      "{{ ldap.dn.administrator.data }}"
 | 
			
		||||
    bind_pw:      "{{ ldap.bind_credential }}"
 | 
			
		||||
    objectClass:  "{{ ldap.user.objects.structural }}"
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
- name: Ensure required objectClass values and mail address are present
 | 
			
		||||
  community.general.ldap_attrs:
 | 
			
		||||
    dn: "{{ ldap.user.attributes.id }}={{ item.key }},{{ ldap.dn.ou.users }}"
 | 
			
		||||
    server_uri: "{{ ldap_server_uri }}"
 | 
			
		||||
    server_uri: "{{ openldap_server_uri }}"
 | 
			
		||||
    bind_dn: "{{ ldap.dn.administrator.data }}"
 | 
			
		||||
    bind_pw: "{{ ldap.bind_credential }}"
 | 
			
		||||
    attributes:
 | 
			
		||||
@@ -46,7 +46,7 @@
 | 
			
		||||
- name: "Ensure container for application roles exists"
 | 
			
		||||
  community.general.ldap_entry:
 | 
			
		||||
    dn: "{{ ldap.dn.ou.roles }}"
 | 
			
		||||
    server_uri: "{{ ldap_server_uri }}"
 | 
			
		||||
    server_uri: "{{ openldap_server_uri }}"
 | 
			
		||||
    bind_dn:  "{{ ldap.dn.administrator.data }}"
 | 
			
		||||
    bind_pw:  "{{ ldap.bind_credential }}"
 | 
			
		||||
    objectClass: organizationalUnit
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
- name: Gather all users with their current objectClass list
 | 
			
		||||
  community.general.ldap_search:
 | 
			
		||||
    server_uri: "{{ ldap_server_uri }}"
 | 
			
		||||
    server_uri: "{{ openldap_server_uri }}"
 | 
			
		||||
    bind_dn:    "{{ ldap.dn.administrator.data }}"
 | 
			
		||||
    bind_pw:    "{{ ldap.bind_credential }}"
 | 
			
		||||
    dn:         "{{ ldap.dn.ou.users }}"
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
 | 
			
		||||
- name: Add only missing auxiliary classes
 | 
			
		||||
  community.general.ldap_attrs:
 | 
			
		||||
    server_uri: "{{ ldap_server_uri }}"
 | 
			
		||||
    server_uri: "{{ openldap_server_uri }}"
 | 
			
		||||
    bind_dn:    "{{ ldap.dn.administrator.data }}"
 | 
			
		||||
    bind_pw:    "{{ ldap.bind_credential }}"
 | 
			
		||||
    dn:         "{{ item.dn }}"
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
- name: "Create LDIF files at {{ ldif_host_path }}{{ folder }}"
 | 
			
		||||
- name: "Create LDIF files at {{ openldap_ldif_host_path }}{{ folder }}"
 | 
			
		||||
  template:
 | 
			
		||||
    src: "{{ item }}"
 | 
			
		||||
    dest: "{{ ldif_host_path }}{{ folder }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
 | 
			
		||||
    dest: "{{ openldap_ldif_host_path }}{{ folder }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
 | 
			
		||||
    mode: '770'
 | 
			
		||||
  loop: >-
 | 
			
		||||
    {{
 | 
			
		||||
 
 | 
			
		||||
@@ -22,28 +22,30 @@
 | 
			
		||||
    name: "{{ applications | get_app_conf(application_id, 'network.name', True) }}"
 | 
			
		||||
    state: present
 | 
			
		||||
    ipam_config:
 | 
			
		||||
      - subnet: "{{ networks.local['svc-db-openldap'].subnet }}"
 | 
			
		||||
      - subnet: "{{ networks.local[application_id].subnet }}"
 | 
			
		||||
 | 
			
		||||
- meta: flush_handlers
 | 
			
		||||
 | 
			
		||||
- name: "Wait for LDAP to be available"
 | 
			
		||||
  wait_for:
 | 
			
		||||
    host: "127.0.0.1"
 | 
			
		||||
    port: "{{ ports.localhost.ldap['svc-db-openldap'] }}"
 | 
			
		||||
    port: "{{ ports.localhost.ldap[application_id] }}"
 | 
			
		||||
    delay: 5
 | 
			
		||||
    timeout: 120
 | 
			
		||||
    state: started
 | 
			
		||||
 | 
			
		||||
- name: "Reset LDAP Credentials"
 | 
			
		||||
  include_tasks: 01_credentials.yml
 | 
			
		||||
  when: applications | get_app_conf(application_id, 'network.local', True)
 | 
			
		||||
  when:
 | 
			
		||||
    - applications | get_app_conf(application_id, 'network.local', True)
 | 
			
		||||
    - applications | get_app_conf(application_id, 'provisioning.credentials', True)
 | 
			
		||||
 | 
			
		||||
- name: "create directory {{ldif_host_path}}{{item}}"
 | 
			
		||||
- name: "create directory {{openldap_ldif_host_path}}{{item}}"
 | 
			
		||||
  file:
 | 
			
		||||
    path: "{{ldif_host_path}}{{item}}"
 | 
			
		||||
    path: "{{openldap_ldif_host_path}}{{item}}"
 | 
			
		||||
    state: directory
 | 
			
		||||
    mode: 0755
 | 
			
		||||
  loop: "{{ldif_types}}"
 | 
			
		||||
  loop: "{{openldap_ldif_types}}"
 | 
			
		||||
 | 
			
		||||
- name: "Import LDIF Configuration"
 | 
			
		||||
  include_tasks: ldifs_creation.yml
 | 
			
		||||
@@ -51,6 +53,7 @@
 | 
			
		||||
    - configuration
 | 
			
		||||
  loop_control:
 | 
			
		||||
    loop_var: folder
 | 
			
		||||
  when: applications | get_app_conf(application_id, 'provisioning.configuration', True)
 | 
			
		||||
 | 
			
		||||
- name: flush LDIF handlers
 | 
			
		||||
  meta: flush_handlers
 | 
			
		||||
@@ -63,16 +66,20 @@
 | 
			
		||||
 | 
			
		||||
- name: "Include Schemas (if enabled)"
 | 
			
		||||
  include_tasks: 02_schemas.yml
 | 
			
		||||
  when: applications | get_app_conf(application_id, 'provisioning.schemas', True)
 | 
			
		||||
 | 
			
		||||
- name: "Import LDAP Entries (if enabled)"
 | 
			
		||||
  include_tasks: 03_entries.yml
 | 
			
		||||
  include_tasks: 03_users.yml
 | 
			
		||||
  when: applications | get_app_conf(application_id, 'provisioning.users', True)
 | 
			
		||||
 | 
			
		||||
- name: "Import LDIF Data (if enabled)"
 | 
			
		||||
  include_tasks: ldifs_creation.yml
 | 
			
		||||
  loop:
 | 
			
		||||
    - data
 | 
			
		||||
    - groups
 | 
			
		||||
  loop_control:
 | 
			
		||||
    loop_var: folder
 | 
			
		||||
  when: applications | get_app_conf(application_id, 'provisioning.groups', True)
 | 
			
		||||
 | 
			
		||||
- name: "Add Objects to all users"
 | 
			
		||||
  include_tasks: 04_user_updates.yml
 | 
			
		||||
  include_tasks: 04_update.yml
 | 
			
		||||
  when: applications | get_app_conf(application_id, 'provisioning.update', True)
 | 
			
		||||
@@ -13,9 +13,9 @@
 | 
			
		||||
      - "( 1.3.6.1.4.1.99999.2 NAME 'nextcloudUser' DESC 'Auxiliary class for Nextcloud attributes' AUXILIARY MAY ( {{ ldap.user.attributes.nextcloud_quota }} ) )"
 | 
			
		||||
  command: >
 | 
			
		||||
    ldapsm
 | 
			
		||||
      -s {{ ldap_server_uri }}
 | 
			
		||||
      -D '{{ ldap_bind_dn }}'
 | 
			
		||||
      -W '{{ ldap_bind_pw }}'
 | 
			
		||||
      -s {{ openldap_server_uri }}
 | 
			
		||||
      -D '{{ openldap_bind_dn }}'
 | 
			
		||||
      -W '{{ openldap_bind_pw }}'
 | 
			
		||||
      -n {{ schema_name }}
 | 
			
		||||
      {% for at in attribute_defs %}
 | 
			
		||||
      -a "{{ at }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,9 @@
 | 
			
		||||
 | 
			
		||||
  command: >
 | 
			
		||||
    ldapsm
 | 
			
		||||
      -s {{ ldap_server_uri }}
 | 
			
		||||
      -D '{{ ldap_bind_dn }}'
 | 
			
		||||
      -W '{{ ldap_bind_pw }}'
 | 
			
		||||
      -s {{ openldap_server_uri }}
 | 
			
		||||
      -D '{{ openldap_bind_dn }}'
 | 
			
		||||
      -W '{{ openldap_bind_pw }}'
 | 
			
		||||
      -n {{ schema_name }}
 | 
			
		||||
      {% for at in attribute_defs %}
 | 
			
		||||
      -a "{{ at }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
 | 
			
		||||
 | 
			
		||||
  application:
 | 
			
		||||
    image: "{{ applications | get_app_conf(application_id, 'images.openldap', True) }}"
 | 
			
		||||
    container_name: {{ applications | get_app_conf(application_id, 'hostname', True) }}
 | 
			
		||||
    image: "{{ openldap_image }}:{{ openldap_version }}"
 | 
			
		||||
    container_name: "{{ openldap_container }}"
 | 
			
		||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
 | 
			
		||||
{% if applications | get_app_conf(application_id, 'network.public', True) | bool or applications | get_app_conf(application_id, 'network.local', True) | bool %}
 | 
			
		||||
{% if openldap_network_expose_local %}
 | 
			
		||||
    ports:
 | 
			
		||||
      - 127.0.0.1:{{ports.localhost.ldap['svc-db-openldap']}}:{{ldap_docker_port}}
 | 
			
		||||
      - 127.0.0.1:{{ports.localhost.ldap['svc-db-openldap']}}:{{openldap_docker_port_open}}
 | 
			
		||||
{% endif %}
 | 
			
		||||
    volumes:
 | 
			
		||||
      - 'data:/bitnami/openldap'
 | 
			
		||||
      - '{{ldif_host_path}}:{{ldif_docker_path}}:ro'
 | 
			
		||||
      - '{{openldap_ldif_host_path}}:{{openldap_ldif_docker_path}}:ro'
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: >
 | 
			
		||||
        bash -c '
 | 
			
		||||
        ldapsearch -x -H ldap://localhost:{{ ldap_docker_port }} \
 | 
			
		||||
        ldapsearch -x -H ldap://localhost:{{ openldap_docker_port_open }} \
 | 
			
		||||
          -D "{{ ldap.dn.administrator.data }}" -w "{{ ldap.bind_credential }}" -b "{{ ldap.dn.root }}" > /dev/null \
 | 
			
		||||
        && ldapsearch -Y EXTERNAL -H ldapi:/// \
 | 
			
		||||
          -b cn=config "(&(objectClass=olcOverlayConfig)(olcOverlay=memberof))" \
 | 
			
		||||
@@ -24,5 +24,6 @@
 | 
			
		||||
 | 
			
		||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
 | 
			
		||||
  data:
 | 
			
		||||
    name: "{{ openldap_volume }}"
 | 
			
		||||
 | 
			
		||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
 | 
			
		||||
@@ -18,9 +18,9 @@ LDAP_CONFIG_ADMIN_USERNAME= {{applications | get_app_conf(application_id, 'users
 | 
			
		||||
LDAP_CONFIG_ADMIN_PASSWORD= {{applications | get_app_conf(application_id, 'credentials.administrator_password', True)}}
 | 
			
		||||
  
 | 
			
		||||
# Network
 | 
			
		||||
LDAP_PORT_NUMBER=           {{ldap_docker_port}}            # Route to default port
 | 
			
		||||
LDAP_PORT_NUMBER=           {{openldap_docker_port_open}}            # Route to default port
 | 
			
		||||
LDAP_ENABLE_TLS=            no                              # Using nginx proxy for tls
 | 
			
		||||
LDAP_LDAPS_PORT_NUMBER=     {{ldaps_docker_port}}           # Port used for TLS secure traffic. Priviledged port is supported (e.g. 636). Default: 1636 (non privileged port).
 | 
			
		||||
LDAP_LDAPS_PORT_NUMBER=     {{openldap_docker_port_secure}}           # Port used for TLS secure traffic. Priviledged port is supported (e.g. 636). Default: 1636 (non privileged port).
 | 
			
		||||
 | 
			
		||||
# Security
 | 
			
		||||
LDAP_ALLOW_ANON_BINDING=    no                              # Allow anonymous bindings to the LDAP server. Default: yes.
 | 
			
		||||
@@ -1,17 +1,24 @@
 | 
			
		||||
application_id:     "svc-db-openldap"
 | 
			
		||||
application_id:                 "svc-db-openldap"
 | 
			
		||||
 | 
			
		||||
# LDAP Variables
 | 
			
		||||
ldaps_docker_port:  636
 | 
			
		||||
ldap_docker_port:   389
 | 
			
		||||
ldap_server_uri:    "ldap://127.0.0.1:{{ ports.localhost.ldap['svc-db-openldap'] }}"
 | 
			
		||||
ldap_hostname:      "{{ applications | get_app_conf(application_id, 'hostname', True) }}"
 | 
			
		||||
ldap_bind_dn:       "{{ ldap.dn.administrator.configuration }}"
 | 
			
		||||
ldap_bind_pw:       "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
 | 
			
		||||
openldap_docker_port_secure:    636
 | 
			
		||||
openldap_docker_port_open:      389
 | 
			
		||||
openldap_server_uri:            "ldap://127.0.0.1:{{ ports.localhost.ldap[application_id] }}"
 | 
			
		||||
openldap_hostname:              "{{ applications | get_app_conf(application_id, 'hostname', True) }}"
 | 
			
		||||
openldap_bind_dn:               "{{ ldap.dn.administrator.configuration }}"
 | 
			
		||||
openldap_bind_pw:               "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
 | 
			
		||||
 | 
			
		||||
# LDIF Variables
 | 
			
		||||
ldif_host_path:     "{{docker_compose.directories.volumes}}ldif/"
 | 
			
		||||
ldif_docker_path:   "/tmp/ldif/"
 | 
			
		||||
ldif_types:
 | 
			
		||||
openldap_ldif_host_path:        "{{docker_compose.directories.volumes}}ldif/"
 | 
			
		||||
openldap_ldif_docker_path:      "/tmp/ldif/"
 | 
			
		||||
openldap_ldif_types:
 | 
			
		||||
  - configuration
 | 
			
		||||
  - data
 | 
			
		||||
  - schema          # Don't know if this is still needed, it's now setup via tasks
 | 
			
		||||
  - schema                      # Don't know if this is still needed, it's now setup via tasks
 | 
			
		||||
 | 
			
		||||
openldap_container:             "{{ applications | get_app_conf(application_id, 'docker.services.openldap.container', True) }}"
 | 
			
		||||
openldap_image:                 "{{ applications | get_app_conf(application_id, 'docker.services.openldap.image', True) }}"
 | 
			
		||||
openldap_version:               "{{ applications | get_app_conf(application_id, 'docker.services.openldap.version', True) }}"
 | 
			
		||||
openldap_volume:                "{{ application_id }}_data"
 | 
			
		||||
 | 
			
		||||
openldap_network_expose_local:  "{{ applications | get_app_conf(application_id, 'network.public', True) | bool or applications | get_app_conf(application_id, 'network.local', True) | bool }}"
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
hostname: "svc-db-postgres"
 | 
			
		||||
network:  "<< defaults_applications[svc-db-postgres].hostname >>"
 | 
			
		||||
volume:   "<< defaults_applications[svc-db-postgres].hostname >>"
 | 
			
		||||
docker:
 | 
			
		||||
  services:
 | 
			
		||||
    postgres:
 | 
			
		||||
@@ -8,4 +7,5 @@ docker:
 | 
			
		||||
      image: postgis/postgis
 | 
			
		||||
      # Please set an version in your inventory file! 
 | 
			
		||||
      # Rolling release isn't recommended
 | 
			
		||||
      version: "latest"
 | 
			
		||||
      version: "latest"
 | 
			
		||||
      volume:  "<< defaults_applications[svc-db-postgres].hostname >>_data"
 | 
			
		||||
@@ -1,25 +1,25 @@
 | 
			
		||||
- name: Create Docker network for PostgreSQL
 | 
			
		||||
  docker_network:
 | 
			
		||||
    name: "{{ applications | get_app_conf(application_id, 'network', True) }}"
 | 
			
		||||
    name: "{{ postgres_network_name }}"
 | 
			
		||||
    state: present
 | 
			
		||||
    ipam_config:
 | 
			
		||||
      - subnet: "{{ networks.local['svc-db-postgres'].subnet }}"
 | 
			
		||||
      - subnet: "{{ postgres_subnet }}"
 | 
			
		||||
  when: run_once_docker_postgres is not defined
 | 
			
		||||
 | 
			
		||||
- name: Install PostgreSQL
 | 
			
		||||
  docker_container:
 | 
			
		||||
    name: "{{ applications | get_app_conf(application_id, 'hostname', True) }}"
 | 
			
		||||
    image: "{{ applications | get_app_conf(application_id, 'docker.services.postgres.image', True) }}:{{ applications | get_app_conf(application_id, 'docker.services.postgres.version', True) }}"
 | 
			
		||||
    name: "{{ postgres_hostname }}"
 | 
			
		||||
    image: "{{ postgres_image }}:{{ postgres_version }}"
 | 
			
		||||
    detach: yes
 | 
			
		||||
    env:
 | 
			
		||||
      POSTGRES_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.postgres_password', True) }}"
 | 
			
		||||
      POSTGRES_PASSWORD: "{{ postgres_password }}"
 | 
			
		||||
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" # Necessary for web-app-matrix
 | 
			
		||||
    networks:
 | 
			
		||||
      - name: "{{ applications | get_app_conf(application_id, 'network', True) }}"
 | 
			
		||||
      - name: "{{ postgres_network_name }}"
 | 
			
		||||
    published_ports:
 | 
			
		||||
      - "127.0.0.1:{{ database_port }}:5432"
 | 
			
		||||
      - "127.0.0.1:{{ postgres_port }}:5432"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - "{{ applications['svc-db-postgres'].volume }}:/var/lib/postgresql/data"
 | 
			
		||||
      - "{{ postgres_volume }}:/var/lib/postgresql/data"
 | 
			
		||||
    restart_policy: "{{ docker_restart_policy }}"
 | 
			
		||||
    healthcheck:
 | 
			
		||||
      test: ["CMD-SHELL", "pg_isready -U postgres"]
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
  when: run_once_docker_postgres is not defined
 | 
			
		||||
 | 
			
		||||
- name: Wait for Postgres inside the container
 | 
			
		||||
  shell: "docker exec {{ applications | get_app_conf(application_id, 'hostname', True) }} pg_isready -U postgres"
 | 
			
		||||
  shell: "docker exec {{ postgres_hostname }} pg_isready -U postgres"
 | 
			
		||||
  register: pg_ready
 | 
			
		||||
  until: pg_ready.rc == 0
 | 
			
		||||
  retries: 30
 | 
			
		||||
@@ -47,12 +47,9 @@
 | 
			
		||||
    state: present
 | 
			
		||||
  when: run_once_docker_postgres is not defined
 | 
			
		||||
 | 
			
		||||
- name: Load database initialization tasks dynamically
 | 
			
		||||
  include_tasks: init_database.yml
 | 
			
		||||
  when:
 | 
			
		||||
    - database_username is defined
 | 
			
		||||
    - database_password is defined
 | 
			
		||||
    - database_name is defined
 | 
			
		||||
- name: "Initialize database for '{{ database_name }}'"
 | 
			
		||||
  include_tasks: init.yml
 | 
			
		||||
  when: "{{ postgres_init }}"
 | 
			
		||||
 | 
			
		||||
- name: Run the docker_postgres tasks once
 | 
			
		||||
  set_fact:
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,10 @@
 | 
			
		||||
application_id: svc-db-postgres
 | 
			
		||||
application_id:         svc-db-postgres
 | 
			
		||||
postgres_volume:        "{{ applications | get_app_conf(application_id, 'docker.services.postgres.volume', True) }}"
 | 
			
		||||
postgres_hostname:      "{{ applications | get_app_conf(application_id, 'hostname', True) }}"
 | 
			
		||||
postgres_image:         "{{ applications | get_app_conf(application_id, 'docker.services.postgres.image', True) }}"
 | 
			
		||||
postgres_subnet:        "{{ networks.local['svc-db-postgres'].subnet }}"
 | 
			
		||||
postgres_network_name:  "{{ applications | get_app_conf(application_id, 'network', True) }}"
 | 
			
		||||
postgres_version:       "{{ applications | get_app_conf(application_id, 'docker.services.postgres.version', True) }}"
 | 
			
		||||
postgres_password:      "{{ applications | get_app_conf(application_id, 'credentials.postgres_password', True) }}"
 | 
			
		||||
postgres_port:          "{{ database_port | default(ports.localhost.database[ application_id ]) }}"
 | 
			
		||||
postgres_init:          "{{ database_username is defined and database_password is defined and database_name is defined }}"
 | 
			
		||||
@@ -1,10 +1,8 @@
 | 
			
		||||
images: 
 | 
			
		||||
  keycloak:           "quay.io/keycloak/keycloak:latest"
 | 
			
		||||
import_realm:         True                                # If True realm will be imported. If false skip.
 | 
			
		||||
features:
 | 
			
		||||
  matomo:             true
 | 
			
		||||
  css:                false
 | 
			
		||||
  port-ui-desktop:   true
 | 
			
		||||
  port-ui-desktop:    true
 | 
			
		||||
  ldap:               true
 | 
			
		||||
  central_database:   true
 | 
			
		||||
  recaptcha:          true
 | 
			
		||||
@@ -26,6 +24,9 @@ scopes:
 | 
			
		||||
rbac_groups:  "/rbac"
 | 
			
		||||
docker:
 | 
			
		||||
  services:
 | 
			
		||||
    keycloak:
 | 
			
		||||
      image:    "quay.io/keycloak/keycloak"
 | 
			
		||||
      version:  "latest"
 | 
			
		||||
    database:
 | 
			
		||||
      enabled: true
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -72,11 +72,11 @@
 | 
			
		||||
- name: Render user-profile JSON for SSH key
 | 
			
		||||
  template:
 | 
			
		||||
    src:  import/user-profile.json.j2
 | 
			
		||||
    dest: "{{ import_directory_host }}/user-profile.json"
 | 
			
		||||
    dest: "{{ keycloak_host_import_directory }}/user-profile.json"
 | 
			
		||||
    mode: '0644'
 | 
			
		||||
  notify: docker compose up
 | 
			
		||||
 | 
			
		||||
- name: Apply SSH Public Key to user-profile via kcadm
 | 
			
		||||
  shell: |
 | 
			
		||||
    docker exec -i {{ container_name }} \
 | 
			
		||||
      /opt/keycloak/bin/kcadm.sh update realms/{{ keycloak_realm }} -f {{ import_directory_docker }}user-profile.json
 | 
			
		||||
      /opt/keycloak/bin/kcadm.sh update realms/{{ keycloak_realm }} -f {{ keycloak_docker_import_directory }}user-profile.json
 | 
			
		||||
 
 | 
			
		||||
@@ -3,16 +3,16 @@
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: cmp-db-docker-proxy
 | 
			
		||||
 | 
			
		||||
- name: "create directory {{import_directory_host}}"
 | 
			
		||||
- name: "create directory {{keycloak_host_import_directory}}"
 | 
			
		||||
  file:
 | 
			
		||||
    path: "{{import_directory_host}}"
 | 
			
		||||
    path: "{{keycloak_host_import_directory}}"
 | 
			
		||||
    state: directory
 | 
			
		||||
    mode: 0755
 | 
			
		||||
 | 
			
		||||
- name: "Copy import files to {{ import_directory_host }}"
 | 
			
		||||
- name: "Copy import files to {{ keycloak_host_import_directory }}"
 | 
			
		||||
  template:
 | 
			
		||||
    src: "{{ item }}"
 | 
			
		||||
    dest: "{{ import_directory_host }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
 | 
			
		||||
    dest: "{{ keycloak_host_import_directory }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
 | 
			
		||||
    mode: '770'
 | 
			
		||||
  loop: "{{ lookup('fileglob', '{{ role_path }}/templates/import/*.j2', wantlist=True) }}"
 | 
			
		||||
  notify: docker compose up
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
 | 
			
		||||
 | 
			
		||||
  application:
 | 
			
		||||
    image: "{{ applications | get_app_conf(application_id, 'images.keycloak', True) }}"
 | 
			
		||||
    container_name: {{container_name}}
 | 
			
		||||
    command: start {% if applications | get_app_conf(application_id, 'import_realm', True) | bool %}--import-realm{% endif %}
 | 
			
		||||
    image: "{{ keycloak_image }}:{{ keycloak_version }}"
 | 
			
		||||
    container_name: {{ keycloak_container }}
 | 
			
		||||
    command: start{% if keycloak_import_realm %} --import-realm{% endif %}{% if keycloak_debug_enabled %} --verbose{% endif %}
 | 
			
		||||
    {% include 'roles/docker-container/templates/base.yml.j2' %}
 | 
			
		||||
    ports:
 | 
			
		||||
      - "{{ keycloak_server_host }}:8080"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - "{{import_directory_host}}:{{import_directory_docker}}"
 | 
			
		||||
      - "{{keycloak_host_import_directory}}:{{keycloak_docker_import_directory}}"
 | 
			
		||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
 | 
			
		||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
 | 
			
		||||
{% set container_port = 9000 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,17 @@
 | 
			
		||||
application_id:                   "keycloak"
 | 
			
		||||
database_type:                    "postgres"
 | 
			
		||||
container_name:                   "{{application_id}}_application"
 | 
			
		||||
import_directory_host:            "{{docker_compose.directories.volumes}}import/"           # Directory in which keycloack import files are placed on the host
 | 
			
		||||
import_directory_docker:          "/opt/keycloak/data/import/"                              # Directory in which keycloack import files are placed in the running docker container
 | 
			
		||||
keycloak_realm:                   "{{ primary_domain}}"                                     # This is the name of the default realm which is used by the applications
 | 
			
		||||
keycloak_administrator:           "{{ applications | get_app_conf(application_id, 'users.administrator', True) }}"  # Master Administrator
 | 
			
		||||
keycloak_administrator_username:  "{{ keycloak_administrator.username}}"                    # Master Administrator Username
 | 
			
		||||
keycloak_administrator_password:  "{{ keycloak_administrator.password}}"                    # Master Administrator Password
 | 
			
		||||
keycloak_kcadm_path:              "docker exec -i {{ container_name }} /opt/keycloak/bin/kcadm.sh"
 | 
			
		||||
application_id:                   "keycloak"                                                                                      # Internal CyMaIS application id 
 | 
			
		||||
database_type:                    "postgres"                                                                                      # Database which will be used
 | 
			
		||||
keycloak_container:               "{{ application_id }}_application"                                                              # Name of the keycloack docker container
 | 
			
		||||
keycloak_host_import_directory:   "{{ docker_compose.directories.volumes }}import/"                                               # Directory in which keycloack import files are placed on the host
 | 
			
		||||
keycloak_docker_import_directory: "/opt/keycloak/data/import/"                                                                    # Directory in which keycloack import files are placed in the running docker container
 | 
			
		||||
keycloak_realm:                   "{{ primary_domain}}"                                                                           # This is the name of the default realm which is used by the applications
 | 
			
		||||
keycloak_administrator:           "{{ applications | get_app_conf(application_id, 'users.administrator', True) }}"                # Master Administrator
 | 
			
		||||
keycloak_administrator_username:  "{{ keycloak_administrator.username }}"                                                         # Master Administrator Username
 | 
			
		||||
keycloak_administrator_password:  "{{ keycloak_administrator.password }}"                                                         # Master Administrator Password
 | 
			
		||||
keycloak_kcadm_path:              "docker exec -i {{ keycloak_container }} /opt/keycloak/bin/kcadm.sh"                            # Init script for keycloak
 | 
			
		||||
keycloak_server_internal_url:     "http://127.0.0.1:8080"
 | 
			
		||||
keycloak_server_host:             "127.0.0.1:{{ports.localhost.http[application_id]}}"
 | 
			
		||||
keycloak_server_host:             "127.0.0.1:{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
keycloak_server_host_url:         "http://{{ keycloak_server_host }}"
 | 
			
		||||
  
 | 
			
		||||
keycloak_image:                   "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image', True) }}"     # Keycloak docker image
 | 
			
		||||
keycloak_version:                 "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version', True) }}"   # Keyloak docker version
 | 
			
		||||
keycloak_import_realm:            "{{ applications | get_app_conf(application_id, 'import_realm', True, True) }}"                 # Activate realm import
 | 
			
		||||
keycloak_debug_enabled:           "{{ enable_debug }}"
 | 
			
		||||
@@ -33,12 +33,12 @@ class TestDeprecatedVersionKey(unittest.TestCase):
 | 
			
		||||
            if uses_version:
 | 
			
		||||
                warnings.append(
 | 
			
		||||
                    f"[DEPRECATION WARNING] {role_path.name}/config/main.yml: "
 | 
			
		||||
                    f"'version' is deprecated. Replace it by docker.versions[version]."
 | 
			
		||||
                    f"'version' is deprecated. Replace it by docker.services[service].version."
 | 
			
		||||
                )
 | 
			
		||||
            if uses_images:
 | 
			
		||||
                warnings.append(
 | 
			
		||||
                    f"[DEPRECATION WARNING] {role_path.name}/config/main.yml: "
 | 
			
		||||
                    f"'images' is deprecated. Replace it by docker.images[image]."
 | 
			
		||||
                    f"'images' is deprecated. Replace it by docker.services[service].image."
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        if warnings:
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,11 @@ import yaml
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
class TestDockerRoleImagesConfiguration(unittest.TestCase):
 | 
			
		||||
    def test_images_keys_and_templates(self):
 | 
			
		||||
class TestDockerRoleServicesConfiguration(unittest.TestCase):
 | 
			
		||||
    def test_services_keys_and_templates(self):
 | 
			
		||||
        """
 | 
			
		||||
        For each web-app-* role, check that:
 | 
			
		||||
        - roles/web-app-*/config/main.yml contains 'images' as a dict with keys/values
 | 
			
		||||
        - Each image key is referenced as:
 | 
			
		||||
            image: "{{ applications[application_id].images.<key> }}"
 | 
			
		||||
          in either roles/web-app-*/templates/docker-compose.yml.j2 or env.j2
 | 
			
		||||
        - roles/web-app-*/config/main.yml contains 'services' as a dict with keys/values
 | 
			
		||||
        """
 | 
			
		||||
        repo_root = Path(__file__).resolve().parent.parent.parent
 | 
			
		||||
        roles_dir = repo_root / "roles"
 | 
			
		||||
@@ -33,13 +30,13 @@ class TestDockerRoleImagesConfiguration(unittest.TestCase):
 | 
			
		||||
                errors.append(f"{role_path.name}: YAML parse error: {e}")
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            images = config.get("docker",{}).get("images")
 | 
			
		||||
            if not images:
 | 
			
		||||
                warnings.append(f"[WARNING] {role_path.name}: No 'docker.images' key in config/main.yml")
 | 
			
		||||
            services = config.get("docker",{}).get("services",{})
 | 
			
		||||
            if not services:
 | 
			
		||||
                warnings.append(f"[WARNING] {role_path.name}: No 'docker.services' key in config/main.yml")
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if not isinstance(images, dict):
 | 
			
		||||
                errors.append(f"{role_path.name}: 'images' must be a dict in config/main.yml")
 | 
			
		||||
            if not isinstance(services, dict):
 | 
			
		||||
                errors.append(f"{role_path.name}: 'services' must be a dict in config/main.yml")
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
                # OPTIONAL: Check if the image is available locally via docker images
 | 
			
		||||
@@ -55,9 +52,9 @@ class TestDockerRoleImagesConfiguration(unittest.TestCase):
 | 
			
		||||
                #     except Exception as e:
 | 
			
		||||
                #         errors.append(f"{role_path.name}: Error running 'docker images' (optional): {e}")
 | 
			
		||||
        if warnings:
 | 
			
		||||
            print("\nWarnings in docker role images configuration:\n" + "\n".join(warnings))
 | 
			
		||||
            print("\nWarnings in docker role services configuration:\n" + "\n".join(warnings))
 | 
			
		||||
        if errors:
 | 
			
		||||
            self.fail("Errors in docker role images configuration:\n" + "\n".join(errors))
 | 
			
		||||
            self.fail("Errors in docker role services configuration:\n" + "\n".join(errors))
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    unittest.main()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										116
									
								
								tests/unit/utils/test_valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								tests/unit/utils/test_valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
# File: tests/unit/utils/test_valid_deploy_id.py
 | 
			
		||||
import os
 | 
			
		||||
import tempfile
 | 
			
		||||
import unittest
 | 
			
		||||
import yaml
 | 
			
		||||
from utils.valid_deploy_id import ValidDeployId
 | 
			
		||||
 | 
			
		||||
class TestValidDeployId(unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # Create a temporary directory for roles
 | 
			
		||||
        self.temp_dir = tempfile.TemporaryDirectory()
 | 
			
		||||
        self.roles_dir = os.path.join(self.temp_dir.name, 'roles')
 | 
			
		||||
        os.makedirs(self.roles_dir)
 | 
			
		||||
 | 
			
		||||
        # Create a dummy role with application_id 'app1'
 | 
			
		||||
        role_path = os.path.join(self.roles_dir, 'role1', 'vars')
 | 
			
		||||
        os.makedirs(role_path)
 | 
			
		||||
        with open(os.path.join(role_path, 'main.yml'), 'w', encoding='utf-8') as f:
 | 
			
		||||
            yaml.safe_dump({'application_id': 'app1'}, f)
 | 
			
		||||
 | 
			
		||||
        # Initialize validator with our temp roles_dir
 | 
			
		||||
        self.validator = ValidDeployId(roles_dir=self.roles_dir)
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        self.temp_dir.cleanup()
 | 
			
		||||
 | 
			
		||||
    def _write_ini_inventory(self, content):
 | 
			
		||||
        fd, path = tempfile.mkstemp(suffix='.ini')
 | 
			
		||||
        os.close(fd)
 | 
			
		||||
        with open(path, 'w', encoding='utf-8') as f:
 | 
			
		||||
            f.write(content)
 | 
			
		||||
        return path
 | 
			
		||||
 | 
			
		||||
    def _write_yaml_inventory(self, data):
 | 
			
		||||
        fd, path = tempfile.mkstemp(suffix='.yml')
 | 
			
		||||
        os.close(fd)
 | 
			
		||||
        with open(path, 'w', encoding='utf-8') as f:
 | 
			
		||||
            yaml.safe_dump(data, f)
 | 
			
		||||
        return path
 | 
			
		||||
 | 
			
		||||
    def test_valid_in_roles_and_ini_inventory(self):
 | 
			
		||||
        # Inventory contains app1 as a host
 | 
			
		||||
        ini_content = """
 | 
			
		||||
        [servers]
 | 
			
		||||
        app1,otherhost
 | 
			
		||||
        """
 | 
			
		||||
        inv = self._write_ini_inventory(ini_content)
 | 
			
		||||
        result = self.validator.validate(inv, ['app1'])
 | 
			
		||||
        self.assertEqual(result, {}, "app1 should be valid when in roles and ini inventory")
 | 
			
		||||
 | 
			
		||||
    def test_missing_in_roles(self):
 | 
			
		||||
        # Inventory contains app2 but roles only have app1
 | 
			
		||||
        ini_content = """
 | 
			
		||||
        [servers]
 | 
			
		||||
        app2
 | 
			
		||||
        """
 | 
			
		||||
        inv = self._write_ini_inventory(ini_content)
 | 
			
		||||
        result = self.validator.validate(inv, ['app2'])
 | 
			
		||||
        # app2 not in roles, but in inventory
 | 
			
		||||
        expected = {'app2': {'in_roles': False, 'in_inventory': True}}
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_missing_in_inventory_ini(self):
 | 
			
		||||
        # Roles have app1 but inventory does not mention it
 | 
			
		||||
        ini_content = """
 | 
			
		||||
        [servers]
 | 
			
		||||
        otherhost
 | 
			
		||||
        """
 | 
			
		||||
        inv = self._write_ini_inventory(ini_content)
 | 
			
		||||
        result = self.validator.validate(inv, ['app1'])
 | 
			
		||||
        expected = {'app1': {'in_roles': True, 'in_inventory': False}}
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_missing_both_ini(self):
 | 
			
		||||
        # Neither roles nor inventory have appX
 | 
			
		||||
        ini_content = """
 | 
			
		||||
        [servers]
 | 
			
		||||
        otherhost
 | 
			
		||||
        """
 | 
			
		||||
        inv = self._write_ini_inventory(ini_content)
 | 
			
		||||
        result = self.validator.validate(inv, ['appX'])
 | 
			
		||||
        expected = {'appX': {'in_roles': False, 'in_inventory': False}}
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_valid_in_roles_and_yaml_inventory(self):
 | 
			
		||||
        # YAML inventory with app1 as a dict key
 | 
			
		||||
        data = {'app1': {'hosts': ['app1']}, 'group': {'app1': {}}}
 | 
			
		||||
        inv = self._write_yaml_inventory(data)
 | 
			
		||||
        result = self.validator.validate(inv, ['app1'])
 | 
			
		||||
        self.assertEqual(result, {}, "app1 should be valid in roles and yaml inventory")
 | 
			
		||||
 | 
			
		||||
    def test_missing_in_roles_yaml(self):
 | 
			
		||||
        # YAML inventory has app2 key but roles only have app1
 | 
			
		||||
        data = {'app2': {}}
 | 
			
		||||
        inv = self._write_yaml_inventory(data)
 | 
			
		||||
        result = self.validator.validate(inv, ['app2'])
 | 
			
		||||
        expected = {'app2': {'in_roles': False, 'in_inventory': True}}
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_missing_in_inventory_yaml(self):
 | 
			
		||||
        # Roles have app1 but YAML inventory has no app1
 | 
			
		||||
        data = {'group': {'other': {}}}
 | 
			
		||||
        inv = self._write_yaml_inventory(data)
 | 
			
		||||
        result = self.validator.validate(inv, ['app1'])
 | 
			
		||||
        expected = {'app1': {'in_roles': True, 'in_inventory': False}}
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
    def test_missing_both_yaml(self):
 | 
			
		||||
        data = {}
 | 
			
		||||
        inv = self._write_yaml_inventory(data)
 | 
			
		||||
        result = self.validator.validate(inv, ['unknown'])
 | 
			
		||||
        expected = {'unknown': {'in_roles': False, 'in_inventory': False}}
 | 
			
		||||
        self.assertEqual(result, expected)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    unittest.main()
 | 
			
		||||
							
								
								
									
										89
									
								
								utils/valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								utils/valid_deploy_id.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
# File: utils/valid_deploy_id.py
 | 
			
		||||
"""
 | 
			
		||||
Utility for validating deployment application IDs against defined roles and inventory.
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
import yaml
 | 
			
		||||
import glob
 | 
			
		||||
import configparser
 | 
			
		||||
 | 
			
		||||
from filter_plugins.get_all_application_ids import get_all_application_ids
 | 
			
		||||
 | 
			
		||||
class ValidDeployId:
 | 
			
		||||
    def __init__(self, roles_dir='roles'):
 | 
			
		||||
        # Load all known application IDs from roles
 | 
			
		||||
        self.valid_ids = set(get_all_application_ids(roles_dir))
 | 
			
		||||
 | 
			
		||||
    def validate(self, inventory_path, ids):
 | 
			
		||||
        """
 | 
			
		||||
        Validate a list of application IDs against both role definitions and inventory.
 | 
			
		||||
        Returns a dict mapping invalid IDs to their presence status.
 | 
			
		||||
        Example:
 | 
			
		||||
          {
 | 
			
		||||
            "app1": {"in_roles": False, "in_inventory": True},
 | 
			
		||||
            "app2": {"in_roles": True, "in_inventory": False}
 | 
			
		||||
          }
 | 
			
		||||
        """
 | 
			
		||||
        invalid = {}
 | 
			
		||||
        for app_id in ids:
 | 
			
		||||
            in_roles = app_id in self.valid_ids
 | 
			
		||||
            in_inventory = self._exists_in_inventory(inventory_path, app_id)
 | 
			
		||||
            if not (in_roles and in_inventory):
 | 
			
		||||
                invalid[app_id] = {
 | 
			
		||||
                    'in_roles': in_roles,
 | 
			
		||||
                    'in_inventory': in_inventory
 | 
			
		||||
                }
 | 
			
		||||
        return invalid
 | 
			
		||||
 | 
			
		||||
    def _exists_in_inventory(self, inventory_path, app_id):
 | 
			
		||||
        _, ext = os.path.splitext(inventory_path)
 | 
			
		||||
        if ext in ('.yml', '.yaml'):
 | 
			
		||||
            return self._search_yaml_keys(inventory_path, app_id)
 | 
			
		||||
        else:
 | 
			
		||||
            return self._search_ini_sections(inventory_path, app_id)
 | 
			
		||||
 | 
			
		||||
    def _search_ini_sections(self, inventory_path, app_id):
 | 
			
		||||
        """
 | 
			
		||||
        Manually parse INI inventory for sections and host lists.
 | 
			
		||||
        Returns True if app_id matches a section name or a host in a section.
 | 
			
		||||
        """
 | 
			
		||||
        present = False
 | 
			
		||||
        with open(inventory_path, 'r', encoding='utf-8') as f:
 | 
			
		||||
            current_section = None
 | 
			
		||||
            for raw in f:
 | 
			
		||||
                line = raw.strip()
 | 
			
		||||
                # Skip blanks and comments
 | 
			
		||||
                if not line or line.startswith(('#', ';')):
 | 
			
		||||
                    continue
 | 
			
		||||
                # Section header
 | 
			
		||||
                if line.startswith('[') and line.endswith(']'):
 | 
			
		||||
                    current_section = line[1:-1].strip()
 | 
			
		||||
                    if current_section == app_id:
 | 
			
		||||
                        return True
 | 
			
		||||
                    continue
 | 
			
		||||
                # Host or variable line under a section
 | 
			
		||||
                if current_section:
 | 
			
		||||
                    # Split on commas or whitespace
 | 
			
		||||
                    for part in [p.strip() for p in line.replace(',', ' ').split()]:
 | 
			
		||||
                        if part == app_id:
 | 
			
		||||
                            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def _search_yaml_keys(self, inventory_path, app_id):
 | 
			
		||||
        with open(inventory_path, 'r', encoding='utf-8') as f:
 | 
			
		||||
            data = yaml.safe_load(f)
 | 
			
		||||
        return self._find_key(data, app_id)
 | 
			
		||||
 | 
			
		||||
    def _find_key(self, node, key):  # recursive search
 | 
			
		||||
        if isinstance(node, dict):
 | 
			
		||||
            for k, v in node.items():
 | 
			
		||||
                # If key matches and maps to a dict or list, consider it present
 | 
			
		||||
                if k == key and isinstance(v, (dict, list)):
 | 
			
		||||
                    return True
 | 
			
		||||
                if self._find_key(v, key):
 | 
			
		||||
                    return True
 | 
			
		||||
        elif isinstance(node, list):
 | 
			
		||||
            for item in node:
 | 
			
		||||
                if self._find_key(item, key):
 | 
			
		||||
                    return True
 | 
			
		||||
        return False
 | 
			
		||||
		Reference in New Issue
	
	Block a user