Compare commits

..

10 Commits

Author SHA1 Message Date
Marcel Arndt d448ca6516 WIP 2026-01-19 12:21:48 +01:00
Marcel Arndt 62e663d053 add log aggregation with HyperDX 2025-12-12 12:00:22 +01:00
Marcel Arndt 0951282f11 implement latest changes to employee payroll process
- Add ContractDaysWeek
- Add Sickness Days to employee export sheet
- Add Gesundheitsbonus value per employee
- Additional fixes
2025-12-12 11:55:31 +01:00
Marcel Arndt 156733f65a migrate infra from single server to multi-server cluster 2025-12-12 11:41:32 +01:00
Marcel Arndt b1a70673a4 modernize ticket and accounting
iteration 0
2025-12-12 11:38:30 +01:00
Marcel Arndt c33ab89354 commit open changes from previous iterations 2025-11-18 14:11:27 +01:00
Marcel Arndt 4cf79f3094 implement Gehaltsaufstellung dyflexis data access 2025-11-18 13:32:36 +01:00
Marcel Arndt 4abe9e44d8 implement payroll generator v0.9 2025-11-06 11:38:59 +01:00
Marcel Arndt 319e7f3614 feat: implement incremental tour update 2025-08-20 10:45:01 +02:00
Marcel Arndt aa29d58243 feat: implement new tour calculation rules 2025-08-20 10:43:29 +02:00
496 changed files with 61782 additions and 34975 deletions

4
.gitignore vendored
View File

@ -2,4 +2,8 @@
.vscode/
.env
.env.prod
.env.stage
.DS_Store
node_modules/

82
GEMINI.md Normal file
View File

@ -0,0 +1,82 @@
# Avicenna Project Documentation
This document provides a comprehensive overview of the Avicenna project, its components, and instructions for development and deployment.
## Project Overview
The Avicenna project is a multi-component system composed of a web application, a data connector, a data transformation service, and the necessary infrastructure to run them. The project is divided into the following main parts:
- **service**: Contains the core application services, including the frontend dashboard and the backend data connector.
- **plattform**: Includes the data transformation and integration services.
- **infrastructure**: Defines the infrastructure as code for deploying the project.
## Services
### App Hub Dashboard (Angular Frontend)
The App Hub Dashboard is the user-facing web application for the Avicenna project, developed with Angular.
- **Location**: `service/app-hub/dashboard`
- **Key Files**:
- `angular.json`: Angular project configuration.
- `package.json`: Project dependencies and scripts.
- `tailwind.config.js`: Tailwind CSS configuration.
#### Commands
- **Run development server**: `ng serve`
- **Build for production**: `ng build`
- **Run unit tests**: `ng test`
### Data Hub Connector (NestJS Backend)
The Data Hub Connector is a NestJS-based backend service responsible for managing data, with database interactions handled by Prisma.
- **Location**: `service/data-hub/data-connector`
- **Key Files**:
- `nest-cli.json`: NestJS project configuration.
- `package.json`: Project dependencies and scripts.
- `prisma/schema.prisma`: Database schema definition.
#### Commands
- **Install dependencies**: `npm install`
- **Run in development mode**: `npm run start:dev`
- **Run in production mode**: `npm run start:prod`
- **Run tests**: `npm run test`
### Synapse (Go Plattforms)
Synapse is a data transformation service written in Go, responsible for processing and integrating data from various sources.
- **Location**: `plattform/synapse`
- **Key Files**:
- `go.mod`: Go module definition.
- `cmd/synapse/main.go`: Main application entry point.
#### Commands
- **Run the application**: `go run ./cmd/synapse`
- **Build the application**: `go build -o synapse ./cmd/synapse`
## Infrastructure
The infrastructure for the Avicenna project is managed using Pulumi and Ansible, ensuring a reproducible and automated deployment process.
- **Location**: `infrastructure`
- **Key Components**:
- **Pulumi**: Used for provisioning cloud resources.
- **Ansible**: Used for configuring services like Traefik, Portainer, and Kestra.
## Workflows
The project uses Kestra for orchestrating complex workflows, such as ETL processes and ticket management.
- **Location**: `service/workflows`
- **Key Files**:
- `avic-etl.yml`: Defines the ETL workflow.
- `avic-tickets.yml`: Defines the ticket management workflow.
## Development
To work on the Avicenna project, follow the instructions in the respective service directories. Ensure you have the required runtimes and dependencies installed, such as Node.js, Go, and the Angular CLI.

2
TODO.md Normal file
View File

@ -0,0 +1,2 @@
- [ ] Anomalie "Anwesenheit vor Arbeitszeit" abschliessen
- [ ] Add Dyflexis User Access Endpoints to ETL

View File

@ -1,3 +0,0 @@
/bin/
/node_modules/
.local/

View File

@ -0,0 +1,34 @@
---
- 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

View File

@ -0,0 +1,13 @@
- name: Infrastruktur Dienste bereitstellen
hosts: all
gather_facts: true
roles:
# - traefik
# - portainer
# - hyperdx
- kestra
# - authentik
# # - dockge
# - signoz
# - signoz-infra
# - leantime

View File

@ -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";

View File

@ -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";

View File

@ -0,0 +1,6 @@
[client.admin]
key = AQDfUDBpnhHKNBAAwIRER17JF8flD9GSeNtFlg==
caps mds = "allow *"
caps mgr = "allow *"
caps mon = "allow *"
caps osd = "allow *"

View File

@ -0,0 +1,4 @@
# minimal ceph.conf for f103f9ee-d058-11f0-aee8-920006d45637
[global]
fsid = f103f9ee-d058-11f0-aee8-920006d45637
mon_host = [v2:10.0.1.2:3300/0,v1:10.0.1.2:6789/0]

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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]

