--- # ============================================================================ # 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 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 }}" # Semaphore stores both env (process env vars) and json (extra # variables). Pass an empty object for json unless someone # extends the schema later. 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 }}" vault_key_id: "{{ _semaphore_key_ids[item.vault_password] | int if item.vault_password is defined else omit }}" survey_vars: "{{ item.survey_vars | default([]) }}" 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."