--- - name: Create a test Portainer stack via SSH tunnel hosts: localhost connection: local gather_facts: false vars: # Bastion SSH bastion_host: bdc.cci17.fr bastion_port: 17100 bastion_user: ansible # Portainer via tunnel (local fixe) local_portainer_url: "https://127.0.0.1:9443" # Stack test stack_name: "test-stack-semaphore" # type=2 => Docker standalone (Compose) ; type=1 => Swarm stack_type: 2 # Endpoint: si tu connais l'ID, mets-le ici (sinon on prend le 1er endpoint) endpoint_id: "" # Tunnel ssh_control_socket: "/tmp/ssh-tunnel-portainer-9443.sock" ssh_log: "/tmp/ssh-tunnel-portainer-9443.log" # Compose minimal pour test stack_compose: | services: web: image: nginx:alpine restart: unless-stopped # Pas d'exposition de port : on teste juste que la stack se crée et démarre # Si tu veux la voir, on pourra ajouter Traefik plus tard. tasks: - block: - name: Check local port 9443 is not already in use shell: "ss -lnt | grep -q ':9443 '" register: port_in_use changed_when: false failed_when: port_in_use.rc == 0 - name: Open SSH tunnel to Portainer (fixed local port 9443) shell: | rm -f "{{ ssh_log }}" ssh -p {{ bastion_port }} \ -o ExitOnForwardFailure=yes \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ServerAliveInterval=10 \ -o ServerAliveCountMax=3 \ -o ConnectTimeout=10 \ -M -S {{ ssh_control_socket }} \ -f -N \ -L 127.0.0.1:9443:10.30.0.151:9443 \ {{ bastion_user }}@{{ bastion_host }} \ > "{{ ssh_log }}" 2>&1 changed_when: true - name: Wait for local tunnel port 9443 wait_for: host: 127.0.0.1 port: 9443 delay: 1 timeout: 20 - name: Check Portainer is reachable uri: url: "{{ local_portainer_url }}/api/status" method: GET validate_certs: false headers: X-API-Key: "{{ lookup('env','PORTAINER_API_KEY') }}" status_code: [200] register: status_check - name: Get endpoints list uri: url: "{{ local_portainer_url }}/api/endpoints" method: GET validate_certs: false headers: X-API-Key: "{{ lookup('env','PORTAINER_API_KEY') }}" status_code: [200] register: endpoints_resp - name: Pick endpoint_id (use provided or first) set_fact: endpoint_id_effective: >- {{ (endpoint_id | string | length > 0) | ternary(endpoint_id | int, (endpoints_resp.json[0].Id | int)) }} - name: Create test stack (Compose, method=string) uri: url: "{{ local_portainer_url }}/api/stacks?type={{ stack_type }}&method=string&endpointId={{ endpoint_id_effective }}" method: POST validate_certs: false headers: X-API-Key: "{{ lookup('env','PORTAINER_API_KEY') }}" Content-Type: "application/json" body_format: json body: Name: "{{ stack_name }}" StackFileContent: "{{ stack_compose }}" Env: [] # Si la stack existe déjà, Portainer peut renvoyer une erreur 409 status_code: [200, 201, 409] register: create_stack_resp - name: Result debug: msg: - "✅ API status: {{ status_check.status }}" - "✅ endpointId utilisé: {{ endpoint_id_effective }}" - "✅ Create stack HTTP status: {{ create_stack_resp.status }}" - "ℹ️ Si status=409: la stack '{{ stack_name }}' existe déjà." always: - name: Close SSH tunnel shell: > test -S {{ ssh_control_socket }} && ssh -p {{ bastion_port }} -S {{ ssh_control_socket }} -O exit {{ bastion_user }}@{{ bastion_host }} || true ignore_errors: true