View File

@ -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

View File

@ -0,0 +1,8 @@
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = {{ ssh_port }}

View File

@ -0,0 +1,175 @@
<?xml version="1.0"?>
<clickhouse>
<logger>
<level>debug</level>
<console>true</console>
<log remove="remove" />
<errorlog remove="remove" />
</logger>
<listen_host>0.0.0.0</listen_host>
<http_port>8123</http_port>
<tcp_port>9000</tcp_port>
<interserver_http_host>ch-server</interserver_http_host>
<interserver_http_port>9009</interserver_http_port>
<max_connections>4096</max_connections>
<keep_alive_timeout>64</keep_alive_timeout>
<max_concurrent_queries>100</max_concurrent_queries>
<uncompressed_cache_size>8589934592</uncompressed_cache_size>
<mark_cache_size>5368709120</mark_cache_size>
<path>/var/lib/clickhouse/</path>
<tmp_path>/var/lib/clickhouse/tmp/</tmp_path>
<user_files_path>/var/lib/clickhouse/user_files/</user_files_path>
<user_directories>
<users_xml>
<path>users.xml</path>
</users_xml>
</user_directories>
<!-- <users_config>users.xml</users_config> -->
<default_profile>default</default_profile>
<default_database>default</default_database>
<timezone>UTC</timezone>
<mlock_executable>false</mlock_executable>
<!-- Prometheus exporter -->
<prometheus>
<endpoint>/metrics</endpoint>
<port>9363</port>
<metrics>true</metrics>
<events>true</events>
<asynchronous_metrics>true</asynchronous_metrics>
<errors>true</errors>
</prometheus>
<!-- Query log. Used only for queries with setting log_queries = 1. -->
<query_log>
<database>system</database>
<table>query_log</table>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</query_log>
<!-- Metric log contains rows with current values of ProfileEvents, CurrentMetrics collected
with "collect_interval_milliseconds" interval. -->
<metric_log>
<database>system</database>
<table>metric_log</table>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
<collect_interval_milliseconds>1000</collect_interval_milliseconds>
</metric_log>
<!--
Asynchronous metric log contains values of metrics from
system.asynchronous_metrics.
-->
<asynchronous_metric_log>
<database>system</database>
<table>asynchronous_metric_log</table>
<!--
Asynchronous metrics are updated once a minute, so there is
no need to flush more often.
-->
<flush_interval_milliseconds>7000</flush_interval_milliseconds>
</asynchronous_metric_log>
<!--
OpenTelemetry log contains OpenTelemetry trace spans.
-->
<opentelemetry_span_log>
<!--
The default table creation code is insufficient, this <engine> spec
is a workaround. There is no 'event_time' for this log, but two times,
start and finish. It is sorted by finish time, to avoid inserting
data too far away in the past (probably we can sometimes insert a span
that is seconds earlier than the last span in the table, due to a race
between several spans inserted in parallel). This gives the spans a
global order that we can use to e.g. retry insertion into some external
system.
-->
<engine>
engine MergeTree
partition by toYYYYMM(finish_date)
order by (finish_date, finish_time_us, trace_id)
</engine>
<database>system</database>
<table>opentelemetry_span_log</table>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</opentelemetry_span_log>
<!-- Crash log. Stores stack traces for fatal errors.
This table is normally empty. -->
<crash_log>
<database>system</database>
<table>crash_log</table>
<partition_by />
<flush_interval_milliseconds>1000</flush_interval_milliseconds>
</crash_log>
<!-- Profiling on Processors level. -->
<processors_profile_log>
<database>system</database>
<table>processors_profile_log</table>
<partition_by>toYYYYMM(event_date)</partition_by>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</processors_profile_log>
<!-- Uncomment if use part log.
Part log contains information about all actions with parts in MergeTree tables (creation, deletion,
merges, downloads).-->
<part_log>
<database>system</database>
<table>part_log</table>
<partition_by>toYYYYMM(event_date)</partition_by>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</part_log>
<!-- Trace log. Stores stack traces collected by query profilers.
See query_profiler_real_time_period_ns and query_profiler_cpu_time_period_ns settings. -->
<trace_log>
<database>system</database>
<table>trace_log</table>
<partition_by>toYYYYMM(event_date)</partition_by>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</trace_log>
<!-- Query thread log. Has information about all threads participated in query execution.
Used only for queries with setting log_query_threads = 1. -->
<query_thread_log>
<database>system</database>
<table>query_thread_log</table>
<partition_by>toYYYYMM(event_date)</partition_by>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</query_thread_log>
<!-- Query views log. Has information about all dependent views associated with a query.
Used only for queries with setting log_query_views = 1. -->
<query_views_log>
<database>system</database>
<table>query_views_log</table>
<partition_by>toYYYYMM(event_date)</partition_by>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</query_views_log>
<remote_servers>
<hdx_cluster>
<shard>
<replica>
<host>ch-server</host>
<port>9000</port>
</replica>
</shard>
</hdx_cluster>
</remote_servers>
<distributed_ddl>
<path>/clickhouse/task_queue/ddl</path>
</distributed_ddl>
<format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>
</clickhouse>

View File

@ -0,0 +1,51 @@
<?xml version="1.0"?>
<clickhouse>
<profiles>
<default>
<max_memory_usage>10000000000</max_memory_usage>
<use_uncompressed_cache>0</use_uncompressed_cache>
<load_balancing>in_order</load_balancing>
<log_queries>1</log_queries>
</default>
</profiles>
<users>
<default>
<password></password>
<profile>default</profile>
<networks>
<ip>::/0</ip>
</networks>
<quota>default</quota>
</default>
<api>
<password>api</password>
<profile>default</profile>
<networks>
<ip>::/0</ip>
</networks>
<quota>default</quota>
</api>
<worker>
<password>worker</password>
<profile>default</profile>
<networks>
<ip>::/0</ip>
</networks>
<quota>default</quota>
</worker>
</users>
<quotas>
<default>
<interval>
<duration>3600</duration>
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</clickhouse>

