--- # ============================================================================ # Semaphore configuration via REST API (idempotent) # ============================================================================ # Each section: GET the existing list, build a name→id lookup, then POST # any declared objects whose names are missing. Existing objects are # never modified — change-by-recreate is the operator's job through the # UI or by hand-editing semaphore_config and deleting the old object. # ============================================================================ - name: Set Semaphore API auth header ansible.builtin.set_fact: _semaphore_headers: Authorization: "Bearer {{ semaphore_api_token }}" Content-Type: "application/json" no_log: true # --------------------------------------------------------------------------- # Project — find-or-create # --------------------------------------------------------------------------- - name: List Semaphore projects ansible.builtin.uri: url: "{{ semaphore_api_url }}/projects" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_projects no_log: true - name: Create Semaphore project ansible.builtin.uri: url: "{{ semaphore_api_url }}/projects" method: POST headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" body_format: json body: name: "{{ semaphore_config.project.name }}" alert: "{{ semaphore_config.project.alert | default(false) }}" max_parallel_tasks: "{{ semaphore_config.project.max_parallel_tasks | default(0) }}" status_code: [201, 204] when: semaphore_config.project.name not in (_semaphore_projects.json | map(attribute='name') | list) no_log: true - name: Resolve project id ansible.builtin.uri: url: "{{ semaphore_api_url }}/projects" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_projects_after no_log: true - name: Set project id fact ansible.builtin.set_fact: _semaphore_project_id: >- {{ (_semaphore_projects_after.json | selectattr('name', 'equalto', semaphore_config.project.name) | list | first).id }} # --------------------------------------------------------------------------- # Keys — find-or-create # --------------------------------------------------------------------------- - name: List existing keys ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/keys" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_keys no_log: true - name: Create missing keys ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/keys" method: POST headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" body_format: json body: >- {{ { 'name': item.name, 'type': item.type, 'project_id': _semaphore_project_id | int } | combine( {'login_password': {'login': item.login | default(''), 'password': item.password | default('')}} if item.type == 'login_password' else {} ) | combine( {'ssh': {'login': item.ssh_login | default(''), 'passphrase': item.ssh_passphrase | default(''), 'private_key': item.ssh_private_key | default('')}} if item.type == 'ssh' else {} ) }} status_code: [201, 204] loop: "{{ semaphore_config['keys'] }}" loop_control: label: "{{ item.name }}" when: item.name not in (_semaphore_keys.json | map(attribute='name') | list) no_log: true - name: Re-list keys with assigned ids ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/keys" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_keys_after no_log: true - name: Build key name→id lookup ansible.builtin.set_fact: _semaphore_key_ids: "{{ dict(_semaphore_keys_after.json | map(attribute='name') | zip(_semaphore_keys_after.json | map(attribute='id'))) }}" # --------------------------------------------------------------------------- # Repositories — find-or-create # --------------------------------------------------------------------------- - name: List existing repositories ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/repositories" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_repos no_log: true - name: Create missing repositories ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/repositories" method: POST headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" body_format: json # Build the body via Jinja so integer fields stay integers (the uri # module preserves dict value types from a Jinja expression but # coerces quoted "{{ ... }}" tokens in inline YAML to strings, # which Semaphore rejects with a 400). body: >- {{ { 'name': item.name, 'project_id': _semaphore_project_id | int, 'git_url': item.git_url, 'git_branch': item.git_branch | default('main'), 'ssh_key_id': _semaphore_key_ids[item.ssh_key] | int } }} status_code: [201, 204] loop: "{{ semaphore_config.repositories }}" loop_control: label: "{{ item.name }}" when: item.name not in (_semaphore_repos.json | map(attribute='name') | list) - name: Re-list repositories with assigned ids ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/repositories" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_repos_after - name: Build repo name→id lookup ansible.builtin.set_fact: _semaphore_repo_ids: "{{ dict(_semaphore_repos_after.json | map(attribute='name') | zip(_semaphore_repos_after.json | map(attribute='id'))) }}" # --------------------------------------------------------------------------- # Inventories — find-or-create # --------------------------------------------------------------------------- - name: List existing inventories ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/inventory" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_inv no_log: true - name: Create missing inventories ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/inventory" method: POST headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" body_format: json body: >- {{ { 'name': item.name, 'project_id': _semaphore_project_id | int, 'type': item.type | default('file'), 'inventory': item.inventory_file, 'repository_id': _semaphore_repo_ids[item.repository] | int, 'ssh_key_id': _semaphore_key_ids[item.ssh_key] | int, 'become_key_id': _semaphore_key_ids[item.become_key | default(item.ssh_key)] | int } }} status_code: [201, 204] loop: "{{ semaphore_config.inventories }}" loop_control: label: "{{ item.name }}" when: item.name not in (_semaphore_inv.json | map(attribute='name') | list) - name: Re-list inventories with assigned ids ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/inventory" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_inv_after - name: Build inventory name→id lookup ansible.builtin.set_fact: _semaphore_inv_ids: "{{ dict(_semaphore_inv_after.json | map(attribute='name') | zip(_semaphore_inv_after.json | map(attribute='id'))) }}" # --------------------------------------------------------------------------- # Environments — find-or-create # --------------------------------------------------------------------------- - name: List existing environments ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/environment" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_envs - name: Create missing environments ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/environment" method: POST headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" body_format: json body: >- {{ { 'name': item.name, 'project_id': _semaphore_project_id | int, 'env': (item.env | default({}) | to_json), 'json': (item.extra_vars | default({}) | to_json) } }} status_code: [201, 204] loop: "{{ semaphore_config.environments }}" loop_control: label: "{{ item.name }}" when: item.name not in (_semaphore_envs.json | map(attribute='name') | list) - name: Re-list environments with assigned ids ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/environment" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_envs_after - name: Build environment name→id lookup ansible.builtin.set_fact: _semaphore_env_ids: "{{ dict(_semaphore_envs_after.json | map(attribute='name') | zip(_semaphore_envs_after.json | map(attribute='id'))) }}" # --------------------------------------------------------------------------- # Templates — find-or-create # --------------------------------------------------------------------------- - name: List existing templates ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/templates" method: GET headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" return_content: true register: _semaphore_templates - name: Create missing templates ansible.builtin.uri: url: "{{ semaphore_api_url }}/project/{{ _semaphore_project_id }}/templates" method: POST headers: "{{ _semaphore_headers }}" validate_certs: "{{ semaphore_api_validate_certs }}" body_format: json body: >- {{ { 'project_id': _semaphore_project_id | int, 'name': item.name, 'description': item.description | default(''), 'app': item.app | default('ansible'), 'playbook': item.playbook, 'arguments': item.arguments | default('[]'), 'allow_override_args_in_task': item.allow_override_args | default(false), 'inventory_id': _semaphore_inv_ids[item.inventory] | int, 'repository_id': _semaphore_repo_ids[item.repository] | int, 'environment_id': _semaphore_env_ids[item.environment] | int, 'survey_vars': item.survey_vars | default([]) } | combine( {'vault_key_id': _semaphore_key_ids[item.vault_password] | int} if item.vault_password is defined else {} ) }} status_code: [201, 204] loop: "{{ semaphore_config.templates }}" loop_control: label: "{{ item.name }}" when: item.name not in (_semaphore_templates.json | map(attribute='name') | list) # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- - name: Report configuration status ansible.builtin.debug: msg: - "Semaphore project: {{ semaphore_config.project.name }} (id={{ _semaphore_project_id }})" - "Keys: {{ _semaphore_key_ids | length }}" - "Repositories: {{ _semaphore_repo_ids | length }}" - "Inventories: {{ _semaphore_inv_ids | length }}" - "Environments: {{ _semaphore_env_ids | length }}" - "Configuration applied successfully."