diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore
deleted file mode 100644
index 3d787a2..0000000
--- a/infrastructure/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/bin/
-/node_modules/
-.local/
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/assets/20auto-upgrades b/infrastructure/docker-cluster/ansible/assets/20auto-upgrades
new file mode 100644
index 0000000..7587020
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/assets/20auto-upgrades
@@ -0,0 +1,4 @@
+APT::Periodic::Update-Package-Lists "1";
+APT::Periodic::Download-Upgradeable-Packages "1";
+APT::Periodic::AutocleanInterval "7";
+APT::Periodic::Unattended-Upgrade "1";
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/assets/50unattended-upgrades b/infrastructure/docker-cluster/ansible/assets/50unattended-upgrades
new file mode 100644
index 0000000..76dd90b
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/assets/50unattended-upgrades
@@ -0,0 +1,9 @@
+Unattended-Upgrade::Allowed-Origins {
+ "${distro_id}:${distro_codename}-security";
+};
+Unattended-Upgrade::Package-Blacklist {
+};
+Unattended-Upgrade::DevRelease "false";
+Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
+Unattended-Upgrade::Remove-Unused-Dependencies "true";
+Unattended-Upgrade::Automatic-Reboot "false";
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/group_vars/all.yml b/infrastructure/docker-cluster/ansible/group_vars/all.yml
new file mode 100644
index 0000000..78c8f40
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/group_vars/all.yml
@@ -0,0 +1,16 @@
+admin_user: 'admin'
+# Passwords are generated by this command: ansible all -i localhost, -m debug -a 'msg={{ "" | password_hash("sha512", "") }}'
+admin_password: '$6$rounds=656000$C43bVkou378rvtx8$J1KJZ6uw/KnLGc7nFSixAdsNDKUaSw.SGA38ZtZHwHB8eB5a/rRm1qJ8QR/wIxxylLJ0i7ByaW9CAjpbDYaQH/' # 7V#4kW9RvJfmxAnJV8zWbLrp%%SXRJ#kvGkowR98TY # salt=C43bVkou378rvtx8
+cephadm_password: '$6$rounds=656000$nE9mLm8AbEshiHr7$9HkqmR2Fr.0gKXwzml7CPhaWY9Ul4t0hdHiFldAI8ShISp6yB0NvehbTDnCKnfBO5UTL591IsNKda54WMa6MS.' # fM5$kZP4d!%$ZocBkhj85q^2GuwwN35YHxS!neC3AM # salt=nE9mLm8AbEshiHr7
+ssh_port: 22
+cephfs_name: "shared-fs"
+ceph_osd_device: "/dev/sdb"
+public_interface: 'eth0'
+private_interface: 'enp7s0'
+authorized_keys:
+ - 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtYTptTN9ggoy0aUKXmxaPKpclEn86jM7s5UtTw1JJI'
+hetzner_api_key: "wqwAS4LINKOxgkyeECrHFOq1MRiOhFe236UyNBjs7fIhmOcKrXCmuncVPyt0S7Ac"
+
+ceph_volume: /mnt/cephfs
+main_domain: avicenna.hamburg
+traefik_public_net: traefik_public
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/inventory.ini b/infrastructure/docker-cluster/ansible/inventory.ini
new file mode 100644
index 0000000..9d9f87f
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/inventory.ini
@@ -0,0 +1,10 @@
+[all:children]
+managers
+workers
+
+[managers]
+manager-node-1 ansible_host=188.245.52.124 ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=./.local/secure/private_key
+manager-node-2 ansible_host=116.202.18.42 ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=./.local/secure/private_key
+manager-node-3 ansible_host=167.235.227.234 ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=./.local/secure/private_key
+
+[workers]
diff --git a/infrastructure/docker-cluster/ansible/playbook.yml b/infrastructure/docker-cluster/ansible/playbook.yml
new file mode 100644
index 0000000..724ced4
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/playbook.yml
@@ -0,0 +1,48 @@
+---
+# - name: Nodes initialisieren und härten
+# hosts: all
+# become: true
+
+# roles:
+# - role: common
+# tags: common
+# - role: ssh_hardening
+# tags: ssh
+# - role: ufw_firewall
+# tags: firewall
+# - role: fail2ban
+# tags: fail2ban
+# handlers:
+# - name: restart sshd
+# ansible.builtin.service:
+# name: ssh
+# state: restarted
+# - name: restart fail2ban
+# ansible.builtin.service:
+# name: fail2ban
+# state: restarted
+# - name: Setup Ceph Cluster and CephFS
+# hosts: all
+# become: true
+# roles:
+# - role: ceph_setup
+
+# - name: Docker Swarm initialisieren
+# hosts: all
+# become: true
+# roles:
+# - role: docker_swarm
+
+- name: Infrastruktur Dienste bereitstellen
+ hosts: all
+ gather_facts: true
+ roles:
+ - traefik
+ # - authentik
+ # # - dockge
+ - portainer
+ # - signoz
+ # - signoz-infra
+ # - leantime
+ - kestra
+ - hyperdx
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/ceph_setup/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/ceph_setup/tasks/main.yml
new file mode 100644
index 0000000..91f7252
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/ceph_setup/tasks/main.yml
@@ -0,0 +1,93 @@
+---
+- name: CEPH | Private IP des ersten Managers ermitteln
+ ansible.builtin.set_fact:
+ ceph_bootstrap_ip: "{{ hostvars[inventory_hostname]['ansible_' + private_interface]['ipv4']['address'] }}"
+ when: inventory_hostname == groups['managers'][0]
+
+- name: CEPH | Cluster auf dem ersten Manager initialisieren (Bootstrap)
+ ansible.builtin.command:
+ cmd: "cephadm bootstrap --mon-ip {{ ceph_bootstrap_ip }}"
+ creates: /etc/ceph/ceph.conf
+ when: inventory_hostname == groups['managers'][0]
+
+- name: CEPH | Öffentlichen SSH-Schlüssel von cephadm abrufen
+ ansible.builtin.command: "cephadm shell -- ceph cephadm get-pub-key"
+ register: cephadm_pub_key
+ changed_when: false
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+
+- name: CEPH | Öffentlichen Schlüssel von cephadm auf allen Knoten für root verteilen
+ ansible.posix.authorized_key:
+ user: root
+ key: "{{ hostvars[groups['managers'][0]]['cephadm_pub_key'].stdout }}"
+ state: present
+ key_options: 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
+
+- name: CEPH | Andere Knoten zum Ceph-Cluster hinzufügen
+ ansible.builtin.command:
+ cmd: "ceph orch host add {{ item }} {{ hostvars[item]['ansible_' + private_interface]['ipv4']['address'] }}"
+ loop: "{{ groups['all'] }}"
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+
+- name: CEPH | Prüfen, ob bereits OSDs (Speichergeräte) vorhanden sind
+ ansible.builtin.command: "ceph osd ls"
+ register: existing_osds
+ changed_when: false
+ failed_when: false
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+
+- name: CEPH | Spezifische Festplatte ({{ ceph_osd_device }}) auf jedem Knoten als OSD hinzufügen
+ ansible.builtin.command: "ceph orch daemon add osd {{ item }}:{{ ceph_osd_device }}"
+ loop: "{{ groups['all'] }}"
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+ when: existing_osds.stdout | length == 0
+
+- name: CEPH | Prüfen, ob CephFS bereits existiert
+ ansible.builtin.command: "ceph fs ls -f json"
+ register: cephfs_list
+ changed_when: false
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+
+- name: CEPH | CephFS Pools und Dateisystem erstellen, falls nicht vorhanden
+ block:
+ - name: Metadaten-Pool für CephFS erstellen
+ ansible.builtin.command: "ceph osd pool create {{ cephfs_name }}_metadata"
+ - name: Daten-Pool für CephFS erstellen
+ ansible.builtin.command: "ceph osd pool create {{ cephfs_name }}_data"
+ - name: CephFS-Dateisystem erstellen
+ ansible.builtin.command: "ceph fs new {{ cephfs_name }} {{ cephfs_name }}_metadata {{ cephfs_name }}_data"
+ when: cephfs_list.stdout | from_json | length == 0
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+
+- name: CEPH | Metadaten-Server (MDS) für CephFS starten
+ ansible.builtin.command: "ceph orch apply mds {{ cephfs_name }} --placement=2"
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+ when: cephfs_list.stdout | from_json | length == 0
+
+- name: CEPH | Ceph Admin-Schlüssel für das Mounten abrufen
+ ansible.builtin.command: "ceph auth get-key client.admin"
+ register: ceph_admin_key
+ changed_when: false
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
+
+- name: CEPH | Mount-Punkt für CephFS erstellen
+ ansible.builtin.file:
+ path: /mnt/cephfs
+ state: directory
+ mode: '0755'
+
+- name: CEPH | CephFS auf allen Knoten mounten (und in /etc/fstab eintragen)
+ ansible.posix.mount:
+ path: /mnt/cephfs
+ src: "{{ hostvars[groups['managers'][0]]['ceph_bootstrap_ip'] }}:/"
+ fstype: ceph
+ opts: "name=admin,secret={{ ceph_admin_key.stdout }}"
+ state: mounted
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/common/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/common/tasks/main.yml
new file mode 100644
index 0000000..71a3802
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/common/tasks/main.yml
@@ -0,0 +1,113 @@
+---
+- name: COMMON | Systempakete aktualisieren und upgraden
+ ansible.builtin.apt:
+ update_cache: true
+ upgrade: dist
+ autoremove: true
+ autoclean: true
+
+- name: COMMON | Notwendige Pakete installieren
+ ansible.builtin.apt:
+ name:
+ - ufw
+ - fail2ban
+ - unattended-upgrades
+ - apt-listchanges
+ - docker-ce
+ - python3-pip
+ - chrony
+ - lvm2
+ - cephadm
+ - ceph-common
+ state: present
+
+- name: COMMON | Chrony Dienst starten und aktivieren
+ ansible.builtin.service:
+ name: chronyd
+ state: started
+ enabled: true
+
+- name: COMMON | Docker Dienst starten und aktivieren
+ ansible.builtin.service:
+ name: docker
+ state: started
+ enabled: true
+
+- name: COMMON | Einen dedizierten Admin-Benutzer erstellen
+ ansible.builtin.user:
+ name: "{{ admin_user }}"
+ password: "{{ admin_password}}"
+ shell: /bin/bash
+ groups: sudo,docker
+ append: true
+ state: present
+
+- name: COMMON | SSH-Schlüssel für den Admin-Benutzer einrichten
+ ansible.posix.authorized_key:
+ user: "{{ admin_user }}"
+ key: "{{ item }}"
+ state: present
+ with_items: "{{ authorized_keys }}"
+
+- name: COMMON | Ensure group "cephadm" exists
+ group:
+ name: cephadm
+ state: present
+
+- name: COMMON | cephadm-Benutzer erstellen
+ ansible.builtin.user:
+ name: "cephadm"
+ password: "{{ cephadm_password }}"
+ shell: /bin/bash
+ groups: sudo,docker
+ append: yes
+ state: present
+
+- name: COMMON | .ssh Verzeichnis für cephadm-Benutzer erstellen
+ ansible.builtin.file:
+ path: /home/cephadm/.ssh
+ state: directory
+
+- name: COMMON | Passwortloses Sudo für cephadm-Benutzer erlauben
+ ansible.builtin.copy:
+ dest: "/etc/sudoers.d/91-cephadm-nopasswd"
+ content: "cephadm ALL=(ALL) NOPASSWD: ALL"
+ mode: '0440'
+ validate: 'visudo -cf %s'
+
+- name: COMMON | ed25519 SSH-Schlüssel für cephadm-Benutzer generieren (nur auf dem ersten Manager)
+ community.crypto.openssh_keypair:
+ path: /home/cephadm/.ssh/id_ed25519
+ type: ed25519
+ owner: cephadm
+ group: cephadm
+ mode: '0600'
+ when: inventory_hostname == groups['managers'][0]
+
+- name: COMMON | Öffentlichen SSH-Schlüssel von cephadm abrufen
+ ansible.builtin.slurp:
+ src: /home/cephadm/.ssh/id_ed25519.pub
+ register: cephadm_ssh_pub_key
+ when: inventory_hostname == groups['managers'][0]
+
+- name: COMMON | Öffentlichen SSH-Schlüssel von cephadm auf allen Knoten verteilen
+ ansible.posix.authorized_key:
+ user: cephadm
+ key: "{{ hostvars[groups['managers'][0]]['cephadm_ssh_pub_key']['content'] | b64decode }}"
+ state: present
+
+- name: COMMON | Automatische Sicherheitsupdates konfigurieren
+ ansible.builtin.copy:
+ src: assets/50unattended-upgrades
+ dest: /etc/apt/apt.conf.d/50unattended-upgrades
+ owner: root
+ group: root
+ mode: '0644'
+
+- name: COMMON | Periodische Auto-Updates aktivieren
+ ansible.builtin.copy:
+ src: assets/20auto-upgrades
+ dest: /etc/apt/apt.conf.d/20auto-upgrades
+ owner: root
+ group: root
+ mode: '0644'
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/docker_swarm/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/docker_swarm/tasks/main.yml
new file mode 100644
index 0000000..f18b250
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/docker_swarm/tasks/main.yml
@@ -0,0 +1,58 @@
+---
+- name: SWARM | Ensure Docker SDK for Python is installed
+ ansible.builtin.apt:
+ name: python3-docker
+ state: present
+
+- name: SWARM | Get interface IP address for the manager
+ ansible.builtin.set_fact:
+ manager_ip: "{{ hostvars[inventory_hostname]['ansible_' + private_interface]['ipv4']['address'] }}"
+ when: inventory_hostname == groups['managers'][0]
+
+- name: SWARM | Initialize the Docker Swarm
+ community.docker.docker_swarm:
+ state: present
+ advertise_addr: "{{ manager_ip }}"
+ when: inventory_hostname == groups['managers'][0]
+ register: swarm_init_result
+
+- name: SWARM | Get the join tokens
+ community.docker.docker_swarm_info:
+ register: swarm_info
+ when: inventory_hostname == groups['managers'][0]
+
+- name: SWARM | Verify that join tokens were fetched
+ ansible.builtin.assert:
+ that:
+ - swarm_info is defined
+ - swarm_info.swarm_facts is defined
+ - swarm_info.swarm_facts.JoinTokens.Manager is defined
+ - swarm_info.swarm_facts.JoinTokens.Worker is defined
+ fail_msg: "Konnte die Join-Tokens vom Swarm Manager nicht abrufen. Ist der Swarm korrekt initialisiert?"
+ success_msg: "Join-Tokens erfolgreich abgerufen."
+ when: inventory_hostname == groups['managers'][0]
+
+- name: SWARM | Join manager nodes to the Swarm
+ community.docker.docker_swarm:
+ state: join
+ remote_addrs: [ "{{ hostvars[groups['managers'][0]]['manager_ip'] }}:2377" ]
+ join_token: "{{ hostvars[groups['managers'][0]]['swarm_info']['swarm_facts']['JoinTokens']['Manager'] }}"
+ when: inventory_hostname in groups['managers']
+
+- name: SWARM | Join worker nodes to the Swarm
+ community.docker.docker_swarm:
+ state: join
+ remote_addrs: [ "{{ hostvars[groups['managers'][0]]['manager_ip'] }}:2377" ]
+ join_token: "{{ hostvars[groups['managers'][0]]['swarm_info']['swarm_facts']['JoinTokens']['Worker'] }}"
+ when: inventory_hostname in groups['workers']
+
+- name: SWARM | Verify Swarm Cluster State (run on manager)
+ ansible.builtin.command: docker node ls
+ register: swarm_nodes
+ changed_when: false
+ when: inventory_hostname == groups['managers'][0]
+
+- name: SWARM | Display cluster state
+ ansible.builtin.debug:
+ msg: "{{ swarm_nodes.stdout_lines }}"
+ when: inventory_hostname == groups['managers'][0]
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/fail2ban/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/fail2ban/tasks/main.yml
new file mode 100644
index 0000000..275505d
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/fail2ban/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+- name: FAIL2BAN | Eine lokale Jail-Konfiguration erstellen
+ ansible.builtin.template:
+ src: jail.local.j2
+ dest: /etc/fail2ban/jail.local
+ owner: root
+ group: root
+ mode: '0644'
+ notify: restart fail2ban
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/fail2ban/templates/jail.local.j2 b/infrastructure/docker-cluster/ansible/roles/fail2ban/templates/jail.local.j2
new file mode 100644
index 0000000..0039dca
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/fail2ban/templates/jail.local.j2
@@ -0,0 +1,8 @@
+[DEFAULT]
+bantime = 1h
+findtime = 10m
+maxretry = 5
+
+[sshd]
+enabled = true
+port = {{ ssh_port }}
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/hyperdx/files/config.xml b/infrastructure/docker-cluster/ansible/roles/hyperdx/files/config.xml
new file mode 100644
index 0000000..84eca4b
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/hyperdx/files/config.xml
@@ -0,0 +1,175 @@
+
+
+
+ debug
+ true
+
+
+
+
+ 0.0.0.0
+ 8123
+ 9000
+ ch-server
+ 9009
+
+ 4096
+ 64
+ 100
+ 8589934592
+ 5368709120
+
+ /var/lib/clickhouse/
+ /var/lib/clickhouse/tmp/
+ /var/lib/clickhouse/user_files/
+
+
+
+ users.xml
+
+
+
+ default
+ default
+ UTC
+ false
+
+
+
+ /metrics
+ 9363
+ true
+ true
+ true
+ true
+
+
+
+
+ system
+
+ 7500
+
+
+
+
+ system
+
+ 7500
+ 1000
+
+
+
+
+ system
+
+
+ 7000
+
+
+
+
+
+
+ engine MergeTree
+ partition by toYYYYMM(finish_date)
+ order by (finish_date, finish_time_us, trace_id)
+
+ system
+
+ 7500
+
+
+
+
+
+ system
+
+
+
+ 1000
+
+
+
+
+ system
+
+
+ toYYYYMM(event_date)
+ 7500
+
+
+
+
+ system
+
+ toYYYYMM(event_date)
+ 7500
+
+
+
+
+ system
+
+
+ toYYYYMM(event_date)
+ 7500
+
+
+
+
+ system
+
+ toYYYYMM(event_date)
+ 7500
+
+
+
+
+ system
+
+ toYYYYMM(event_date)
+ 7500
+
+
+
+
+
+
+ ch-server
+ 9000
+
+
+
+
+
+
+ /clickhouse/task_queue/ddl
+
+
+ /var/lib/clickhouse/format_schemas/
+
diff --git a/infrastructure/docker-cluster/ansible/roles/hyperdx/files/users.xml b/infrastructure/docker-cluster/ansible/roles/hyperdx/files/users.xml
new file mode 100644
index 0000000..80c8d4f
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/hyperdx/files/users.xml
@@ -0,0 +1,51 @@
+
+
+
+
+ 10000000000
+ 0
+ in_order
+ 1
+
+
+
+
+
+
+ default
+
+ ::/0
+
+ default
+
+
+ api
+ default
+
+ ::/0
+
+ default
+
+
+ worker
+ default
+
+ ::/0
+
+ default
+
+
+
+
+
+
+ 3600
+ 0
+ 0
+ 0
+ 0
+ 0
+
+
+
+
diff --git a/infrastructure/docker-cluster/ansible/roles/hyperdx/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/hyperdx/tasks/main.yml
new file mode 100644
index 0000000..e0e3e34
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/hyperdx/tasks/main.yml
@@ -0,0 +1,46 @@
+---
+- name: HYPERDX | Verzeichnisse erstellen
+ ansible.builtin.file:
+ path: "/mnt/cephfs/hyperdx/{{ item.path }}"
+ state: directory
+ owner: "{{ item.uid }}"
+ group: "{{ item.gid }}"
+ mode: '0755'
+ recurse: yes
+ loop:
+ - { path: 'mongo', uid: 999, gid: 999 } # MongoDB Standard
+ - { path: 'clickhouse/data', uid: 101, gid: 101 } # ClickHouse Standard
+ - { path: 'clickhouse/logs', uid: 101, gid: 101 }
+ - { path: 'clickhouse/config', uid: 101, gid: 101 }
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: HYPERDX | ClickHouse Konfiguration kopieren
+ ansible.builtin.copy:
+ src: "{{ item }}"
+ dest: "/mnt/cephfs/hyperdx/clickhouse/config/"
+ owner: 101
+ group: 101
+ mode: '0644'
+ loop:
+ - files/config.xml # Lokal in deinem Ansible Repo
+ - files/users.xml
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: HYPERDX | Generate Compose file
+ ansible.builtin.template:
+ src: docker-compose.yml.j2
+ dest: '{{ data_dir }}/hyperdx.yml'
+ mode: 0644
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: HYPERDX | Deploy stack
+ community.docker.docker_stack:
+ state: present
+ name: hyperdx
+ compose:
+ - '{{ data_dir }}/hyperdx.yml'
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
diff --git a/infrastructure/docker-cluster/ansible/roles/hyperdx/templates/docker-compose.yml.j2 b/infrastructure/docker-cluster/ansible/roles/hyperdx/templates/docker-compose.yml.j2
new file mode 100644
index 0000000..9fc8b24
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/hyperdx/templates/docker-compose.yml.j2
@@ -0,0 +1,100 @@
+version: '3.9'
+
+services:
+ db:
+ image: mongo:5.0.14-focal
+ volumes:
+ - "{{ data_dir }}/mongo:/data/db"
+ networks:
+ - internal
+ deploy:
+ mode: replicated
+ replicas: 1
+ # placement:
+ # constraints: [node.role == worker] # DBs besser auf Workern lassen wenn möglich
+
+ otel-collector:
+ image: "clickhouse/clickstack-otel-collector:2"
+ environment:
+ CLICKHOUSE_ENDPOINT: 'tcp://ch-server:9000?dial_timeout=10s'
+ HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE: "default"
+ HYPERDX_LOG_LEVEL: "{{ hyperdx_log_level | default('info') }}"
+ OPAMP_SERVER_URL: 'http://app:{{ hyperdx_opamp_port | default(4320) }}'
+ # Ingestion Ports direkt via Swarm Mesh exponieren für Performance
+ ports:
+ - "4317:4317" # OTLP gRPC
+ - "4318:4318" # OTLP HTTP
+ # - "8888:8888" # Metrics (optional)
+ networks:
+ - internal
+ - shared-observability
+ - traefik_public
+ deploy:
+ mode: replicated
+ replicas: 1
+ labels:
+ - "traefik.enable=true"
+ - "traefik.docker.network=traefik_public"
+ - "traefik.http.routers.otel-collector.rule=Host(`{{ otlp_domain }}`)"
+ - "traefik.http.routers.otel-collector.entrypoints=https"
+ - "traefik.http.routers.otel-collector.tls.certresolver=main"
+ - "traefik.http.services.otel-collector.loadbalancer.server.port=4318"
+
+ app:
+ image: "hyperdx/hyperdx:2"
+ environment:
+ # URLs anpassen für Traefik Erreichbarkeit
+ FRONTEND_URL: "https://{{ hdx_domain }}"
+ HYPERDX_APP_URL: "https://{{ hdx_domain }}"
+
+ HYPERDX_API_KEY: "{{ hyperdx_api_key }}"
+ HYPERDX_API_PORT: "{{ hyperdx_api_port | default(8000) }}"
+ HYPERDX_APP_PORT: "{{ hyperdx_app_port | default(8080) }}"
+ HYPERDX_LOG_LEVEL: "{{ hyperdx_log_level | default('info') }}"
+ MINER_API_URL: 'http://miner:5123' # Falls miner benötigt wird (in original compose nicht definiert?)
+ MONGO_URI: 'mongodb://db:27017/hyperdx'
+ SERVER_URL: "http://127.0.0.1:{{ hyperdx_api_port | default(8000) }}"
+ OPAMP_PORT: "{{ hyperdx_opamp_port | default(4320) }}"
+ OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel-collector:4318'
+ OTEL_SERVICE_NAME: 'hdx-oss-app'
+ USAGE_STATS_ENABLED: "{{ usage_stats_enabled | default('false') }}"
+ # Clickhouse Connection String (Default User/Pass from Clickhouse Image)
+ DEFAULT_CONNECTIONS: >-
+ [{"name":"Local ClickHouse","host":"http://ch-server:8123","username":"default","password":""}]
+ DEFAULT_SOURCES: '{{ hyperdx_default_sources | to_json }}'
+ networks:
+ - internal
+ - traefik_public
+ deploy:
+ labels:
+ - "traefik.enable=true"
+ - "traefik.docker.network=traefik_public"
+ - "traefik.http.routers.hyperdx.rule=Host(`{{ subdomain }}.{{ main_domain }}`)"
+ - "traefik.http.routers.hyperdx.entrypoints=https"
+ - "traefik.http.routers.hyperdx.tls.certresolver=main"
+ - "traefik.http.services.hyperdx.loadbalancer.server.port={{ hyperdx_app_port | default(8080) }}"
+
+ ch-server:
+ image: clickhouse/clickhouse-server:25.6-alpine
+ environment:
+ CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
+ volumes:
+ - "{{ data_dir }}/clickhouse/config/config.xml:/etc/clickhouse-server/config.xml"
+ - "{{ data_dir }}/clickhouse/config/users.xml:/etc/clickhouse-server/users.xml"
+ - "{{ data_dir }}/clickhouse/data:/var/lib/clickhouse"
+ - "{{ data_dir }}/clickhouse/logs:/var/log/clickhouse-server"
+ deploy:
+ mode: replicated
+ replicas: 1
+ # placement:
+ # constraints: [node.role == worker]
+ networks:
+ - internal
+
+networks:
+ internal:
+ driver: overlay
+ traefik_public:
+ external: true
+ shared-observability:
+ external: true
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/hyperdx/vars/main.yml b/infrastructure/docker-cluster/ansible/roles/hyperdx/vars/main.yml
new file mode 100644
index 0000000..cb95a6e
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/hyperdx/vars/main.yml
@@ -0,0 +1,107 @@
+data_dir: "{{ ceph_volume }}/hyperdx"
+subdomain: "hdx"
+
+hdx_domain: "{{ subdomain }}.{{ main_domain }}"
+otlp_domain: "otlp.{{ main_domain }}"
+
+# Generiere einen sicheren Key: `openssl rand -hex 16`
+hyperdx_api_key: "2bb00477e073d6e2b72e310c39785fa1"
+hyperdx_api_port: 8000
+hyperdx_app_port: 8080
+hyperdx_log_level: "info"
+hyperdx_opamp_port: 4320
+
+usage_stats_enabled: "false"
+
+# vars/main.yml
+
+# ... (deine anderen Variablen) ...
+
+# Definition der Datenquellen für das Frontend
+hyperdx_default_sources:
+ - name: "Logs"
+ kind: "log"
+ from:
+ databaseName: "default"
+ tableName: "otel_logs"
+ timestampValueExpression: "TimestampTime"
+ displayedTimestampValueExpression: "Timestamp"
+ implicitColumnExpression: "Body"
+ serviceNameExpression: "ServiceName"
+ bodyExpression: "Body"
+ eventAttributesExpression: "LogAttributes"
+ resourceAttributesExpression: "ResourceAttributes"
+ defaultTableSelectExpression: "Timestamp,ServiceName,SeverityText,Body"
+ severityTextExpression: "SeverityText"
+ traceIdExpression: "TraceId"
+ spanIdExpression: "SpanId"
+ connection: "Local ClickHouse"
+ traceSourceId: "Traces"
+ sessionSourceId: "Sessions"
+ metricSourceId: "Metrics"
+
+ - name: "Traces"
+ kind: "trace"
+ from:
+ databaseName: "default"
+ tableName: "otel_traces"
+ timestampValueExpression: "Timestamp"
+ displayedTimestampValueExpression: "Timestamp"
+ implicitColumnExpression: "SpanName"
+ serviceNameExpression: "ServiceName"
+ bodyExpression: "SpanName"
+ eventAttributesExpression: "SpanAttributes"
+ resourceAttributesExpression: "ResourceAttributes"
+ defaultTableSelectExpression: "Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName"
+ traceIdExpression: "TraceId"
+ spanIdExpression: "SpanId"
+ durationExpression: "Duration"
+ durationPrecision: 9
+ parentSpanIdExpression: "ParentSpanId"
+ spanNameExpression: "SpanName"
+ spanKindExpression: "SpanKind"
+ statusCodeExpression: "StatusCode"
+ statusMessageExpression: "StatusMessage"
+ connection: "Local ClickHouse"
+ logSourceId: "Logs"
+ sessionSourceId: "Sessions"
+ metricSourceId: "Metrics"
+
+ - name: "Metrics"
+ kind: "metric"
+ from:
+ databaseName: "default"
+ tableName: ""
+ timestampValueExpression: "TimeUnix"
+ resourceAttributesExpression: "ResourceAttributes"
+ metricTables:
+ gauge: "otel_metrics_gauge"
+ histogram: "otel_metrics_histogram"
+ sum: "otel_metrics_sum"
+ _id: "682586a8b1f81924e628e808"
+ id: "682586a8b1f81924e628e808"
+ connection: "Local ClickHouse"
+ logSourceId: "Logs"
+ traceSourceId: "Traces"
+ sessionSourceId: "Sessions"
+
+ - name: "Sessions"
+ kind: "session"
+ from:
+ databaseName: "default"
+ tableName: "hyperdx_sessions"
+ timestampValueExpression: "TimestampTime"
+ displayedTimestampValueExpression: "Timestamp"
+ implicitColumnExpression: "Body"
+ serviceNameExpression: "ServiceName"
+ bodyExpression: "Body"
+ eventAttributesExpression: "LogAttributes"
+ resourceAttributesExpression: "ResourceAttributes"
+ defaultTableSelectExpression: "Timestamp,ServiceName,SeverityText,Body"
+ severityTextExpression: "SeverityText"
+ traceIdExpression: "TraceId"
+ spanIdExpression: "SpanId"
+ connection: "Local ClickHouse"
+ logSourceId: "Logs"
+ traceSourceId: "Traces"
+ metricSourceId: "Metrics"
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/kestra/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/kestra/tasks/main.yml
new file mode 100644
index 0000000..fba5cbe
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/kestra/tasks/main.yml
@@ -0,0 +1,47 @@
+---
+- name: KESTRA | Ensure data directory
+ ansible.builtin.file:
+ path: '{{ data_dir }}/data/data'
+ state: directory
+ mode: '0755'
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: KESTRA | Ensure db directory
+ ansible.builtin.file:
+ path: '{{ data_dir }}/data/db'
+ state: directory
+ mode: '0755'
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: KESTRA | Konfigurationsdatei für tmpfiles.d erstellen
+ ansible.builtin.copy:
+ content: "d /tmp/kestra-wd 0755 root root -"
+ dest: /etc/tmpfiles.d/kestra-wd.conf
+ owner: root
+ group: root
+ mode: '0644'
+
+- name: KESTRA | Create Kestra working directory
+ ansible.builtin.file:
+ path: /tmp/kestra-wd
+ state: directory
+ mode: '0755'
+
+- name: KESTRA | Generate Compose file
+ ansible.builtin.template:
+ src: docker-compose.yml.j2
+ dest: '{{ data_dir }}/kestra.yml'
+ mode: 0644
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: KESTRA | Deploy stack
+ community.docker.docker_stack:
+ state: present
+ name: kestra
+ compose:
+ - /mnt/cephfs/kestra/kestra.yml
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/kestra/templates/docker-compose.yml.j2 b/infrastructure/docker-cluster/ansible/roles/kestra/templates/docker-compose.yml.j2
new file mode 100644
index 0000000..6f39b1d
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/kestra/templates/docker-compose.yml.j2
@@ -0,0 +1,92 @@
+networks:
+ internal:
+ {{ traefik_public_net }}:
+ external: true
+
+services:
+ postgres:
+ image: postgres:17
+ volumes:
+ - {{ data_dir }}/data/db:/var/lib/postgresql/data
+ environment:
+ POSTGRES_DB: kestra
+ POSTGRES_USER: kestra
+ POSTGRES_PASSWORD: k3str4
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
+ interval: 30s
+ timeout: 10s
+ retries: 10
+ networks:
+ - internal
+ deploy:
+ mode: replicated
+ replicas: 1
+
+ kestra:
+ image: kestra/kestra:v0.24.2
+ entrypoint: /bin/bash
+ # Note that this is meant for development only. Refer to the documentation for production deployments of Kestra which runs without a root user.
+ user: "root"
+ command:
+ - -c
+ - /app/kestra server standalone --worker-thread=128
+ volumes:
+ - {{ data_dir }}/data/data:/app/storage
+ - /var/run/docker.sock:/var/run/docker.sock
+ - /tmp/kestra-wd:/tmp/kestra-wd
+ environment:
+ KESTRA_CONFIGURATION: |
+ datasources:
+ postgres:
+ url: jdbc:postgresql://postgres:5432/kestra
+ driverClassName: org.postgresql.Driver
+ username: kestra
+ password: k3str4
+ kestra:
+ tutorialFlows:
+ enabled: false
+ traces:
+ root: DEFAULT
+ micronaut:
+ metrics:
+ export:
+ otlp:
+ enabled: true
+ url: http://signoz_otel-collector:4318/v1/metrics
+ otel:
+ traces:
+ exporter: otlp
+ exporter:
+ otlp:
+ endpoint: http://signoz_otel-collector:4318
+ server:
+ basic-auth:
+ username: ma@coachhamburg.com
+ password: "igyozi9B87yTeiQ6z2sbe8Y4aQLJV58jdaCNu"
+ repository:
+ type: postgres
+ storage:
+ type: local
+ local:
+ base-path: "/app/storage"
+ queue:
+ type: postgres
+ tasks:
+ tmp-dir:
+ path: /tmp/kestra-wd/tmp
+ url: http://localhost:8080/
+ networks:
+ - {{ traefik_public_net }}
+ - internal
+ deploy:
+ mode: replicated
+ replicas: 1
+ labels:
+ - "traefik.enable=true"
+ - "traefik.swarm.network={{ traefik_public_net }}"
+ - "traefik.http.routers.kestra.rule=Host(`{{ subdomain }}.{{ main_domain }}`)"
+ - "traefik.http.routers.kestra.entrypoints=https"
+ - "traefik.http.routers.kestra.tls=true"
+ - "traefik.http.routers.kestra.tls.certresolver=main"
+ - "traefik.http.services.kestra.loadbalancer.server.port=8080"
diff --git a/infrastructure/docker-cluster/ansible/roles/kestra/vars/main.yml b/infrastructure/docker-cluster/ansible/roles/kestra/vars/main.yml
new file mode 100644
index 0000000..269e7f6
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/kestra/vars/main.yml
@@ -0,0 +1,2 @@
+subdomain: workflow
+data_dir: "{{ ceph_volume }}/kestra"
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/portainer/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/portainer/tasks/main.yml
new file mode 100644
index 0000000..ece4462
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/portainer/tasks/main.yml
@@ -0,0 +1,25 @@
+---
+- name: PORTAINER | Ensure data directories
+ ansible.builtin.file:
+ path: '{{ data_dir }}/data'
+ state: directory
+ mode: '0755'
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: PORTAINER | Generate Compose file
+ ansible.builtin.template:
+ src: docker-compose.yml.j2
+ dest: '{{ data_dir }}/portainer.yml'
+ mode: 0644
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: PORTAINER | Deploy stack
+ community.docker.docker_stack:
+ state: present
+ name: portainer
+ compose:
+ - '{{ data_dir }}/portainer.yml'
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
diff --git a/infrastructure/docker-cluster/ansible/roles/portainer/templates/docker-compose.yml.j2 b/infrastructure/docker-cluster/ansible/roles/portainer/templates/docker-compose.yml.j2
new file mode 100644
index 0000000..6c69e60
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/portainer/templates/docker-compose.yml.j2
@@ -0,0 +1,39 @@
+version: '3.2'
+
+services:
+ agent:
+ image: portainer/agent:2.33.5
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - /var/lib/docker/volumes:/var/lib/docker/volumes
+ networks:
+ - {{ traefik_public_net }}
+ deploy:
+ mode: global
+ placement:
+ constraints: [node.platform.os == linux]
+
+ portainer:
+ image: portainer/portainer-ce:2.33.5
+ command: -H tcp://portainer_agent:9001 --tlsskipverify
+ volumes:
+ - {{ data_dir }}/data:/data
+ networks:
+ - {{ traefik_public_net }}
+ deploy:
+ mode: replicated
+ replicas: 1
+ labels:
+ - "traefik.enable=true"
+ - "traefik.swarm.network={{ traefik_public_net }}"
+ - "traefik.http.routers.portainer.rule=Host(`{{ subdomain }}.{{ main_domain }}`)"
+ - "traefik.http.routers.portainer.entrypoints=https"
+ - "traefik.http.routers.portainer.tls=true"
+ - "traefik.http.routers.portainer.tls.certresolver=main"
+ - "traefik.http.services.portainer.loadbalancer.server.port=9000"
+ placement:
+ constraints: [node.hostname == manager-node-3]
+
+networks:
+ {{ traefik_public_net }}:
+ external: true
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/portainer/vars/main.yml b/infrastructure/docker-cluster/ansible/roles/portainer/vars/main.yml
new file mode 100644
index 0000000..4f605e4
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/portainer/vars/main.yml
@@ -0,0 +1,2 @@
+subdomain: port
+data_dir: "{{ ceph_volume }}/portainer"
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/ssh_hardening/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/ssh_hardening/tasks/main.yml
new file mode 100644
index 0000000..8c456d8
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/ssh_hardening/tasks/main.yml
@@ -0,0 +1,30 @@
+---
+- name: SSH | Ensure privilege separation directory exists
+ ansible.builtin.file:
+ path: /run/sshd
+ state: directory
+ mode: '0755'
+
+- name: SSH | Root-Login nur mit Schlüssel erlauben
+ ansible.builtin.lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: '^#?PermitRootLogin'
+ line: 'PermitRootLogin prohibit-password'
+ validate: 'sshd -t -f %s'
+ notify: restart sshd
+
+- name: SSH | Passwort-Authentifizierung deaktivieren
+ ansible.builtin.lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: '^#?PasswordAuthentication'
+ line: 'PasswordAuthentication no'
+ validate: 'sshd -t -f %s'
+ notify: restart sshd
+
+- name: SSH | Leere Passwörter verbieten
+ ansible.builtin.lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: '^#?PermitEmptyPasswords'
+ line: 'PermitEmptyPasswords no'
+ validate: 'sshd -t -f %s'
+ notify: restart sshd
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/config/dynamic/http.middlewares.authentik.toml b/infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/config/dynamic/http.middlewares.authentik.toml
new file mode 100644
index 0000000..8db552f
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/config/dynamic/http.middlewares.authentik.toml
@@ -0,0 +1,18 @@
+[http]
+ [http.middlewares]
+ [http.middlewares.authentik.forwardAuth]
+ address = "http://authentik_server:9000/outpost.goauthentik.io/auth/traefik"
+ trustForwardHeader = true
+ authResponseHeaders = [
+ "X-authentik-username",
+ "X-authentik-groups",
+ "X-authentik-email",
+ "X-authentik-name",
+ "X-authentik-uid",
+ "X-authentik-jwt",
+ "X-authentik-meta-jwks",
+ "X-authentik-meta-outpost",
+ "X-authentik-meta-provider",
+ "X-authentik-meta-app",
+ "X-authentik-meta-version"
+ ]
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/config/traefik.toml b/infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/config/traefik.toml
new file mode 100644
index 0000000..8014a6d
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/config/traefik.toml
@@ -0,0 +1,80 @@
+[global]
+ checkNewVersion = true
+ sendAnonymousUsage = false
+
+[experimental]
+ otlpLogs = true
+
+[core]
+ defaultRuleSyntax = "v2"
+
+[accessLog]
+ filePath = "/logs/access.log"
+ format = "json"
+
+# Enable the Dashboard
+[api]
+ dashboard = true
+
+# Write out Traefik logs
+[log]
+ level = "INFO"
+ format = "json"
+ filePath = "/logs/traefik.log"
+ # [log.otlp.http]
+ # endpoint = "http://signoz_otel-collector:4318/v1/logs"
+
+[entryPoints]
+ [entryPoints.http]
+ address = ":80"
+ [entryPoints.http.http.redirections.entryPoint]
+ to = "https"
+ scheme = "https"
+
+ [entryPoints.https]
+ address = ":443"
+ # [entryPoints.https.http.tls]
+ # certResolver = "main"
+
+# OTel
+# [tracing]
+# serviceName = "traefik"
+# [tracing.otlp.http]
+# endpoint = "http://signoz_otel-collector:4318/v1/traces"
+# [tracing.otlp.http.tls]
+# insecureSkipVerify = true
+
+# # Metrics
+# [metrics]
+# addInternals = false
+# [metrics.otlp]
+# serviceName = "traefik"
+# addEntryPointsLabels = true
+# addRoutersLabels = true
+# addServicesLabels = true
+# [metrics.otlp.http]
+# endpoint = "http://signoz_otel-collector:4318/v1/metrics"
+ # [metrics.otlp.grpc]
+ # endpoint = "monitoring_alloy:4317"
+ # insecure = true
+
+# Let's Encrypt
+[certificatesResolvers.main.acme]
+ email = "ma@coachhamburg.com"
+ storage = "acme.json"
+ # uncomment to use staging CA for testing
+ # caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
+ # [certificatesResolvers.main.acme.tlsChallenge]
+ [certificatesResolvers.main.acme.dnsChallenge]
+ provider = "hetzner"
+ # Uncomment to use HTTP validation, like a caveman!
+ # [certificatesResolvers.main.acme.httpChallenge]
+ # entryPoint = "http"
+
+[providers]
+ [providers.swarm]
+ endpoint = "unix:///var/run/docker.sock"
+ exposedByDefault = false
+ [providers.file]
+ directory = "/etc/traefik/dynamic"
+ watch = true
diff --git a/infrastructure/ansible/resources/traefik/data/acme.json b/infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/data/acme.json
similarity index 100%
rename from infrastructure/ansible/resources/traefik/data/acme.json
rename to infrastructure/docker-cluster/ansible/roles/traefik/files/traefik/data/acme.json
diff --git a/infrastructure/docker-cluster/ansible/roles/traefik/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/traefik/tasks/main.yml
new file mode 100644
index 0000000..6ab1a2c
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/traefik/tasks/main.yml
@@ -0,0 +1,44 @@
+---
+- name: TRAEFIK | Copy Stack Files
+ copy:
+ directory_mode: true
+ src: traefik
+ dest: "{{ ceph_volume }}"
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: TRAEFIK | Generate Compose file
+ ansible.builtin.template:
+ src: docker-compose.yml.j2
+ dest: "{{ data_dir }}/traefik.yml"
+ mode: 0644
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: TRAEFIK | Ensure permissions on acme.json
+ ansible.builtin.file:
+ path: "{{ data_dir }}/data/acme.json"
+ mode: '0600'
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: TRAEFIK | traefik_public Netzwerk erstellen
+ community.docker.docker_network:
+ name: traefik_public
+ driver: overlay
+ state: present
+ attachable: yes
+ ipam_config:
+ - subnet: '172.16.200.0/24'
+ gateway: '172.16.200.1'
+ run_once: true
+ delegate_to: "{{ groups['managers'][0] }}"
+
+- name: TRAEFIK | Deploy app stack
+ community.docker.docker_stack:
+ state: present
+ name: traefik
+ compose:
+ - "{{ data_dir }}/traefik.yml"
+ delegate_to: "{{ groups['managers'][0] }}"
+ run_once: true
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/traefik/templates/docker-compose.yml.j2 b/infrastructure/docker-cluster/ansible/roles/traefik/templates/docker-compose.yml.j2
new file mode 100644
index 0000000..b3d0f0b
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/traefik/templates/docker-compose.yml.j2
@@ -0,0 +1,52 @@
+services:
+ app:
+ image: traefik:v3.6.2
+ ports:
+ - target: 80
+ published: 80
+ protocol: tcp
+ mode: host
+ - target: 443
+ published: 443
+ protocol: tcp
+ mode: host
+ - target: 8080
+ published: 8080
+ protocol: tcp
+ environment:
+ - HETZNER_API_TOKEN={{ hetzner_api_key }}
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - {{ data_dir }}/config:/etc/traefik
+ - {{ data_dir }}/data/logs:/logs
+ - {{ data_dir }}/data/acme.json:/acme.json
+ # healthcheck:
+ # test: ["CMD", "traefik", "healthcheck", "--ping"]
+ # timeout: 1s
+ # interval: 10s
+ # retries: 3
+ # start_period: 10s
+ networks:
+ - {{ traefik_public_net }}
+ # Global mode makes an instance of traefik listen on _every_ node, so that regardless of which
+ # node the request arrives on, it'll be forwarded to the correct backend service.
+ deploy:
+ mode: global
+ labels:
+ - "traefik.enable=true"
+ - "traefik.swarm.network={{ traefik_public_net }}"
+ - "traefik.http.routers.api.rule=Host(`{{ subdomain }}.{{ main_domain }}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
+ - "traefik.http.routers.api.entrypoints=https"
+{% if use_authentik %} - "traefik.http.routers.api.middlewares=authentik@file"{% endif %}
+# - "traefik.http.routers.api.tls.domains[0].main={{ main_domain }}"
+# - "traefik.http.routers.api.tls.domains[0].sans=*.{{ main_domain }}"
+ - "traefik.http.routers.api.tls=true"
+ - "traefik.http.routers.api.tls.certresolver=main"
+ - "traefik.http.routers.api.service=api@internal"
+ - "traefik.http.services.dummy.loadbalancer.server.port=9999"
+ placement:
+ constraints: [node.role == manager]
+
+networks:
+ {{ traefik_public_net }}:
+ external: true
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/traefik/vars/main.yml b/infrastructure/docker-cluster/ansible/roles/traefik/vars/main.yml
new file mode 100644
index 0000000..b3b43ff
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/traefik/vars/main.yml
@@ -0,0 +1,3 @@
+subdomain: router
+use_authentik: false
+data_dir: "{{ ceph_volume }}/traefik"
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/ansible/roles/ufw_firewall/tasks/main.yml b/infrastructure/docker-cluster/ansible/roles/ufw_firewall/tasks/main.yml
new file mode 100644
index 0000000..e992ebb
--- /dev/null
+++ b/infrastructure/docker-cluster/ansible/roles/ufw_firewall/tasks/main.yml
@@ -0,0 +1,84 @@
+---
+- name: FIREWALL | UFW auf Standardeinstellungen zurücksetzen
+ community.general.ufw:
+ state: reset
+
+- name: FIREWALL | Standardmäßig allen ausgehenden Traffic erlauben
+ community.general.ufw:
+ direction: outgoing
+ policy: allow
+
+- name: FIREWALL | Standardmäßig allen eingehenden Traffic blockieren
+ community.general.ufw:
+ direction: incoming
+ policy: deny
+
+- name: FIREWALL | Eingehenden SSH-Traffic auf öffentlichem Interface erlauben
+ community.general.ufw:
+ rule: allow
+ port: "{{ ssh_port }}"
+ proto: tcp
+ interface: "{{ public_interface }}"
+ direction: in
+
+- name: FIREWALL | Eingehenden SSH-Traffic auf privatem Interface erlauben
+ community.general.ufw:
+ rule: allow
+ port: "{{ ssh_port }}"
+ proto: tcp
+ interface: "{{ private_interface }}"
+ direction: in
+
+- name: FIREWALL | Eingehenden HTTP/HTTPS-Traffic auf öffentlichem Interface erlauben
+ community.general.ufw:
+ rule: allow
+ port: "{{ item.port }}"
+ proto: "{{ item.proto }}"
+ interface: "{{ public_interface }}"
+ direction: in
+ with_items:
+ - { port: '80', proto: 'tcp' }
+ - { port: '443', proto: 'tcp' }
+
+- name: FIREWALL | Ceph Monitor Ports auf privatem Interface erlauben
+ community.general.ufw:
+ rule: allow
+ port: "{{ item }}"
+ proto: tcp
+ interface: "{{ private_interface }}"
+ direction: in
+ with_items:
+ - '3300'
+ - '6789'
+
+- name: FIREWALL | Ceph OSD/MGR Port-Range auf öffentlichem Interface erlauben
+ community.general.ufw:
+ rule: allow
+ port: "6800:7568"
+ proto: tcp
+ interface: "{{ private_interface }}"
+ direction: in
+
+- name: FIREWALL | Docker Swarm Management Ports auf privatem Interface erlauben
+ community.general.ufw:
+ rule: allow
+ port: "2377"
+ proto: tcp
+ interface: "{{ private_interface }}"
+ direction: in
+
+- name: FIREWALL | Docker Swarm Discovery/Network Ports auf privatem Interface erlauben
+ community.general.ufw:
+ rule: allow
+ port: "{{ item.port }}"
+ proto: "{{ item.proto }}"
+ interface: "{{ private_interface }}"
+ direction: in
+ with_items:
+ - { port: '7946', proto: 'tcp' }
+ - { port: '7946', proto: 'udp' }
+ - { port: '4789', proto: 'udp' }
+
+- name: FIREWALL | UFW aktivieren
+ community.general.ufw:
+ state: enabled
\ No newline at end of file
diff --git a/infrastructure/docker-cluster/pulumi/Pulumi.prod.yaml b/infrastructure/docker-cluster/pulumi/Pulumi.prod.yaml
new file mode 100644
index 0000000..47921a5
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/Pulumi.prod.yaml
@@ -0,0 +1,3 @@
+config:
+ hcloud:token:
+ secure: AAABAGkphVtyNKTaI7oPUA7mZ4PehhSPRdIxfnEVdOc2Q+D+Q3bLpRofkiula4FSk+0bh8ModR46zmoDznrlrhqvhsm/mIcWmDOqKq+pyzRGrYKXpqi7sw1LhUanNV6Z
diff --git a/infrastructure/docker-cluster/pulumi/Pulumi.yaml b/infrastructure/docker-cluster/pulumi/Pulumi.yaml
new file mode 100644
index 0000000..6ee725a
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/Pulumi.yaml
@@ -0,0 +1,7 @@
+name: avicenna-infra
+description: Infrastructure to host and run the Applications and Services developed for Avicenna
+runtime: go
+config:
+ pulumi:tags:
+ value:
+ pulumi:template: go
diff --git a/infrastructure/docker-cluster/pulumi/config/config.go b/infrastructure/docker-cluster/pulumi/config/config.go
new file mode 100644
index 0000000..48086b6
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/config/config.go
@@ -0,0 +1,8 @@
+package config
+
+type InfraConfig struct {
+ StackReference string
+ SwarmNetworkName string
+ SwarmIpRange string
+ SwarmSubnetIpRange string
+}
diff --git a/infrastructure/docker-cluster/pulumi/go.mod b/infrastructure/docker-cluster/pulumi/go.mod
new file mode 100644
index 0000000..97726ac
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/go.mod
@@ -0,0 +1,90 @@
+module avicenna-infra
+
+go 1.24.0
+
+require github.com/pulumi/pulumi/sdk/v3 v3.209.0
+
+require (
+ dario.cat/mergo v1.0.0 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/ProtonMail/go-crypto v1.1.3 // indirect
+ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
+ github.com/agext/levenshtein v1.2.3 // indirect
+ github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/blang/semver v3.5.1+incompatible // indirect
+ github.com/charmbracelet/bubbles v0.16.1 // indirect
+ github.com/charmbracelet/bubbletea v0.25.0 // indirect
+ github.com/charmbracelet/lipgloss v0.7.1 // indirect
+ github.com/cheggaaa/pb v1.0.29 // indirect
+ github.com/cloudflare/circl v1.6.1 // indirect
+ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
+ github.com/cyphar/filepath-securejoin v0.3.6 // indirect
+ github.com/djherbis/times v1.5.0 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.6.1 // indirect
+ github.com/go-git/go-git/v5 v5.13.1 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/glog v1.2.4 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/hcl/v2 v2.22.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/kevinburke/ssh_config v1.2.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/mitchellh/go-ps v1.0.0 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/opentracing/basictracer-go v1.1.0 // indirect
+ github.com/opentracing/opentracing-go v1.2.0 // indirect
+ github.com/pgavlin/fx v0.1.6 // indirect
+ github.com/pjbgf/sha1cd v0.3.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pkg/term v1.1.0 // indirect
+ github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect
+ github.com/pulumi/esc v0.17.0 // indirect
+ github.com/pulumi/pulumi-command/sdk v1.1.3 // indirect
+ github.com/pulumi/pulumi-hcloud/sdk v1.29.0 // indirect
+ github.com/pulumi/pulumi-tls/sdk/v5 v5.2.3 // indirect
+ github.com/rivo/uniseg v0.4.4 // indirect
+ github.com/rogpeppe/go-internal v1.13.1 // indirect
+ github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+ github.com/skeema/knownhosts v1.3.0 // indirect
+ github.com/spf13/cobra v1.8.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/texttheater/golang-levenshtein v1.0.1 // indirect
+ github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
+ github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ github.com/zclconf/go-cty v1.13.2 // indirect
+ go.uber.org/atomic v1.9.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
+ golang.org/x/mod v0.29.0 // indirect
+ golang.org/x/net v0.47.0 // indirect
+ golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/sys v0.38.0 // indirect
+ golang.org/x/term v0.37.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
+ golang.org/x/tools v0.38.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
+ google.golang.org/grpc v1.72.1 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ lukechampine.com/frand v1.4.2 // indirect
+)
diff --git a/infrastructure/docker-cluster/pulumi/go.sum b/infrastructure/docker-cluster/pulumi/go.sum
new file mode 100644
index 0000000..c84d8ce
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/go.sum
@@ -0,0 +1,309 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
+github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
+github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
+github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
+github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
+github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
+github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
+github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
+github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
+github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
+github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=
+github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
+github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
+github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
+github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
+github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU=
+github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0=
+github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
+github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
+github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
+github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
+github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
+github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
+github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
+github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
+github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0=
+github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU=
+github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M=
+github.com/pgavlin/fx/v2 v2.0.3 h1:ZBVklTFjxcWvBVPE+ti5qwnmTIQ0Gq6nuj3J5RKDtKk=
+github.com/pgavlin/fx/v2 v2.0.3/go.mod h1:Cvnwqq0BopdHUJ7CU50h1XPeKrF4ZwdFj1nJLXbAjCE=
+github.com/pgavlin/fx/v2 v2.0.10 h1:ggyQ6pB+lEQEbEae48Wh/X221eLOamMD7i01ISe88u4=
+github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk=
+github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0=
+github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE=
+github.com/pulumi/esc v0.17.0 h1:oaVOIyFTENlYDuqc3pW75lQT9jb2cd6ie/4/Twxn66w=
+github.com/pulumi/esc v0.17.0/go.mod h1:XnSxlt5NkmuAj304l/gK4pRErFbtqq6XpfX1tYT9Jbc=
+github.com/pulumi/pulumi-command/sdk v1.1.3 h1:2FdcqVenuHcGJfcVnUg6G22IeoQ/lY5UX6VexFJ4kT8=
+github.com/pulumi/pulumi-command/sdk v1.1.3/go.mod h1:3ochnip+NSR3+lQh8//Cni6hR9ckswuc1c6URsmX4RM=
+github.com/pulumi/pulumi-hcloud/sdk v1.29.0 h1:TQasXTpZlib8zCQXnsRSUpX0cB1/pRoPBIpPip0jwQc=
+github.com/pulumi/pulumi-hcloud/sdk v1.29.0/go.mod h1:JID9el4HeuNGU9BJiPHQsGddjvkzoVbUUZtmD1dbuTg=
+github.com/pulumi/pulumi-tls/sdk/v5 v5.2.3 h1:HEHeifaSKme+mWXYkps5qYD45JJc86Y/0awBj7UC6vU=
+github.com/pulumi/pulumi-tls/sdk/v5 v5.2.3/go.mod h1:UxIBAyqaLIo5bn63paPM7FU7DahbfeSIKbgTf9KldLY=
+github.com/pulumi/pulumi/sdk/v3 v3.209.0 h1:Ti0FohAset2HEgogTxTOWoyvQd2N2+pkTSIn5DW3W7s=
+github.com/pulumi/pulumi/sdk/v3 v3.209.0/go.mod h1:qBMg01woyYVNqNDJXpFL1e5gdN7oQQvCNNNtfq1gsEo=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
+github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
+github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
+github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
+github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
+github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
+github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0=
+github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
+golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
+google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw=
+lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s=
+pgregory.net/rapid v0.5.5 h1:jkgx1TjbQPD/feRoK+S/mXw9e1uj6WilpHrXJowi6oA=
+pgregory.net/rapid v0.5.5/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
diff --git a/infrastructure/docker-cluster/pulumi/main.go b/infrastructure/docker-cluster/pulumi/main.go
new file mode 100644
index 0000000..c4680ec
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/main.go
@@ -0,0 +1,121 @@
+package main
+
+import (
+ "avicenna-infra/config"
+ "avicenna-infra/utils"
+ "fmt"
+ "net"
+ "slices"
+ "strings"
+
+ "github.com/pulumi/pulumi-command/sdk/go/command/remote"
+ "github.com/pulumi/pulumi-hcloud/sdk/go/hcloud"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+)
+
+type Infrastructure struct {
+ placementGroup *hcloud.PlacementGroup
+ networkID *pulumi.IDOutput
+ managerNodes []*hcloud.Server
+ workerNodes []*hcloud.Server
+}
+
+func main() {
+ infra := &Infrastructure{}
+
+ pulumi.Run(func(ctx *pulumi.Context) error {
+ var err error
+
+ cfg := config.InfraConfig{
+ StackReference: strings.Join(
+ []string{ctx.Organization(), ctx.Project(), ctx.Stack()},
+ "/"),
+ SwarmNetworkName: "gc-swarmnet",
+ SwarmIpRange: "10.0.0.0/16",
+ SwarmSubnetIpRange: "10.0.1.0/24",
+ }
+ pk, err := utils.CreateSshKey(ctx)
+ if err != nil {
+ panic(err.Error())
+ }
+ ctx.Export("privKey", pk.PrivateKeyOpenssh)
+
+ hkey, err := hcloud.NewSshKey(ctx, strings.Join([]string{cfg.StackReference, "key"}, "-"), &hcloud.SshKeyArgs{PublicKey: pk.PublicKeyOpenssh})
+ if err != nil {
+ panic(err.Error())
+ }
+
+ infra.placementGroup, err = utils.CreatePlacementGroup(ctx, cfg.StackReference)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ infra.networkID, err = utils.CreateClusterNet(ctx, cfg)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ infra.managerNodes, err = utils.CreateServer(ctx, utils.CreateServerArgs{
+ PlacementGroupId: infra.placementGroup.ID(),
+ NetworkId: infra.networkID,
+ NetworkFirstIP: string(utils.IncrementIP(net.ParseIP("10.0.1.0"))),
+ Basename: "manager-node",
+ Count: 3,
+ SshKey: hkey,
+ ServerType: "ccx23",
+ })
+ if err != nil {
+ panic(err.Error())
+ }
+ // infra.workerNodes, err = utils.CreateServer(ctx, utils.CreateServerArgs{
+ // PlacementGroupId: infra.placementGroup.ID(),
+ // NetworkId: infra.networkID,
+ // NetworkFirstIP: string(utils.IncrementIP(net.ParseIP("10.0.1.20"))),
+ // Basename: "worker-node",
+ // Count: 2,
+ // SshKey: hkey,
+ // })
+ // if err != nil {
+ // panic(err.Error())
+ // }
+
+ for idx, s := range slices.Concat(infra.managerNodes, infra.workerNodes) {
+ err := utils.InstallAnsibleDependencies(ctx, remote.ConnectionArgs{
+ Host: s.Ipv4Address,
+ User: pulumi.String("root"),
+ PrivateKey: pk.PrivateKeyOpenssh,
+ }, fmt.Sprintf("%d", idx))
+ if err != nil {
+ panic(err.Error())
+ }
+ }
+
+ // var advAddr = infra.managerNodes[0].Networks.ApplyT(func(net []hcloud.ServerNetworkType) string {
+ // return *net[0].Ip
+ // }).(pulumi.StringOutput)
+
+ // tokens, err := utils.InitDockerSwarm(ctx, remote.ConnectionArgs{
+ // Host: infra.managerNodes[0].Ipv4Address,
+ // User: pulumi.String("root"),
+ // PrivateKey: pk.PrivateKeyOpenssh}, advAddr)
+ // if err != nil {
+ // panic(err.Error())
+ // }
+
+ // ctx.Export("SwarmTokens", tokens)
+
+ // inventory, err := utils.CreateAnsibleInventory(infra.managerNodes, infra.workerNodes)
+ // if err != nil {
+ // panic(err.Error())
+ // }
+ // ctx.Export("inventory", inventory)
+
+ sm := map[string]pulumi.Input{}
+ for idx, s := range slices.Concat(infra.managerNodes, infra.workerNodes) {
+ sm[fmt.Sprintf("node-%d-ip", idx)] = s.Ipv4Address
+ }
+ ctx.Export("server-ips", pulumi.Map(sm))
+
+ return nil
+ })
+}
diff --git a/infrastructure/docker-cluster/pulumi/utils/ansible-configuration.go b/infrastructure/docker-cluster/pulumi/utils/ansible-configuration.go
new file mode 100644
index 0000000..ceb1dd8
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/utils/ansible-configuration.go
@@ -0,0 +1,154 @@
+package utils
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+ "strings"
+ "text/template"
+
+ "github.com/pulumi/pulumi-command/sdk/go/command/remote"
+ "github.com/pulumi/pulumi-hcloud/sdk/go/hcloud"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+)
+
+type SwarmJoinTokens struct {
+ ManagerToken string
+ WorkerToken string
+}
+
+type ServerInfo struct {
+ Name pulumi.StringOutput
+ IP pulumi.StringOutput
+}
+
+func InstallAnsibleDependencies(ctx *pulumi.Context, connArgs remote.ConnectionArgs, uniqueness string) error {
+ _, err := remote.NewCommand(ctx, strings.Join([]string{uniqueness, "Install Ansible Dependencies"}, ": "),
+ &remote.CommandArgs{
+ Connection: connArgs,
+ Create: pulumi.String("apt-get update && apt-get install -y python3-pip python3-jsondiff"),
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func InitDockerSwarm(ctx *pulumi.Context, connArgs remote.ConnectionArgs, advertiseAddr pulumi.StringOutput) (pulumi.Output, error) {
+ var tokens SwarmJoinTokens
+
+ fullCommand := advertiseAddr.ApplyT(func(addr string) *string {
+ initCommand := fmt.Sprintf("docker swarm init --advertise-addr %s", addr)
+ fullCommand := strings.Join([]string{initCommand, "echo \"Manager Token: $(docker swarm join-token -q manager)\"", "echo \"Worker Token: $(docker swarm join-token -q worker)\""}, " && ")
+ return &fullCommand
+ }).(pulumi.StringPtrOutput)
+
+ out, err := remote.NewCommand(ctx, "Init docker swarm",
+ &remote.CommandArgs{
+ Connection: connArgs,
+ Create: fullCommand,
+ })
+ if err != nil {
+ return pulumi.StringOutput{}, err
+ }
+
+ return out.Stdout.ApplyT(func(output string) SwarmJoinTokens {
+ searchWorker := "Worker Token: "
+ patternWorker := regexp.MustCompile(searchWorker + `(\S+)`)
+ searchManager := "Manager Token: "
+ patternManager := regexp.MustCompile(searchManager + `(\S+)`)
+
+ matches := patternWorker.FindStringSubmatch(output)
+ if len(matches) > 1 {
+ extracted := matches[1]
+ tokens.WorkerToken = extracted
+ }
+ matches = patternManager.FindStringSubmatch(output)
+ if len(matches) > 1 {
+ extracted := matches[1]
+ tokens.ManagerToken = extracted
+ }
+ return tokens
+ }), nil
+}
+
+func CreateAnsibleInventory(managerNodes, workerNodes []*hcloud.Server) (pulumi.Output, error) {
+ serverInfos := toServerInfo(managerNodes)
+ return pulumi.All(pulumi.ToOutput(serverInfos)).ApplyT(func(results []interface{}) (string, error) {
+ var serverInfos = results[0].([]ServerInfo)
+ // var workerSlice = results[1].([]*hcloud.Server)
+
+ serverData := make(map[string][]ServerInfo)
+
+ for _, s := range serverInfos {
+ serverData["Manager"] = append(serverData["Manager"], ServerInfo{
+ Name: s.Name,
+ IP: s.IP,
+ })
+ }
+ // for _, result := range workerSlice {
+ // server := result.(map[string]interface{})
+ // serverData["Worker"] = append(serverData["Worker"], ServerInfo{
+ // Name: server["name"].(string),
+ // IP: server["ipv4_address"].(string),
+ // })
+ // }
+ fmt.Println(serverData["Manager"])
+ fmt.Println(results[0])
+ return generateInventoryFile(serverData)
+ }).(pulumi.Output), nil
+}
+
+func toServerInfo(server []*hcloud.Server) pulumi.ArrayOutput {
+ serverInfo := []ServerInfo{}
+ for _, s := range server {
+ serverInfo = append(serverInfo, ServerInfo{
+ Name: s.Name,
+ IP: s.Ipv4Address,
+ })
+ }
+ return pulumi.All(serverInfo).ApplyT(func(args []interface{}) []interface{} {
+ var serverInfo []interface{}
+
+ for _, s := range args {
+ val := s.(map[string]interface{})
+ serverInfo = append(serverInfo, map[string]interface{}{
+ "Name": val["Name"].(string),
+ "IP": val["IP"].(string),
+ })
+ }
+ return serverInfo
+ }).(pulumi.ArrayOutput)
+}
+
+func generateInventoryFile(inventory map[string][]ServerInfo) (string, error) {
+ const inventoryTmpl = `
+[all]
+{{ range .Manager }}
+{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
+{{ end }}
+{{ range .Worker }}
+{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
+{{ end }}
+
+[manager]
+{{ range .Manager }}
+{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
+{{ end }}
+
+[worker]
+{{ range .Worker }}
+{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
+{{ end }}
+ `
+ tmpl, err := template.New("inventory").Parse(inventoryTmpl)
+ if err != nil {
+ return "", err
+ }
+ var buf bytes.Buffer
+ err = tmpl.Execute(&buf, inventory)
+ if err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+}
diff --git a/infrastructure/docker-cluster/pulumi/utils/resource-creation.go b/infrastructure/docker-cluster/pulumi/utils/resource-creation.go
new file mode 100644
index 0000000..b1f26cd
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/utils/resource-creation.go
@@ -0,0 +1,110 @@
+package utils
+
+import (
+ "avicenna-infra/config"
+ "fmt"
+ "net"
+
+ "github.com/pulumi/pulumi-hcloud/sdk/go/hcloud"
+ "github.com/pulumi/pulumi-tls/sdk/v5/go/tls"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+)
+
+func CreateSshKey(ctx *pulumi.Context) (*tls.PrivateKey, error) {
+ return tls.NewPrivateKey(ctx, "sshKey", &tls.PrivateKeyArgs{
+ Algorithm: pulumi.String("ED25519"),
+ }, pulumi.AdditionalSecretOutputs([]string{"privKey"}))
+}
+
+func CreatePlacementGroup(ctx *pulumi.Context, name string) (*hcloud.PlacementGroup, error) {
+ pg, err := hcloud.NewPlacementGroup(ctx, name, &hcloud.PlacementGroupArgs{
+ Name: pulumi.String(name),
+ Type: pulumi.String("spread"),
+ })
+ return pg, err
+}
+
+func CreateClusterNet(ctx *pulumi.Context, cfg config.InfraConfig) (*pulumi.IDOutput, error) {
+ var id pulumi.IDOutput
+ network, err := hcloud.NewNetwork(ctx, cfg.SwarmNetworkName, &hcloud.NetworkArgs{
+ Name: pulumi.String(cfg.SwarmNetworkName),
+ IpRange: pulumi.String(cfg.SwarmIpRange),
+ })
+ if err != nil {
+ return nil, err
+ }
+ id = network.ID()
+ _, err = hcloud.NewNetworkSubnet(ctx, "network-subnet", &hcloud.NetworkSubnetArgs{
+ Type: pulumi.String("cloud"),
+ NetworkId: IDtoIntOutput(id),
+ NetworkZone: pulumi.String("eu-central"),
+ IpRange: pulumi.String(cfg.SwarmSubnetIpRange),
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &id, nil
+}
+
+type CreateServerArgs struct {
+ NetworkId pulumi.IDInput
+ PlacementGroupId pulumi.IDInput
+ // The first assignable IP of the network
+ NetworkFirstIP string
+ Basename string
+ Count int
+ SshKey *hcloud.SshKey
+ ServerType string
+}
+
+func CreateServer(ctx *pulumi.Context, cfg CreateServerArgs) ([]*hcloud.Server, error) {
+ var nodes []*hcloud.Server
+ nextIp := cfg.NetworkFirstIP
+ for i := range cfg.Count {
+ sn := fmt.Sprintf("%s-%d", cfg.Basename, i+1)
+ s, err := hcloud.NewServer(ctx, sn, &hcloud.ServerArgs{
+ Name: pulumi.String(sn),
+ Image: pulumi.String("docker-ce"),
+ ServerType: pulumi.String(cfg.ServerType),
+ Location: pulumi.StringPtr("fsn1"),
+ Networks: hcloud.ServerNetworkTypeArray{
+ &hcloud.ServerNetworkTypeArgs{
+ NetworkId: IDtoIntOutput(cfg.NetworkId),
+ Ip: pulumi.String(nextIp),
+ },
+ },
+ PlacementGroupId: IDtoIntPtrOutput(cfg.PlacementGroupId),
+ PublicNets: hcloud.ServerPublicNetArray{
+ &hcloud.ServerPublicNetArgs{
+ // Ipv4Enabled: pulumi.Bool(true),
+ Ipv6Enabled: pulumi.Bool(true),
+ },
+ },
+ SshKeys: pulumi.StringArray{cfg.SshKey.ID()},
+ })
+ if err != nil {
+ return nodes, err
+ }
+
+ cephVolume, err := hcloud.NewVolume(ctx, fmt.Sprintf("ceph-%s", sn), &hcloud.VolumeArgs{
+ Name: pulumi.Sprintf("%s-ceph-vol-0%d", s.Name, i+1),
+ Size: pulumi.Int(100),
+ Location: s.Location,
+ })
+ if err != nil {
+ return nodes, fmt.Errorf("couldn't create volume: %w", err)
+ }
+
+ _, err = hcloud.NewVolumeAttachment(ctx, fmt.Sprintf("ceph-vol-attach-%s", sn), &hcloud.VolumeAttachmentArgs{
+ VolumeId: IDtoIntOutput(cephVolume.ID()),
+ ServerId: IDtoIntOutput(s.ID()),
+ })
+ if err != nil {
+ return nodes, fmt.Errorf("couldn't attach volume to node %d", i)
+ }
+
+ nodes = append(nodes, s)
+ nextIp = IncrementIP(net.ParseIP(nextIp)).String()
+ }
+ return nodes, nil
+}
diff --git a/infrastructure/docker-cluster/pulumi/utils/utils.go b/infrastructure/docker-cluster/pulumi/utils/utils.go
new file mode 100644
index 0000000..1b2f2d1
--- /dev/null
+++ b/infrastructure/docker-cluster/pulumi/utils/utils.go
@@ -0,0 +1,41 @@
+package utils
+
+import (
+ "net"
+ "strconv"
+
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+)
+
+func IncrementIP(ip net.IP) net.IP {
+ if ip == nil {
+ return ip
+ }
+ ip = ip.To4()
+ ip[3]++
+ return ip
+}
+
+func IDtoInt(val string) (int, error) {
+ i, err := strconv.Atoi(val)
+ return i, err
+}
+
+func IDtoIntPtr(val string) (*int, error) {
+ i, err := strconv.Atoi(val)
+ return &i, err
+}
+
+// Convert pulumi.IDOutput to pulumi.InOutput
+//
+// Some hcloud ID's are int based, but pulumi.IDOutput doesn't have a ToIntOutput method
+func IDtoIntOutput(val pulumi.IDInput) pulumi.IntOutput {
+ return val.ToIDOutput().ToStringOutput().ApplyT(IDtoInt).(pulumi.IntOutput)
+}
+
+// Convert pulumi.IDOutput to pulumi.InOutput
+//
+// Some hcloud ID's are int based, but pulumi.IDOutput doesn't have a ToIntOutput method
+func IDtoIntPtrOutput(val pulumi.IDInput) pulumi.IntPtrOutput {
+ return val.ToIDOutput().ToStringOutput().ApplyT(IDtoIntPtr).(pulumi.IntPtrOutput)
+}
diff --git a/infrastructure/single-docker-server/.gitignore b/infrastructure/single-docker-server/.gitignore
new file mode 100644
index 0000000..5c99f2f
--- /dev/null
+++ b/infrastructure/single-docker-server/.gitignore
@@ -0,0 +1,3 @@
+/bin/
+node_modules/
+.local/
\ No newline at end of file
diff --git a/infrastructure/.gitkeep b/infrastructure/single-docker-server/.gitkeep
similarity index 100%
rename from infrastructure/.gitkeep
rename to infrastructure/single-docker-server/.gitkeep
diff --git a/infrastructure/Pulumi.dev.yaml b/infrastructure/single-docker-server/Pulumi.dev.yaml
similarity index 100%
rename from infrastructure/Pulumi.dev.yaml
rename to infrastructure/single-docker-server/Pulumi.dev.yaml
diff --git a/infrastructure/Pulumi.prod.yaml b/infrastructure/single-docker-server/Pulumi.prod.yaml
similarity index 100%
rename from infrastructure/Pulumi.prod.yaml
rename to infrastructure/single-docker-server/Pulumi.prod.yaml
diff --git a/infrastructure/Pulumi.yaml b/infrastructure/single-docker-server/Pulumi.yaml
similarity index 100%
rename from infrastructure/Pulumi.yaml
rename to infrastructure/single-docker-server/Pulumi.yaml
diff --git a/infrastructure/single-docker-server/README.md b/infrastructure/single-docker-server/README.md
new file mode 100644
index 0000000..c2e22b0
--- /dev/null
+++ b/infrastructure/single-docker-server/README.md
@@ -0,0 +1,8 @@
+# Infrastructure Project
+
+Dieses Projekt erstellt einen Hetzner Server mit Docker vorinstalliert.
+
+## Verwendung
+
+Hier liegen die SSH Schlüssel für die ursprüngliche Avicenna Produktionsumgebung.
+Desweiteren liegt hier ein Schlüssel von Factorypuls, vermutlich der Schlüssel für den Hetzner Server, welcher das Visual hostet.
\ No newline at end of file
diff --git a/infrastructure/ansible/playbook.yml b/infrastructure/single-docker-server/ansible/playbook.yml
similarity index 100%
rename from infrastructure/ansible/playbook.yml
rename to infrastructure/single-docker-server/ansible/playbook.yml
diff --git a/infrastructure/ansible/resources/kestra/kestra.yml b/infrastructure/single-docker-server/ansible/resources/kestra/kestra.yml
similarity index 100%
rename from infrastructure/ansible/resources/kestra/kestra.yml
rename to infrastructure/single-docker-server/ansible/resources/kestra/kestra.yml
diff --git a/infrastructure/ansible/resources/portainer/portainer.yml b/infrastructure/single-docker-server/ansible/resources/portainer/portainer.yml
similarity index 100%
rename from infrastructure/ansible/resources/portainer/portainer.yml
rename to infrastructure/single-docker-server/ansible/resources/portainer/portainer.yml
diff --git a/infrastructure/ansible/resources/traefik/config/traefik.toml b/infrastructure/single-docker-server/ansible/resources/traefik/config/traefik.toml
similarity index 100%
rename from infrastructure/ansible/resources/traefik/config/traefik.toml
rename to infrastructure/single-docker-server/ansible/resources/traefik/config/traefik.toml
diff --git a/infrastructure/ansible/resources/traefik/data/access.log b/infrastructure/single-docker-server/ansible/resources/traefik/data/access.log
similarity index 100%
rename from infrastructure/ansible/resources/traefik/data/access.log
rename to infrastructure/single-docker-server/ansible/resources/traefik/data/access.log
diff --git a/infrastructure/ansible/resources/traefik/data/traefik.log b/infrastructure/single-docker-server/ansible/resources/traefik/data/acme.json
similarity index 100%
rename from infrastructure/ansible/resources/traefik/data/traefik.log
rename to infrastructure/single-docker-server/ansible/resources/traefik/data/acme.json
diff --git a/infrastructure/single-docker-server/ansible/resources/traefik/data/traefik.log b/infrastructure/single-docker-server/ansible/resources/traefik/data/traefik.log
new file mode 100644
index 0000000..e69de29
diff --git a/infrastructure/ansible/resources/traefik/network.yml b/infrastructure/single-docker-server/ansible/resources/traefik/network.yml
similarity index 100%
rename from infrastructure/ansible/resources/traefik/network.yml
rename to infrastructure/single-docker-server/ansible/resources/traefik/network.yml
diff --git a/infrastructure/ansible/resources/traefik/traefik.env b/infrastructure/single-docker-server/ansible/resources/traefik/traefik.env
similarity index 100%
rename from infrastructure/ansible/resources/traefik/traefik.env
rename to infrastructure/single-docker-server/ansible/resources/traefik/traefik.env
diff --git a/infrastructure/ansible/resources/traefik/traefik.yml b/infrastructure/single-docker-server/ansible/resources/traefik/traefik.yml
similarity index 100%
rename from infrastructure/ansible/resources/traefik/traefik.yml
rename to infrastructure/single-docker-server/ansible/resources/traefik/traefik.yml
diff --git a/infrastructure/ansible/roles/kestra/tasks/main.yml b/infrastructure/single-docker-server/ansible/roles/kestra/tasks/main.yml
similarity index 100%
rename from infrastructure/ansible/roles/kestra/tasks/main.yml
rename to infrastructure/single-docker-server/ansible/roles/kestra/tasks/main.yml
diff --git a/infrastructure/ansible/roles/portainer/tasks/main.yml b/infrastructure/single-docker-server/ansible/roles/portainer/tasks/main.yml
similarity index 100%
rename from infrastructure/ansible/roles/portainer/tasks/main.yml
rename to infrastructure/single-docker-server/ansible/roles/portainer/tasks/main.yml
diff --git a/infrastructure/ansible/roles/traefik/tasks/main.yml b/infrastructure/single-docker-server/ansible/roles/traefik/tasks/main.yml
similarity index 100%
rename from infrastructure/ansible/roles/traefik/tasks/main.yml
rename to infrastructure/single-docker-server/ansible/roles/traefik/tasks/main.yml
diff --git a/infrastructure/single-docker-server/dashboard.tar.gz b/infrastructure/single-docker-server/dashboard.tar.gz
new file mode 100644
index 0000000..7fd1640
Binary files /dev/null and b/infrastructure/single-docker-server/dashboard.tar.gz differ
diff --git a/infrastructure/index.ts b/infrastructure/single-docker-server/index.ts
similarity index 100%
rename from infrastructure/index.ts
rename to infrastructure/single-docker-server/index.ts
diff --git a/infrastructure/package-lock.json b/infrastructure/single-docker-server/package-lock.json
similarity index 100%
rename from infrastructure/package-lock.json
rename to infrastructure/single-docker-server/package-lock.json
diff --git a/infrastructure/package.json b/infrastructure/single-docker-server/package.json
similarity index 100%
rename from infrastructure/package.json
rename to infrastructure/single-docker-server/package.json
diff --git a/infrastructure/tsconfig.json b/infrastructure/single-docker-server/tsconfig.json
similarity index 100%
rename from infrastructure/tsconfig.json
rename to infrastructure/single-docker-server/tsconfig.json