View File

@ -0,0 +1,58 @@
---
- 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 | shared-observability Netzwerk erstellen
community.docker.docker_network:
name: shared-observability
driver: overlay
state: present
attachable: yes
ipam_config:
- subnet: '172.16.201.0/24'
gateway: '172.16.201.1'
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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1,2 @@
subdomain: workflow
data_dir: "{{ ceph_volume }}/kestra"

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
subdomain: port
data_dir: "{{ ceph_volume }}/portainer"

View File

@ -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

View File

@ -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"
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
subdomain: router
use_authentik: false
data_dir: "{{ ceph_volume }}/traefik"

View File

@ -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

View File

@ -0,0 +1,3 @@
config:
hcloud:token:
secure: AAABAGkphVtyNKTaI7oPUA7mZ4PehhSPRdIxfnEVdOc2Q+D+Q3bLpRofkiula4FSk+0bh8ModR46zmoDznrlrhqvhsm/mIcWmDOqKq+pyzRGrYKXpqi7sw1LhUanNV6Z

View File

@ -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

View File

@ -0,0 +1,8 @@
package config
type InfraConfig struct {
StackReference string
SwarmNetworkName string
SwarmIpRange string
SwarmSubnetIpRange string
}

View File

@ -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
)

View File

@ -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=

View File

@ -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
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -0,0 +1,3 @@
/bin/
node_modules/
.local/

View File

@ -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.

Binary file not shown.

5
service/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules
dist
.git
.local
.vscode

View File

@ -0,0 +1,5 @@
.angular
.vscode
dist
node_modules

View File

@ -0,0 +1,10 @@
{
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
}

View File

@ -1,18 +1,37 @@
FROM node:20.15.1-alpine AS builder
FROM node:20.19.1-slim AS builder
WORKDIR /app/builder
COPY . .
COPY package*.json .
COPY packages/shared-dtos/package*.json ./packages/shared-dtos/
# COPY data-hub/data-connector/package*.json ./data-hub/data-connector/
COPY app-hub/dashboard/package*.json ./app-hub/dashboard/
RUN npm install \
&& npm run build
ENV LIBC=glibc
ENV npm_config_arch=x64
ENV npm_config_platform=linux
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++ \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN npm ci --verbose
COPY packages/shared-dtos ./packages/shared-dtos/
COPY app-hub/dashboard ./app-hub/dashboard/
WORKDIR /app/builder/app-hub/dashboard
RUN npm run build
FROM nginx:1.19.2
WORKDIR /usr/share/nginx/html
COPY --from=builder /app/builder/dist/dashboard/browser ./
COPY --from=builder /app/builder/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/builder/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=builder /app/builder/app-hub/dashboard/dist/dashboard/browser ./
COPY --from=builder /app/builder/app-hub/dashboard/nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/builder/app-hub/dashboard/docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@ -0,0 +1,4 @@
{
"componentsPath": "src/app/core/base-components",
"importAlias": "@spartan-ng/helm"
}

View File

