The 'uri' module with body_format=json was sending integer fields as
strings when they came from quoted Jinja expressions in inline YAML
(e.g. ssh_key_id: "{{ ... | int }}"), because YAML loads the quoted
form as a string and Ansible doesn't coerce back. Semaphore rejects
that with HTTP 400.
Build each body as a Jinja dict literal in a folded scalar so types
survive: integers stay integers, strings stay strings.
Also restores no_log: true on key creation now that we're past the
debug round.
330 lines
12 KiB
YAML
330 lines
12 KiB
YAML
---
|
|
# ============================================================================
|
|
# 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."
|