@ -1,5 +1,6 @@
#!/bin/bash
set -xe
: "${API_BASEURL?API_BASEURL is not provided}"
: "${GRAPHQL_ENDPOINT?GRAPHQL_ENDPOINT is not provided}"
: "${GRAPHQL_WS_ENDPOINT?GRAPHQL_WS_ENDPOINT is not provided}"
: "${MS_AUTHENTICATION_CLIENT_ID?MS_AUTHENTICATION_CLIENT_ID is not provided}"
@ -10,14 +11,14 @@ set -xe
: "${OTLP_SERVICE_NAME?OTLP_SERVICE_NAME is not provided}"
: "${COLLECTOR_SECRET?COLLECTOR_SECRET is not provided}"
env_vars=("GRAPHQL_ENDPOINT" "GRAPHQL_WS_ENDPOINT" "MS_AUTHENTICATION_CLIENT_ID" "MS_AUTHENTICATION_AUTHORITY" "MS_AUTHENTICATION_REDIRECT_URI" "MS_AUTHENTICATION_POST_LOGOUT_REDIRECT_URI" "OTLP_ENDPOINT" "OTLP_SERVICE_NAME" "COLLECTOR_SECRET")
env_vars=("API_BASEURL" "GRAPHQL_ENDPOINT" "GRAPHQL_WS_ENDPOINT" "MS_AUTHENTICATION_CLIENT_ID" "MS_AUTHENTICATION_AUTHORITY" "MS_AUTHENTICATION_REDIRECT_URI" "MS_AUTHENTICATION_POST_LOGOUT_REDIRECT_URI" "OTLP_ENDPOINT" "OTLP_SERVICE_NAME" "COLLECTOR_SECRET")
ls /usr/share/nginx/html/
for var in "${env_vars[@]}"
do
echo "$var ${!var}"
sed -i "s~{{$var}}~${!var}~gi" /usr/share/nginx/html/main*.js
sed -i "s~{{$var}}~${!var}~gi" /usr/share/nginx/html/*.js
done
exec "$@"

File diff suppressed because it is too large Load Diff

View File

@ -23,10 +23,12 @@
"@angular/platform-browser-dynamic": "^19.2.6",
"@angular/router": "^19.2.6",
"@apollo/client": "^3.13.8",
"@avicenna/shared-dtos": "^1.0.0",
"@azure/msal-angular": "^3.0.22",
"@azure/msal-browser": "^3.19.1",
"@grafana/faro-web-sdk": "^1.15.0",
"@grafana/faro-web-tracing": "^1.15.0",
"@hyperdx/browser": "^0.22.0",
"@ng-icons/core": "^31.4.0",
"@ng-icons/lucide": "^31.4.0",
"@ngneat/dialog": "^5.1.1",
"@ngneat/overview": "^6.0.0",
"@ngxpert/hot-toast": "^3.0.0",
@ -41,12 +43,18 @@
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-trace-web": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.34.0",
"@spartan-ng/brain": "^0.0.1-alpha.561",
"@tanstack/angular-query-experimental": "^5.90.20",
"apollo-angular": "^10.0.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"echarts": "^5.5.1",
"graphql": "^16.11.0",
"graphql-ws": "^5.16.0",
"ngx-scrollbar": "^19.1.1",
"rxjs": "~7.8.0",
"tailwind-merge": "^3.3.1",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
@ -60,6 +68,7 @@
"@graphql-codegen/typescript-apollo-angular": "^4.0.0",
"@graphql-codegen/typescript-operations": "^4.6.0",
"@material/material-color-utilities": "^0.3.0",
"@spartan-ng/cli": "^0.0.1-alpha.561",
"@tailwindcss/postcss": "^4.1.4",
"@types/jasmine": "~5.1.0",
"autoprefixer": "^10.4.19",
@ -70,7 +79,9 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.4.39",
"prettier": "^3.8.0",
"tailwindcss": "^4.1.4",
"tw-animate-css": "^1.4.0",
"typescript": "~5.5.2"
}
}

View File

@ -2,17 +2,21 @@ import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { AppShellComponent } from './core/components/app-shell/app-shell.component';
import { MsalService } from '@azure/msal-angular';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-root',
imports: [AppShellComponent, RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
selector: 'app-root',
imports: [AppShellComponent, RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
title = 'dashboard';
constructor(private readonly authService: MsalService) {
this.authService.handleRedirectObservable().subscribe();
this.authService
.handleRedirectObservable()
.pipe(takeUntilDestroyed())
.subscribe();
}
}

View File

@ -9,18 +9,40 @@ import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHotToastConfig } from '@ngxpert/hot-toast';
import {
provideTanStackQuery,
QueryClient,
} from '@tanstack/angular-query-experimental';
import { withDevtools } from '@tanstack/angular-query-experimental/devtools';
import { environment } from '../environments/environment';
import { routes } from './app.routes';
import { MsAuthenticationModule } from './core/components/ms-authentication/ms-authentication.module';
import { GlobalErrorHandler } from './core/telemetry/global-error-handler';
import { graphqlProvider } from './graphql.provider';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Daten gelten für 2 Minute als "frisch" (kein Refetch)
staleTime: 2000 * 60 * 1,
retry: 2,
refetchOnWindowFocus: true,
},
},
});
declare global {
interface Window {
__TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient;
}
}
// This code is for all users
window.__TANSTACK_QUERY_CLIENT__ = queryClient;
export const appConfig: ApplicationConfig = {
providers: [
// provideAppInitializer(() => {
// const initializerFn = faroInitializer();
// return initializerFn();
// }),
{
provide: ErrorHandler,
useClass: GlobalErrorHandler,
@ -36,6 +58,7 @@ export const appConfig: ApplicationConfig = {
postLogoutRedirectUri: environment.msAuth.postLogoutRedirectUri,
})
),
provideTanStackQuery(queryClient, withDevtools()),
provideHttpClient(),
graphqlProvider,
provideHotToastConfig(),

View File

@ -53,7 +53,7 @@ export const routes: Routes = [
{
path: 'tickets',
loadChildren: () =>
import('./pages/ticket-system/ticket-system.module').then(
import('./pages/tickets/ticket-system.module').then(
(m) => m.TicketSystemModule
),
canActivate: [MsalGuard],

View File

@ -0,0 +1,31 @@
import { HlmAlertDialog } from './lib/hlm-alert-dialog';
import { HlmAlertDialogActionButton } from './lib/hlm-alert-dialog-action-button';
import { HlmAlertDialogCancelButton } from './lib/hlm-alert-dialog-cancel-button';
import { HlmAlertDialogContent } from './lib/hlm-alert-dialog-content';
import { HlmAlertDialogDescription } from './lib/hlm-alert-dialog-description';
import { HlmAlertDialogFooter } from './lib/hlm-alert-dialog-footer';
import { HlmAlertDialogHeader } from './lib/hlm-alert-dialog-header';
import { HlmAlertDialogOverlay } from './lib/hlm-alert-dialog-overlay';
import { HlmAlertDialogTitle } from './lib/hlm-alert-dialog-title';
export * from './lib/hlm-alert-dialog';
export * from './lib/hlm-alert-dialog-action-button';
export * from './lib/hlm-alert-dialog-cancel-button';
export * from './lib/hlm-alert-dialog-content';
export * from './lib/hlm-alert-dialog-description';
export * from './lib/hlm-alert-dialog-footer';
export * from './lib/hlm-alert-dialog-header';
export * from './lib/hlm-alert-dialog-overlay';
export * from './lib/hlm-alert-dialog-title';
export const HlmAlertDialogImports = [
HlmAlertDialogContent,
HlmAlertDialogDescription,
HlmAlertDialogFooter,
HlmAlertDialogHeader,
HlmAlertDialogOverlay,
HlmAlertDialogTitle,
HlmAlertDialogActionButton,
HlmAlertDialogCancelButton,
HlmAlertDialog,
] as const;

View File

@ -0,0 +1,8 @@
import { Directive } from '@angular/core';
import { HlmButton } from '@spartan-ng/helm/button';
@Directive({
selector: 'button[hlmAlertDialogAction]',
hostDirectives: [{ directive: HlmButton, inputs: ['variant', 'size'] }],
})
export class HlmAlertDialogActionButton {}

View File

@ -0,0 +1,9 @@
import { Directive } from '@angular/core';
import { HlmButton, provideBrnButtonConfig } from '@spartan-ng/helm/button';
@Directive({
selector: 'button[hlmAlertDialogCancel]',
providers: [provideBrnButtonConfig({ variant: 'outline' })],
hostDirectives: [{ directive: HlmButton, inputs: ['variant', 'size'] }],
})
export class HlmAlertDialogCancelButton {}

View File

@ -0,0 +1,28 @@
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import { injectExposesStateProvider } from '@spartan-ng/brain/core';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Component({
selector: 'hlm-alert-dialog-content',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class]': '_computedClass()',
'[attr.data-state]': 'state()',
},
template: `
<ng-content />
`,
})
export class HlmAlertDialogContent {
private readonly _stateProvider = injectExposesStateProvider({ optional: true, host: true });
public readonly state = this._stateProvider?.state ?? signal('closed');
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() =>
hlm(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 relative z-50 mx-auto grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border p-6 shadow-lg data-[state=closed]:duration-200 data-[state=open]:duration-200 sm:mx-0 sm:max-w-lg',
this.userClass(),
),
);
}

View File

@ -0,0 +1,16 @@
import { Directive, computed, input } from '@angular/core';
import { BrnAlertDialogDescription } from '@spartan-ng/brain/alert-dialog';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Directive({
selector: '[hlmAlertDialogDescription]',
hostDirectives: [BrnAlertDialogDescription],
host: {
'[class]': '_computedClass()',
},
})
export class HlmAlertDialogDescription {
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() => hlm('text-muted-foreground text-sm', this.userClass()));
}

View File

@ -0,0 +1,20 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Component({
selector: 'hlm-alert-dialog-footer',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class]': '_computedClass()',
},
template: `
<ng-content />
`,
})
export class HlmAlertDialogFooter {
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() =>
hlm('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', this.userClass()),
);
}

View File

@ -0,0 +1,20 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Component({
selector: 'hlm-alert-dialog-header',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class]': '_computedClass()',
},
template: `
<ng-content />
`,
})
export class HlmAlertDialogHeader {
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() =>
hlm('flex flex-col gap-2 text-center sm:text-left', this.userClass()),
);
}

View File

@ -0,0 +1,26 @@
import { Directive, computed, effect, input, untracked } from '@angular/core';
import { injectCustomClassSettable } from '@spartan-ng/brain/core';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Directive({
selector: '[hlmAlertDialogOverlay],brn-alert-dialog-overlay[hlm]',
})
export class HlmAlertDialogOverlay {
private readonly _classSettable = injectCustomClassSettable({ optional: true, host: true });
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() =>
hlm(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/50',
this.userClass(),
),
);
constructor() {
effect(() => {
const classValue = this._computedClass();
untracked(() => this._classSettable?.setClassToCustomElement(classValue));
});
}
}

View File

@ -0,0 +1,16 @@
import { Directive, computed, input } from '@angular/core';
import { BrnAlertDialogTitle } from '@spartan-ng/brain/alert-dialog';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Directive({
selector: '[hlmAlertDialogTitle]',
hostDirectives: [BrnAlertDialogTitle],
host: {
'[class]': '_computedClass()',
},
})
export class HlmAlertDialogTitle {
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() => hlm('text-lg font-semibold', this.userClass()));
}

View File

@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import {
BRN_ALERT_DIALOG_DEFAULT_OPTIONS,
BrnAlertDialog,
BrnAlertDialogOverlay,
} from '@spartan-ng/brain/alert-dialog';
import { BrnDialog, provideBrnDialogDefaultOptions } from '@spartan-ng/brain/dialog';
import { HlmAlertDialogOverlay } from './hlm-alert-dialog-overlay';
@Component({
selector: 'hlm-alert-dialog',
exportAs: 'hlmAlertDialog',
imports: [BrnAlertDialogOverlay, HlmAlertDialogOverlay],
providers: [
{
provide: BrnDialog,
useExisting: forwardRef(() => HlmAlertDialog),
},
provideBrnDialogDefaultOptions({
...BRN_ALERT_DIALOG_DEFAULT_OPTIONS,
}),
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<brn-alert-dialog-overlay hlm />
<ng-content />
`,
})
export class HlmAlertDialog extends BrnAlertDialog {}

View File

@ -0,0 +1,23 @@
import { HlmAutocomplete } from './lib/hlm-autocomplete';
import { HlmAutocompleteEmpty } from './lib/hlm-autocomplete-empty';
import { HlmAutocompleteGroup } from './lib/hlm-autocomplete-group';
import { HlmAutocompleteItem } from './lib/hlm-autocomplete-item';
import { HlmAutocompleteList } from './lib/hlm-autocomplete-list';
import { HlmAutocompleteTrigger } from './lib/hlm-autocomplete-trigger';
export * from './lib/hlm-autocomplete';
export * from './lib/hlm-autocomplete-empty';
export * from './lib/hlm-autocomplete-group';
export * from './lib/hlm-autocomplete-item';
export * from './lib/hlm-autocomplete-list';
export * from './lib/hlm-autocomplete-trigger';
export * from './lib/hlm-autocomplete.token';
export const HlmAutocompleteImports = [
HlmAutocomplete,
HlmAutocompleteEmpty,
HlmAutocompleteGroup,
HlmAutocompleteItem,
HlmAutocompleteList,
HlmAutocompleteTrigger,
] as const;

View File

@ -0,0 +1,14 @@
import { Directive, computed, input } from '@angular/core';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
@Directive({
selector: '[hlmAutocompleteEmpty]',
host: {
'[class]': '_computedClass()',
},
})
export class HlmAutocompleteEmpty {
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() => hlm('py-6 text-center text-sm', this.userClass()));
}

View File

@ -0,0 +1,27 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { BrnAutocompleteGroup } from '@spartan-ng/brain/autocomplete';
import { hlm } from '@spartan-ng/helm/utils';
@Component({
selector: 'hlm-autocomplete-group',
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [
{
directive: BrnAutocompleteGroup,
inputs: ['id'],
},
],
host: {
'[class]': '_computedClass()',
},
template: '<ng-content />',
})
export class HlmAutocompleteGroup {
/** The user defined class */
public readonly userClass = input<string>('', { alias: 'class' });
/** The styles to apply */
protected readonly _computedClass = computed(() =>
hlm('text-foreground block overflow-hidden p-1', this.userClass()),
);
}

View File

@ -0,0 +1,33 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { BrnAutocompleteItem } from '@spartan-ng/brain/autocomplete';
import { hlm } from '@spartan-ng/helm/utils';
@Component({
selector: 'button[hlm-autocomplete-item]',
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [
{
directive: BrnAutocompleteItem,
inputs: ['value', 'disabled', 'id'],
outputs: ['selected'],
},
],
host: {
'[class]': '_computedClass()',
},
template: `
<ng-content />
`,
})
export class HlmAutocompleteItem {
/** The user defined class */
public readonly userClass = input<string>('', { alias: 'class' });
/** The styles to apply */
protected readonly _computedClass = computed(() =>
hlm(
'data-[selected]:bg-accent data-[selected=true]:text-accent-foreground [&>ng-icon]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-start text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[hidden]:hidden [&>ng-icon]:pointer-events-none [&>ng-icon]:shrink-0 [&>ng-icon]:text-base',
this.userClass(),
),
);
}

View File

@ -0,0 +1,27 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { BrnAutocompleteList } from '@spartan-ng/brain/autocomplete';
import { hlm } from '@spartan-ng/helm/utils';
@Component({
selector: 'hlm-autocomplete-list',
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [
{
directive: BrnAutocompleteList,
inputs: ['id'],
},
],
host: {
'[class]': '_computedClass()',
},
template: '<ng-content />',
})
export class HlmAutocompleteList {
/** The user defined class */
public readonly userClass = input<string>('', { alias: 'class' });
/** The styles to apply */
protected readonly _computedClass = computed(() =>
hlm('block max-h-60 overflow-x-hidden overflow-y-auto', this.userClass()),
);
}

View File

@ -0,0 +1,32 @@
import type { BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, Directive, ElementRef, inject, input } from '@angular/core';
import { BrnDialog } from '@spartan-ng/brain/dialog';
@Directive({
selector: '[hlmAutocompleteTrigger]',
host: {
'(click)': 'open()',
},
})
export class HlmAutocompleteTrigger {
private readonly _host = inject(ElementRef, { host: true });
private readonly _brnDialog = inject(BrnDialog, { optional: true });
/** Whether the trigger is disabled. */
public readonly disabledTrigger = input<boolean, BooleanInput>(false, {
transform: booleanAttribute,
});
constructor() {
if (!this._brnDialog) return;
this._brnDialog.mutableAttachTo.set(this._host.nativeElement);
}
open() {
if (this.disabledTrigger()) return;
this._brnDialog?.open();
}
}

View File

@ -0,0 +1,35 @@
import { inject, InjectionToken, type ValueProvider } from '@angular/core';
export type TransformValueToString<T> = (option: T) => string;
export interface HlmAutocompleteConfig<T, V = T> {
transformValueToSearch: TransformValueToString<T>;
transformOptionToString: TransformValueToString<T>;
transformOptionToValue: ((option: T) => V) | undefined;
requireSelection: boolean;
showClearBtn: boolean;
debounceTime: number;
}
function getDefaultConfig<T, V = T>(): HlmAutocompleteConfig<T, V> {
return {
transformValueToSearch: (option: T) => (typeof option === 'string' ? option : String(option)),
transformOptionToString: (option: T) => (typeof option === 'string' ? option : String(option)),
transformOptionToValue: undefined,
requireSelection: false,
showClearBtn: false,
debounceTime: 150,
};
}
const HlmAutocompleteConfigToken = new InjectionToken<HlmAutocompleteConfig<unknown, unknown>>('HlmAutocompleteConfig');
export function provideHlmAutocompleteConfig<T, V = T>(config: Partial<HlmAutocompleteConfig<T, V>>): ValueProvider {
return { provide: HlmAutocompleteConfigToken, useValue: { ...getDefaultConfig(), ...config } };
}
export function injectHlmAutocompleteConfig<T, V = T>(): HlmAutocompleteConfig<T, V> {
return (
(inject(HlmAutocompleteConfigToken, { optional: true }) as HlmAutocompleteConfig<T, V> | null) ?? getDefaultConfig()
);
}

View File

@ -0,0 +1,426 @@
import type { BooleanInput } from '@angular/cdk/coercion';
import { NgTemplateOutlet } from '@angular/common';
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
forwardRef,
inject,
input,
linkedSignal,
model,
output,
type TemplateRef,
viewChild,
} from '@angular/core';
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { provideIcons } from '@ng-icons/core';
import {
lucideChevronDown,
lucideCircleX,
lucideSearch,
} from '@ng-icons/lucide';
import {
BrnAutocomplete,
BrnAutocompleteEmpty,
BrnAutocompleteImports,
} from '@spartan-ng/brain/autocomplete';
import { debouncedSignal } from '@spartan-ng/brain/core';
import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
import { BrnPopoverImports } from '@spartan-ng/brain/popover';
import { HlmIconImports } from '@spartan-ng/helm/icon';
import { HlmPopoverImports } from '@spartan-ng/helm/popover';
import { hlm } from '@spartan-ng/helm/utils';
import type { ClassValue } from 'clsx';
import { HlmAutocompleteEmpty } from './hlm-autocomplete-empty';
import { HlmAutocompleteGroup } from './hlm-autocomplete-group';
import { HlmAutocompleteItem } from './hlm-autocomplete-item';
import { HlmAutocompleteList } from './hlm-autocomplete-list';
import { HlmInputGroupImports } from '@spartan-ng/helm/input-group';
import { HlmAutocompleteTrigger } from './hlm-autocomplete-trigger';
import { injectHlmAutocompleteConfig } from './hlm-autocomplete.token';
export const HLM_AUTOCOMPLETE_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => HlmAutocomplete),
multi: true,
};
@Component({
selector: 'hlm-autocomplete',
imports: [
NgTemplateOutlet,
BrnAutocomplete,
BrnAutocompleteEmpty,
HlmAutocompleteEmpty,
HlmAutocompleteGroup,
HlmAutocompleteItem,
HlmAutocompleteList,
HlmAutocompleteTrigger,
BrnPopoverImports,
HlmPopoverImports,
HlmIconImports,
HlmInputGroupImports,
BrnAutocompleteImports,
],
providers: [
HLM_AUTOCOMPLETE_VALUE_ACCESSOR,
provideIcons({ lucideSearch, lucideChevronDown, lucideCircleX }),
],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class]': '_computedClass()',
},
template: `
@let transformer = transformOptionToValue();
<brn-popover
#popover
align="start"
autoFocus="first-heading"
sideOffset="5"
closeDelay="100"
(closed)="_closed()"
[closeOnOutsidePointerEvents]="true"
>
<div brnAutocomplete (selectionCleared)="_selectionCleared()">
<div
hlmInputGroup
hlmAutocompleteTrigger
[class]="_computedAutocompleteSearchClass()"
[disabledTrigger]="!_search() || _disabled()"
>
<input
#input
brnAutocompleteSearchInput
hlmInputGroupInput
type="text"
autocomplete="off"
[id]="inputId()"
[class]="_computedAutocompleteInputClass()"
[placeholder]="searchPlaceholderText()"
[disabled]="_disabled()"
[value]="search()"
(input)="_searchChanged($event)"
data-bwignore="true"
data-1p-ignore="true"
data-lpignore="true"
/>
<div hlmInputGroupAddon>
<ng-icon name="lucideSearch" />
</div>
<div hlmInputGroupAddon align="inline-end">
@if (showClearBtn() && value() !== undefined) {
<button
hlmInputGroupButton
type="button"
tabindex="-1"
aria-label="clear"
[disabled]="_disabled()"
(click)="_selectionCleared()"
size="icon-xs"
>
<ng-icon name="lucideCircleX" />
</button>
}
<button
hlmInputGroupButton
type="button"
tabindex="-1"
[attr.aria-label]="ariaLabelToggleButton()"
[disabled]="_disabled()"
(click)="_toggleOptions()"
size="icon-xs"
>
<ng-icon name="lucideChevronDown" />
</button>
</div>
</div>
<div
*brnPopoverContent="let ctx"
hlmPopoverContent
class="p-0"
[style.width.px]="_elementRef.nativeElement.offsetWidth"
>
<hlm-autocomplete-list
[class]="_computedAutocompleteListClass()"
[class.hidden]="filteredOptions().length === 0"
>
<hlm-autocomplete-group>
@for (option of filteredOptions(); track option) {
<button
hlm-autocomplete-item
[class]="_computedAutocompleteItemClass()"
[value]="transformer ? transformer(option) : option"
(selected)="_optionSelected(option)"
>
@if (optionTemplate(); as optionTemplate) {
<ng-container
*ngTemplateOutlet="
optionTemplate;
context: { $implicit: option }
"
/>
} @else {
{{ transformOptionToString()(option) }}
}
</button>
}
</hlm-autocomplete-group>
</hlm-autocomplete-list>
<div
*brnAutocompleteEmpty
hlmAutocompleteEmpty
[class]="_computedAutocompleteEmptyClass()"
>
@if (loading()) {
<ng-content select="[loading]">{{ loadingText() }}</ng-content>
} @else {
<ng-content select="[empty]">{{ emptyText() }}</ng-content>
}
</div>
</div>
</div>
</brn-popover>
`,
})
export class HlmAutocomplete<T, V = T> implements ControlValueAccessor {
private static _id = 0;
private readonly _config = injectHlmAutocompleteConfig<T, V>();
private readonly _brnAutocomplete = viewChild.required(BrnAutocomplete);
private readonly _inputRef = viewChild.required('input', {
read: ElementRef,
});
protected readonly _elementRef = inject(ElementRef<HTMLElement>);
/** The user defined class */
public readonly userClass = input<ClassValue>('', { alias: 'class' });
protected readonly _computedClass = computed(() =>
hlm('block w-full', this.userClass())
);
/** Custom class for the autocomplete search container. */
public readonly autocompleteSearchClass = input<ClassValue>('');
protected readonly _computedAutocompleteSearchClass = computed(() =>
hlm('', this.autocompleteSearchClass())
);
/** Custom class for the autocomplete input. */
public readonly autocompleteInputClass = input<ClassValue>('');
protected readonly _computedAutocompleteInputClass = computed(() =>
hlm('', this.autocompleteInputClass())
);
/** Custom class for the autocomplete list. */
public readonly autocompleteListClass = input<ClassValue>('');
protected readonly _computedAutocompleteListClass = computed(() =>
hlm('', this.autocompleteListClass())
);
/** Custom class for each autocomplete item. */
public readonly autocompleteItemClass = input<ClassValue>('');
protected readonly _computedAutocompleteItemClass = computed(() =>
hlm('', this.autocompleteItemClass())
);
/** Custom class for the empty and loading state container. */
public readonly autocompleteEmptyClass = input<ClassValue>('');
protected readonly _computedAutocompleteEmptyClass = computed(() =>
hlm('', this.autocompleteEmptyClass())
);
/** The list of filtered options to display in the autocomplete. */
public readonly filteredOptions = input<T[]>([]);
/** The selected value. */
public readonly value = model<T | V>();
/** Debounce time in milliseconds for the search input. */
public readonly debounceTime = input<number>(this._config.debounceTime);
/** The search query. */
public readonly search = model<string>('');
/** Debounced search query. */
protected readonly _search = debouncedSignal(
this.search,
this.debounceTime()
);
/** Function to transform an option value to a search string. Defaults to identity function for strings. */
public readonly transformValueToSearch = input<(option: T) => string>(
this._config.transformValueToSearch
);
/** Whether selection of an option is required. */
public readonly requireSelection = input<boolean, BooleanInput>(
this._config.requireSelection,
{
transform: booleanAttribute,
}
);
/** Function to transform an option value to a display string. Defaults to identity function for strings. */
public readonly transformOptionToString = input<(option: T) => string>(
this._config.transformOptionToString
);
/** Function to transform the object to the value. */
public readonly transformOptionToValue = input<
((option: T) => V) | undefined
>(this._config.transformOptionToValue);
/** Function to display the selected value as a string. */
public readonly displayWith = input<((value: V) => string) | undefined>(
undefined
);
/** Computed function to get the display value for the selected option. */
protected readonly _displaySearchValue = computed(() => {
const displayWith = this.displayWith();
if (displayWith) {
return displayWith;
} else {
return this.transformValueToSearch();
}
});
/** Optional template for rendering each option. */
public readonly optionTemplate =
input<TemplateRef<HlmAutocompleteOption<T>>>();
/** Whether the autocomplete is in a loading state. */
public readonly loading = input<boolean, BooleanInput>(false, {
transform: booleanAttribute,
});
/** Whether to show the clear button when a option is selected. */
public readonly showClearBtn = input<boolean, BooleanInput>(
this._config.showClearBtn,
{
transform: booleanAttribute,
}
);
/** Placeholder text for the input field. */
public readonly searchPlaceholderText = input('Select an option');
/** Text to display when loading options. */
public readonly loadingText = input('Loading options...');
/** Text to display when no options are found. */
public readonly emptyText = input('No options found');
/** Aria label for the toggle button. */
public readonly ariaLabelToggleButton = input('Toggle options');
/** The id of the input field. */
public readonly inputId = input(
`hlm-autocomplete-input-${++HlmAutocomplete._id}`
);
/** Whether the autocomplete is disabled. */
public readonly disabled = input<boolean, BooleanInput>(false, {
transform: booleanAttribute,
});
protected readonly _disabled = linkedSignal(() => this.disabled());
/** Emitted when the selected value changes. */
public readonly valueChange = output<T | V | null>();
/** Emitted when the search query changes. */
public readonly searchChange = output<string>();
protected _onChange?: ChangeFn<T | V | null>;
protected _onTouched?: TouchFn;
constructor() {
effect(() => {
const search = this._search();
this.searchChange.emit(search);
});
}
protected _searchChanged(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.search.set(value ?? '');
if (!this._brnAutocomplete().isExpanded() && value.length > 0) {
this._brnAutocomplete().open();
}
}
/** Toggle the options panel */
protected _toggleOptions() {
if (this._search() || this.filteredOptions().length > 0) {
// only toggle if there's a search term or options to show
this._brnAutocomplete().toggle();
}
this._inputRef().nativeElement.focus();
}
/** Clear the current selection and search input */
protected _selectionCleared() {
this.value.set(undefined);
this._onChange?.(null);
this.valueChange.emit(null);
this.search.set('');
}
protected _optionSelected(option: T) {
const transformer = this.transformOptionToValue();
const value = transformer ? transformer(option) : option;
this.value.set(value);
this._onChange?.(value);
this.valueChange.emit(value);
const searchValue = this._displaySearchValue()(value as any);
this.search.set(searchValue ?? '');
this._brnAutocomplete().close();
}
/** CONTROL VALUE ACCESSOR */
public writeValue(value: T | V | null): void {
this.value.set(value ? value : undefined);
const searchValue = value ? this._displaySearchValue()(value as any) : '';
this.search.set(searchValue);
}
public registerOnChange(fn: ChangeFn<T | V | null>): void {
this._onChange = fn;
}
public registerOnTouched(fn: TouchFn): void {
this._onTouched = fn;
}
public setDisabledState(isDisabled: boolean): void {
this._disabled.set(isDisabled);
}
protected _closed() {
if (this.requireSelection()) {
const value = this.value();
const searchValue = value ? this._displaySearchValue()(value as any) : '';
this.search.set(searchValue ?? '');
}
}
}
export interface HlmAutocompleteOption<T> {
$implicit: T;
}

View File

@ -0,0 +1,5 @@
import { HlmBadge } from './lib/hlm-badge';
export * from './lib/hlm-badge';
export const HlmBadgeImports = [HlmBadge] as const;

Some files were not shown because too many files have changed in this diff Show More