From 6a09e086c472180948f8b94331b005bd6f4f1966 Mon Sep 17 00:00:00 2001 From: florianhandke Date: Mon, 15 Jun 2026 10:05:35 +0200 Subject: [PATCH 01/18] Enhance database queries and add indexes for performance improvements --- trustpoint/devices/revocation.py | 5 ++- trustpoint/devices/views/revoke.py | 2 +- .../pki/migrations/0002_tp_v0_6_0_dev1.py | 24 ++++++++++++ trustpoint/pki/models/certificate.py | 16 +++++++- trustpoint/pki/models/issued_credential.py | 8 ++-- trustpoint/pki/views/issuing_cas.py | 38 ++++++++----------- trustpoint/request/authentication/est.py | 4 +- trustpoint/request/authentication/rest.py | 4 +- 8 files changed, 71 insertions(+), 30 deletions(-) diff --git a/trustpoint/devices/revocation.py b/trustpoint/devices/revocation.py index 4adc2606e..87cbb88e3 100644 --- a/trustpoint/devices/revocation.py +++ b/trustpoint/devices/revocation.py @@ -13,7 +13,10 @@ class DeviceCredentialRevocation: def revoke_certificate(issued_credential_id: int, reason: str) -> tuple[bool, str]: """Revokes a certificate given an ID of an IssuedCredentialModel instance.""" try: - issued_credential = IssuedCredentialModel.objects.get(id=issued_credential_id) + issued_credential = IssuedCredentialModel.objects.select_related( + 'credential__certificate__revoked_certificate', + 'domain__issuing_ca', + ).get(id=issued_credential_id) except IssuedCredentialModel.DoesNotExist: return False, 'The credential to revoke does not exist.' diff --git a/trustpoint/devices/views/revoke.py b/trustpoint/devices/views/revoke.py index 0effd91d6..46994e931 100644 --- a/trustpoint/devices/views/revoke.py +++ b/trustpoint/devices/views/revoke.py @@ -266,7 +266,7 @@ def post(self, request: HttpRequest, *_args: Any, **_kwargs: Any) -> HttpRespons device__in=self.queryset, credential__certificate__revoked_certificate__isnull=True, credential__certificate__not_valid_after__gte=now, - ) + ).select_related('device', 'domain') n_revoked = 0 for credential in issued_credentials_to_revoke_qs: diff --git a/trustpoint/pki/migrations/0002_tp_v0_6_0_dev1.py b/trustpoint/pki/migrations/0002_tp_v0_6_0_dev1.py index fd8dfd488..9a6935495 100644 --- a/trustpoint/pki/migrations/0002_tp_v0_6_0_dev1.py +++ b/trustpoint/pki/migrations/0002_tp_v0_6_0_dev1.py @@ -19,4 +19,28 @@ class Migration(migrations.Migration): name='domain_credential_profile', field=models.ForeignKey(blank=True, help_text='Certificate profile used for issuing domain credentials. Defaults to "domain_credential".', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='domains_as_credential_profile', to='pki.certificateprofilemodel', verbose_name='Domain Credential Profile'), ), + migrations.AddIndex( + model_name='certificatemodel', + index=models.Index(fields=['not_valid_after'], name='pki_cert_not_valid_after_idx'), + ), + migrations.AddIndex( + model_name='certificatemodel', + index=models.Index(fields=['not_valid_before'], name='pki_cert_not_valid_before_idx'), + ), + migrations.AddIndex( + model_name='certificatemodel', + index=models.Index(fields=['serial_number'], name='pki_cert_serial_num_idx'), + ), + migrations.AddIndex( + model_name='certificatemodel', + index=models.Index(fields=['subject_public_bytes'], name='pki_cert_subj_pub_bytes_idx'), + ), + migrations.AddIndex( + model_name='certificatemodel', + index=models.Index(fields=['issuer_public_bytes', 'issuer_id'], name='pki_cert_iss_pub_bytes_isid_idx'), + ), + migrations.AddIndex( + model_name='revokedcertificatemodel', + index=models.Index(fields=['ca', 'revoked_at'], name='pki_revoked_ca_revoked_at_idx'), + ), ] diff --git a/trustpoint/pki/models/certificate.py b/trustpoint/pki/models/certificate.py index 660f319fe..34837750e 100644 --- a/trustpoint/pki/models/certificate.py +++ b/trustpoint/pki/models/certificate.py @@ -4,7 +4,7 @@ import datetime from types import MappingProxyType -from typing import Any +from typing import Any, ClassVar from cryptography import x509 from cryptography.exceptions import InvalidSignature @@ -362,6 +362,16 @@ class PublicKeyEcCurveOidChoices(models.TextChoices): class Meta(TypedModelMeta): """Meta class configuration.""" + indexes: ClassVar[list[models.Index]] = [ + models.Index(fields=['not_valid_after'], name='pki_cert_not_valid_after_idx'), + models.Index(fields=['not_valid_before'], name='pki_cert_not_valid_before_idx'), + models.Index(fields=['serial_number'], name='pki_cert_serial_num_idx'), + models.Index(fields=['subject_public_bytes'], + name='pki_cert_subj_pub_bytes_idx'), + models.Index(fields=['issuer_public_bytes', 'issuer_id'], + name='pki_cert_iss_pub_bytes_isid_idx'), + ] + # ------------------------------------------ Magic and default methods ------------------------------------------- def __repr__(self) -> str: @@ -796,6 +806,10 @@ class ReasonCode(models.TextChoices): class Meta(TypedModelMeta): """Meta class configuration.""" + indexes: ClassVar[list[models.Index]] = [ + models.Index(fields=['ca', 'revoked_at'], name='pki_revoked_ca_revoked_at_idx'), + ] + def __str__(self) -> str: """String representation of the RevokedCertificateModel instance.""" return f'RevokedCertificate({self.certificate.common_name})' diff --git a/trustpoint/pki/models/issued_credential.py b/trustpoint/pki/models/issued_credential.py index 747a05e2c..ee45c67e2 100644 --- a/trustpoint/pki/models/issued_credential.py +++ b/trustpoint/pki/models/issued_credential.py @@ -2,6 +2,7 @@ from __future__ import annotations +import datetime from typing import TYPE_CHECKING from cryptography.hazmat.primitives import hashes @@ -86,10 +87,11 @@ def revoke(self) -> None: if domain is None or domain.issuing_ca is None: return ca = domain.issuing_ca + now = datetime.datetime.now(datetime.UTC) cert: CertificateModel - for cert in self.credential.certificates.all(): - status = cert.certificate_status - if status in (CertificateModel.CertificateStatus.REVOKED, CertificateModel.CertificateStatus.EXPIRED): + for cert in self.credential.certificates.select_related('revoked_certificate').all(): + + if hasattr(cert, 'revoked_certificate') or cert.not_valid_after <= now: continue RevokedCertificateModel.objects.create( certificate=cert, revocation_reason=RevokedCertificateModel.ReasonCode.CESSATION, ca=ca diff --git a/trustpoint/pki/views/issuing_cas.py b/trustpoint/pki/views/issuing_cas.py index 5898fe674..51a9d4cc0 100644 --- a/trustpoint/pki/views/issuing_cas.py +++ b/trustpoint/pki/views/issuing_cas.py @@ -1402,28 +1402,22 @@ def _build_certificate_chain_for_credential(self, credential: CredentialModel, p def _find_existing_ca_for_certificate(self, cert: x509.Certificate) -> CaModel | None: """Find an existing CA that has the given certificate.""" - # TODO(FHK): comparing the subject public bytes is not sufficient # noqa: FIX002 - for existing_ca in CaModel.objects.filter(certificate__isnull=False): - try: - ca_cert = existing_ca.get_certificate() - if ca_cert and (ca_cert.subject.public_bytes() == cert.subject.public_bytes() and - ca_cert.issuer.public_bytes() == cert.issuer.public_bytes()): - return existing_ca - except (AttributeError, ValueError) as e: - self.logger.debug('Error checking existing keyless CA certificate: %s', e) - continue - - for existing_ca in CaModel.objects.filter(credential__isnull=False): - try: - ca_cert = existing_ca.get_certificate() - if ca_cert and (ca_cert.subject.public_bytes() == cert.subject.public_bytes() and - ca_cert.issuer.public_bytes() == cert.issuer.public_bytes()): - return existing_ca - except (AttributeError, ValueError) as e: - self.logger.debug('Error checking existing issuing CA certificate: %s', e) - continue - - return None + # TODO(FHK): subject+issuer byte equality is not sufficient for # noqa: FIX002 + + subject_bytes = cert.subject.public_bytes().hex().upper() + issuer_bytes = cert.issuer.public_bytes().hex().upper() + + keyless_match = CaModel.objects.filter( + certificate__subject_public_bytes=subject_bytes, + certificate__issuer_public_bytes=issuer_bytes, + ).first() + if keyless_match: + return keyless_match + + return CaModel.objects.filter( + credential__certificate__subject_public_bytes=subject_bytes, + credential__certificate__issuer_public_bytes=issuer_bytes, + ).first() class KeylessCaConfigView(LoggerMixin, KeylessCaContextMixin, DetailView[CaModel]): diff --git a/trustpoint/request/authentication/est.py b/trustpoint/request/authentication/est.py index c35abd33f..ac9387fbe 100644 --- a/trustpoint/request/authentication/est.py +++ b/trustpoint/request/authentication/est.py @@ -33,7 +33,9 @@ def authenticate(self, context: BaseRequestContext) -> None: password = context.est_password try: - device = DeviceModel.objects.select_related().filter( + device = DeviceModel.objects.select_related( + 'onboarding_config', 'no_onboarding_config' + ).filter( common_name=username ).first() diff --git a/trustpoint/request/authentication/rest.py b/trustpoint/request/authentication/rest.py index 2b6105b7c..ae7098611 100644 --- a/trustpoint/request/authentication/rest.py +++ b/trustpoint/request/authentication/rest.py @@ -30,7 +30,9 @@ def authenticate(self, context: BaseRequestContext) -> None: password = context.rest_password try: - device = DeviceModel.objects.select_related().filter( + device = DeviceModel.objects.select_related( + 'onboarding_config', 'no_onboarding_config' + ).filter( common_name=username ).first() From b2602fbd33a3a76f409aa3bc763d51ecc4df3dac Mon Sep 17 00:00:00 2001 From: florianhandke Date: Mon, 15 Jun 2026 10:52:41 +0200 Subject: [PATCH 02/18] Refactor Docker Compose and environment configuration --- .env | 28 +- .env.example | 37 +++ docker-compose.softhsm.yml | 20 +- docker-compose.yml | 46 ++- .../getting_started/quickstart_setup.rst | 306 ++++++++---------- 5 files changed, 245 insertions(+), 192 deletions(-) create mode 100644 .env.example diff --git a/.env b/.env index e29e5de67..5bee86dad 100644 --- a/.env +++ b/.env @@ -1,7 +1,21 @@ -# .env -# Enter all origins allowed to access the Trustpoint server (IPs, mDNS, or DNS domains) -# Example: TP_URLS=10.10.0.2, mytrustpoint.local:8443, trustpoint.example.com -# Note: Do not include the protocol (http:// or https://) in the URLs, and separate multiple URLs with commas. -# You only need to include the port if it is not the default (80 for HTTP, 443 for HTTPS). -# No need to include localhost, unless you are using a custom port. -TP_URLS=trustpoint.local, \ No newline at end of file +# .env — local configuration for docker compose +# This file is listed in .gitignore and must never be committed. +# See .env.example for a documented template. + +# ── Required ───────────────────────────────────────────────────────────────── + +# PostgreSQL credentials used by trustpoint, trustpoint-worker, and postgres. +# docker compose will refuse to start if either value is empty. +DATABASE_USER=admin +DATABASE_PASSWORD=testing321 + +# ── Optional ───────────────────────────────────────────────────────────────── + +# PostgreSQL database name (default: trustpoint_db) +# POSTGRES_DB=trustpoint_db + +# Hostnames / IPs at which Trustpoint is reachable, comma-separated. +# Do not include the protocol (http:// or https://). +# Only include a port if it differs from the default (80/443). +# Example: TP_URLS=10.10.0.2,mytrustpoint.local,trustpoint.example.com +TP_URLS=trustpoint.local, diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..ea9a4ad6d --- /dev/null +++ b/.env.example @@ -0,0 +1,37 @@ +# ── Copy this file to .env and fill in the values before running docker compose ── +# +# Usage: +# cp .env.example .env +# $EDITOR .env +# docker compose up -d +# +# The .env file must NEVER be committed to version control. +# It is already listed in .gitignore. + +# --------------------------------------------------------------------------- +# Required – docker compose will refuse to start if these are left empty. +# --------------------------------------------------------------------------- + +# Database username. +# Avoid generic defaults such as 'admin' or 'postgres' in production. +DATABASE_USER= + +# Database password. +# Use a long, randomly generated value in production. +# Generate one with: openssl rand -base64 32 +DATABASE_PASSWORD= + +# --------------------------------------------------------------------------- +# Optional overrides (defaults shown as comments) +# --------------------------------------------------------------------------- + +# PostgreSQL database name. +# Default: trustpoint_db +# POSTGRES_DB=trustpoint_db + +# Hostnames / IP addresses at which Trustpoint is reachable, comma-separated. +# Used for TLS certificate SAN generation and Django ALLOWED_HOSTS. +# Do NOT include the protocol (http:// or https://). +# Only include a port if it differs from the default (80 for HTTP, 443 for HTTPS). +# Default: localhost +# TP_URLS=trustpoint.local,10.0.0.1,trustpoint.example.com diff --git a/docker-compose.softhsm.yml b/docker-compose.softhsm.yml index 0173e5f8e..b159f3be1 100644 --- a/docker-compose.softhsm.yml +++ b/docker-compose.softhsm.yml @@ -5,12 +5,13 @@ services: dockerfile: docker/trustpoint/Dockerfile image: trustpointproject/trustpoint:latest container_name: trustpoint + restart: unless-stopped ports: - "80:80" - "443:443" depends_on: postgres: - condition: service_started + condition: service_healthy softhsm: condition: service_healthy volumes: @@ -28,6 +29,12 @@ services: - db_password - hsm_pin - hsm_so_pin + healthcheck: + test: ["CMD-SHELL", "curl -fsk --max-time 5 https://localhost/ > /dev/null"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s postgres: build: @@ -35,8 +42,10 @@ services: dockerfile: docker/db/Dockerfile image: trustpointproject/postgres:latest container_name: postgres + restart: unless-stopped ports: - - "5432:5432" + # Bind to loopback only – PostgreSQL must not be reachable from the network. + - "127.0.0.1:5432:5432" volumes: - postgres_data:/var/lib/postgresql environment: @@ -46,12 +55,19 @@ services: secrets: - db_user - db_password + healthcheck: + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s softhsm: build: context: . dockerfile: docker/softhsm/Dockerfile container_name: softhsm + restart: unless-stopped ports: - "5657:5657" healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 1663e3bd6..005d242a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,35 +5,45 @@ services: dockerfile: docker/trustpoint/Dockerfile image: trustpointproject/trustpoint:latest container_name: trustpoint + restart: unless-stopped ports: - "80:80" - "443:443" depends_on: - - postgres + postgres: + condition: service_healthy environment: - POSTGRES_DB: "trustpoint_db" - DATABASE_USER: "admin" - DATABASE_PASSWORD: "testing321" + POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" + DATABASE_USER: "${DATABASE_USER:?DATABASE_USER env var must be set}" + DATABASE_PASSWORD: "${DATABASE_PASSWORD:?DATABASE_PASSWORD env var must be set}" DATABASE_HOST: "postgres" DATABASE_PORT: "5432" TP_URLS: ${TP_URLS} + healthcheck: + test: ["CMD-SHELL", "curl -fsk --max-time 5 https://localhost/ > /dev/null"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s trustpoint-worker: build: context: . dockerfile: docker/trustpoint/Dockerfile image: trustpointproject/trustpoint:latest + restart: on-failure depends_on: - - postgres - - trustpoint + postgres: + condition: service_healthy + trustpoint: + condition: service_healthy environment: - POSTGRES_DB: "trustpoint_db" - DATABASE_USER: "admin" - DATABASE_PASSWORD: "testing321" + POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" + DATABASE_USER: "${DATABASE_USER:?DATABASE_USER env var must be set}" + DATABASE_PASSWORD: "${DATABASE_PASSWORD:?DATABASE_PASSWORD env var must be set}" DATABASE_HOST: "postgres" DATABASE_PORT: "5432" TRUSTPOINT_SERVICE_ROLE: "worker" - restart: on-failure postgres: build: @@ -41,14 +51,22 @@ services: dockerfile: docker/db/Dockerfile image: trustpointproject/postgres:latest container_name: postgres + restart: unless-stopped ports: - - "5432:5432" + # Bind to loopback only – PostgreSQL must not be reachable from the network. + - "127.0.0.1:5432:5432" volumes: - postgres_data:/var/lib/postgresql environment: - POSTGRES_USER: "admin" - POSTGRES_PASSWORD: "testing321" - POSTGRES_DB: "trustpoint_db" + POSTGRES_USER: "${DATABASE_USER:?DATABASE_USER env var must be set}" + POSTGRES_PASSWORD: "${DATABASE_PASSWORD:?DATABASE_PASSWORD env var must be set}" + POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" + healthcheck: + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s volumes: postgres_data: \ No newline at end of file diff --git a/docs/source/getting_started/quickstart_setup.rst b/docs/source/getting_started/quickstart_setup.rst index a651ec58c..18f650207 100644 --- a/docs/source/getting_started/quickstart_setup.rst +++ b/docs/source/getting_started/quickstart_setup.rst @@ -3,266 +3,234 @@ Quickstart Setup Guide ====================== -This guide provides an introduction to Trustpoint and instructions for setting up the Trustpoint using Docker and Docker Compose. +This guide covers setting up Trustpoint using Docker and Docker Compose. -Prerequisites ---------------- -Make sure you have the following installed: +Prerequisites +------------- -1. **Docker**: Version 20.10 or higher. -2. **Docker Compose**: Version v2.32.4 or higher. -3. **Git**: To clone the Trustpoint repository. +- **Docker** 20.10 or higher +- **Docker Compose** v2.32.4 or higher +- **Git** Getting started with the Trustpoint Wizard script ---------------------------------------------------- +-------------------------------------------------- -The ``tp_wizard.sh`` offers a convenient guided CLI for setting up a Docker container environment. +The ``tp_wizard.sh`` script provides a guided CLI for setting up a Docker environment. +This requires a Linux host. -1. **Clone** the Trustpoint repository +.. code-block:: bash - First, clone the Trustpoint source code from the official repository: + git clone https://github.com/Trustpoint-Project/trustpoint.git + cd trustpoint + ./tp_wizard.sh - .. code-block:: bash +Convenience commands: - git clone https://github.com/Trustpoint-Project/trustpoint.git - cd trustpoint +- ``./tp_wizard.sh up`` — start Trustpoint and Postgres with default testing credentials (testing only). +- ``./tp_wizard.sh up demo`` — additionally start SFTP and mailpit demo servers (testing only). +- ``./tp_wizard.sh down`` — stop and remove all containers. +- ``./tp_wizard.sh nuke`` — remove all containers and delete all stored data. -2. **Interactively configure** the Trustpoint environment using the script +Getting started with Docker Compose +------------------------------------ - This requires a Linux host. +The .env file +^^^^^^^^^^^^^ - .. code-block:: bash - - ./tp_wizard.sh +All deployments require a ``.env`` file in the project root. +Copy the provided template and fill in the required values: -| For testing, you can use ``./tp_wizard.sh up`` to directly start the Trustpoint and integrated postgres DB container with default testing credentials. -| Strictly for testing use only, use ``./tp_wizard.sh up demo`` to additionally start SFTP and mailpit demo servers. -| Use ``./tp_wizard.sh down`` to stop and remove all containers. -| To completely remove the Trustpoint volume and delete all stored data, you can use ``./tp_wizard.sh nuke``. +.. code-block:: bash -Getting started with Docker Compose --------------------------------------- + cp .env.example .env -Step-by-Step Setup (Load from Dockerhub) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -1. **Download** `docker-compose.yml `_ +The following variables are supported: -2. **Pull and run the Trustpoint and Postgres Containers** +.. list-table:: + :widths: 30 10 60 + :header-rows: 1 - You can pull the images and start Trustpoint and Postgres containers with following command: + * - Variable + - Required + - Description + * - ``DATABASE_USER`` + - Yes + - PostgreSQL username. Avoid generic defaults such as ``admin``. + * - ``DATABASE_PASSWORD`` + - Yes + - PostgreSQL password. Generate a strong value: ``openssl rand -base64 32`` + * - ``POSTGRES_DB`` + - No + - Database name. Defaults to ``trustpoint_db``. + * - ``TP_URLS`` + - No + - Comma-separated hostnames or IPs at which Trustpoint is reachable (no protocol prefix). Defaults to ``localhost``. - .. code-block:: bash +Minimal ``.env`` example: - docker compose up -d - - - **-d**: Runs the container in detached mode. +.. code-block:: bash - .. note:: + DATABASE_USER=trustpoint + DATABASE_PASSWORD=correct-horse-battery-staple + TP_URLS=trustpoint.myfactory.local,10.0.0.5 - If the specified ports are already in use on your system, modify the port mapping in the `docker-compose.yml` file accordingly. +Setup (Load from Docker Hub) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Step-by-Step Setup (Build container) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +1. Download `docker-compose.yml `_ + and `.env.example `_. -1. **Clone the Trustpoint Repository** +2. Create the ``.env`` file as described above. - First, clone the Trustpoint source code from the official repository: +3. Start all containers: .. code-block:: bash - git clone https://github.com/Trustpoint-Project/trustpoint.git - cd trustpoint - - This command downloads the Trustpoint source code to your local machine and navigates into the project directory. + docker compose up -d .. note:: - The database connection between the containers uses default credentials for testing. THIS IS INSECURE. - It is highly encouraged to change the default credentials in the `docker-compose.yml` file before building the containers. -2. **Edit the .env file to specify allowed URLs** + If the specified ports are already in use, adjust the port mappings in ``docker-compose.yml``. - Open the `.env` file in the project root and set the `TP_URLS` variable to include the URLs you will use to access Trustpoint. For example: +Setup (Build from source) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. Clone the repository: .. code-block:: bash - TP_URLS=trustpoint.myfactory.local,localhost:8443 + git clone https://github.com/Trustpoint-Project/trustpoint.git + cd trustpoint -3. **Build the Trustpoint and Postgres Docker Images** +2. Create the ``.env`` file as described above. - Use docker compose to build the Trustpoint and Postgres images from the source: +3. Build the images: .. code-block:: bash docker compose build -4. **Run the Trustpoint and Postgres Containers** - - Start the Trustpoint and Postgres containers using the images you just built: +4. Start all containers: .. code-block:: bash docker compose up -d - - **-d**: Runs the container in detached mode. - .. note:: - If the specified ports are already in use on your system, modify the port mapping in the `docker-compose.yml` file accordingly. - + If the specified ports are already in use, adjust the port mappings in ``docker-compose.yml``. Getting Started with Docker ---------------------------- -Step-by-Step Setup (Load from Dockerhub) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Setup (Load from Docker Hub) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -1. **Pull the Trustpoint Docker Image** - - First, pull the Trustpoint and Postgres docker images from Docker Hub. This command will download the pre-built container images directly: +1. Pull the images: .. code-block:: bash - docker pull trustpointproject/trustpoint:latest - docker pull trustpointproject/postgres:latest - - These commands pull the latest versions of the Trustpoint and Postgres images. - -2. **Run the Trustpoint and Postgres Containers with a Custom Name and Port Mappings** + docker pull trustpointproject/trustpoint:latest + docker pull trustpointproject/postgres:latest - Once the images are downloaded, you can start containers with custom names and ports mappings: +2. Run the containers: .. code-block:: bash - docker run -d --name postgres -v "postgres_data":/var/lib/postgresql/data -p 5432:5432 trustpointproject/postgres:latest - docker run -d --name trustpoint -p 80:80 -p 443:443 trustpointproject/trustpoint:latest - - ``E.g.: docker run -d --name postgres-v2.0.0 -v "postgres-v2.0.0":/var/lib/postgresql/data -p 5432:5432 trustpointproject/postgres:latest`` - - - **-d**: Runs the container in detached mode. - - **--name trustpoint**: Names the Trustpoint container `trustpoint`. - - **--name postgres**: Names the Postgres container `postgres`. - - **-p 80:80**: Maps the Trustpoint container's HTTP port to your local machine's port 80. - - **-p 443:443**: Maps the Trustpoint container's HTTPs port to your local machine's port 443. - - **-p 5432:5432**: Maps the Postgres container's TCP port to your local machine's port 5432. - - **-v postgres_data:/var/lib/postgresql/data**: Creates a volume for Postgres to persist data. + docker run -d --name postgres- \ + -v postgres_data-:/var/lib/postgresql \ + -p 127.0.0.1:5432:5432 \ + -e POSTGRES_USER= \ + -e POSTGRES_PASSWORD= \ + -e POSTGRES_DB=trustpoint_db \ + trustpointproject/postgres:latest + + docker run -d --name trustpoint- \ + --link postgres- \ + -p 80:80 -p 443:443 \ + -e POSTGRES_DB=trustpoint_db \ + -e DATABASE_USER= \ + -e DATABASE_PASSWORD= \ + -e DATABASE_HOST=postgres- \ + -e DATABASE_PORT=5432 \ + trustpointproject/trustpoint:latest .. note:: - If the specified ports are already in use on your system, modify the port mapping in the command accordingly. - -Step-by-Step Setup (Build container) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + PostgreSQL 18+ stores data under ``/var/lib/postgresql/18/main``. + Mount the volume at ``/var/lib/postgresql``, not ``/var/lib/postgresql/data``. -1. **Clone the Trustpoint Repository** +Setup (Build from source) +^^^^^^^^^^^^^^^^^^^^^^^^^^ - First, clone the Trustpoint source code from the official repository: +1. Clone the repository and build the images: .. code-block:: bash git clone https://github.com/Trustpoint-Project/trustpoint.git cd trustpoint - - This command downloads the Trustpoint source code to your local machine and navigates into the project directory. - -2. **Build the Postgres and Trustpoint Docker Images** - - Use Docker to build the Postgres and Trustpoint images: - - .. code-block:: bash - docker build -t trustpointproject/postgres:latest -f docker/db/Dockerfile . docker build -t trustpointproject/trustpoint:latest -f docker/trustpoint/Dockerfile . - - **-t**: Tags the image with the name `trustpoint` / `postgres`. - - **-f**: specifies the filepath of the `dockerfile`.` - - **.**: Specifies the current directory as the build context. - -3. **Run the Trustpoint Container with a Custom Name and Port Mappings** - - Start the database and Trustpoint container using the images you just built, with custom names and both port mappings: - - .. code-block:: bash - - docker run -d --name postgres -p5432:5432 -v"postgres_data":/var/lib/postgresql/data -ePOSTGRES_USER=admin -ePOSTGRES_PASSWORD=testing321 -ePOSTGRES_DB=trustpoint_db trustpointproject/postgres:latest - docker run -d --name trustpoint --link postgres -p80:80 -p443:443 -ePOSTGRES_DB=trustpoint_db -eDATABASE_USER=admin -eDATABASE_PASSWORD=testing321 -eDATABASE_HOST="postgres" -eDATABASE_PORT=5432 trustpointproject/trustpoint:latest - - **E.g.:** +2. Run the containers (replace ```` and ```` with strong values): .. code-block:: bash - docker run -d --name postgres-v2.0.0 -p5432:5432 -vpostgres_data-v2.0.0:/var/lib/postgresql/data -ePOSTGRES_USER=admin -ePOSTGRES_PASSWORD=testing321 -ePOSTGRES_DB=trustpoint_db trustpointproject/postgres:latest - docker run -d --name trustpoint-v2.0.0 --link postgres-v2.0.0 -p80:80 -p443:443 -ePOSTGRES_DB=trustpoint_db -eDATABASE_USER=admin -eDATABASE_PASSWORD=testing321 -eDATABASE_HOST=postgres-v2.0.0 -eDATABASE_PORT=5432 trustpointproject/trustpoint:latest - - - **-d**: Runs the container in detached mode. - - **--name**: Names the Trustpoint container `trustpoint` / `postgres`. - - **-p**: Maps the container's port to your local machine's port. - - **-v**: Creates a volume to persist data. - - **-e**: Sets environment variables. - - -Verify the Setup 🔍 -------------------- - -Once the containers are running, you can verify the setup: - -- **Web Interface**: Open `http://localhost` in your browser to access the Trustpoint setup wizard. -- **TLS Connection**: As the first step of the wizard, a TLS server certificate is generated. After this, only HTTPs connections will be accepted. - -.. note:: - You may need to accept a self-signed certificate in your browser to proceed. - -- **Set Credentials**: Be sure to choose a strong password for the admin user during the setup wizard. - -.. admonition:: 🥳 CONGRATULATIONS! - :class: tip + docker run -d --name postgres- \ + -v postgres_data-:/var/lib/postgresql \ + -p 127.0.0.1:5432:5432 \ + -e POSTGRES_USER= \ + -e POSTGRES_PASSWORD= \ + -e POSTGRES_DB=trustpoint_db \ + trustpointproject/postgres:latest + + docker run -d --name trustpoint- \ + --link postgres- \ + -p 80:80 -p 443:443 \ + -e POSTGRES_DB=trustpoint_db \ + -e DATABASE_USER= \ + -e DATABASE_PASSWORD= \ + -e DATABASE_HOST=postgres- \ + -e DATABASE_PORT=5432 \ + trustpointproject/trustpoint:latest + +Verify the Setup +---------------- - You’ve successfully set up Trustpoint! Your environment is now ready to securely manage digital identities for your industrial devices. You can start registering devices, issuing certificates, and building a trusted network. +Once the containers are running: -Change the Current Admin User Password ---------------------------------------- +- Open ``http://localhost`` in your browser to access the Trustpoint setup wizard. +- The wizard generates a TLS certificate on first run. After that, only HTTPS connections are accepted. You may need to accept a self-signed certificate in your browser. +- Set a strong password for the admin user when prompted. -To secure your Trustpoint setup, it may be important to change the default admin user password: +Change the Admin User Password +------------------------------- -- Go to https://localhost/admin -- Click on the **Users** section in the Django admin dashboard. -- Select the **admin** user from the list. -- Scroll down to the **password field** and click the "change password" link. -- Enter and confirm the new password. -- Click **Save** to update the password. +- Go to ``https://localhost/admin``. +- Click **Users**, select the **admin** user. +- Click the "change password" link, enter a new password, and click **Save**. -Tips and Troubleshooting +Tips and Troubleshooting ------------------------- -- **View Logs**: For troubleshooting, view logs with: +**View logs:** - .. code-block:: bash +.. code-block:: bash - docker logs -f trustpoint - docker logs -f postgres - docker compose logs trustpoint -f - docker compose logs postgres -f + docker compose logs trustpoint -f + docker compose logs postgres -f -- **Stop and Remove the Container**: Stop and remove the container with: +**Stop and remove containers and volumes:** - .. code-block:: bash - - docker stop trustpoint-container postgres && docker rm trustpoint-container postgres - docker compose down -v - - - - **-v**: Removes the volume. +.. code-block:: bash + docker compose down -v What to Do Next ---------------- -After setting up and Trustpoint, here are some recommended next steps to explore the full capabilities of the platform: - -1. **Explore Trustpoint with test data** 🧪: - Familiarize yourself with Trustpoint’s functionalities by running it with sample test data. To populate test data, navigate to **Home > Notifications > Populate Test Data** in the Trustpoint interface. +1. **Explore with test data**: Navigate to **Home > Notifications > Populate Test Data** in the Trustpoint interface. -2. **Use the Trustpoint in conjunction with the Trustpoint Client** 💻: - The easiest way to fully utilize Trustpoint is by pairing it with the associated Trustpoint Client, which is installed on end devices. The client enables streamlined identity management and certificate issuance. For more details, visit the `Trustpoint-Client Documentation `_. +2. **Use the Trustpoint Client**: Install the `Trustpoint Client `_ on end devices for streamlined certificate issuance. -3. **Issue your first certificate for an end device** 🛡️: - To do this, you need an Issuing CA certificate, a domain and a device that you must define in Trustpoint. Therefore follow the steps described in :ref:`quickstart-operation-guide` +3. **Issue your first certificate**: Follow the steps in :ref:`quickstart-operation-guide`. From e6b24f307b18da72136d6dc36a4a94d92cd1ec85 Mon Sep 17 00:00:00 2001 From: florianhandke Date: Mon, 15 Jun 2026 11:19:38 +0200 Subject: [PATCH 03/18] Optimize queryset performance --- trustpoint/pki/views/certificates.py | 34 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/trustpoint/pki/views/certificates.py b/trustpoint/pki/views/certificates.py index 3ddf13879..eefcc4bf8 100644 --- a/trustpoint/pki/views/certificates.py +++ b/trustpoint/pki/views/certificates.py @@ -84,14 +84,18 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: def get_base_queryset(self) -> QuerySet[CertificateModel]: """Return the annotated base queryset used by the certificates table.""" now = timezone.now() - return CertificateModel.objects.annotate( - certificate_status_sort=Case( - When(revoked_certificate__isnull=False, then=Value(3)), - When(not_valid_before__gt=now, then=Value(4)), - When(not_valid_after__lte=now, then=Value(2)), - default=Value(0), - output_field=IntegerField(), - ), + return ( + CertificateModel.objects + .select_related('revoked_certificate') + .annotate( + certificate_status_sort=Case( + When(revoked_certificate__isnull=False, then=Value(3)), + When(not_valid_before__gt=now, then=Value(4)), + When(not_valid_after__lte=now, then=Value(2)), + default=Value(0), + output_field=IntegerField(), + ), + ) ) def get_queryset(self) -> QuerySet[CertificateModel]: @@ -138,6 +142,20 @@ class CertificateDetailView(CertificatesContextMixin, DetailView[CertificateMode template_name = 'pki/certificates/details.html' context_object_name = 'cert' + def get_queryset(self) -> QuerySet[CertificateModel]: + """Return the queryset with relations eagerly loaded for the detail template.""" + return ( + CertificateModel.objects + .select_related( + 'subject_alternative_name_extension__subject_alt_name', + ) + .prefetch_related( + 'subject', + 'issuer', + 'subject_alternative_name_extension__subject_alt_name__ip_addresses', + ) + ) + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: """Adding map of attribute and its oid with its values. From 8ef42919eadf11114275ce243d5064e065dbbd00 Mon Sep 17 00:00:00 2001 From: florianhandke Date: Mon, 15 Jun 2026 13:28:34 +0200 Subject: [PATCH 04/18] Update .env file template --- .env | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/.env b/.env index 5bee86dad..d91bbad16 100644 --- a/.env +++ b/.env @@ -1,21 +1,37 @@ -# .env — local configuration for docker compose -# This file is listed in .gitignore and must never be committed. -# See .env.example for a documented template. +# ── Copy this file to .env and fill in the values before running docker compose ── +# +# Usage: +# cp .env.example .env +# $EDITOR .env +# docker compose up -d +# +# The .env file must NEVER be committed to version control. +# It is already listed in .gitignore. -# ── Required ───────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- +# Required – docker compose will refuse to start if these are left empty. +# --------------------------------------------------------------------------- -# PostgreSQL credentials used by trustpoint, trustpoint-worker, and postgres. -# docker compose will refuse to start if either value is empty. -DATABASE_USER=admin -DATABASE_PASSWORD=testing321 +# Database username. +# Avoid generic defaults such as 'admin' or 'postgres' in production. +DATABASE_USER=trustpoint -# ── Optional ───────────────────────────────────────────────────────────────── +# Database password. +# Use a long, randomly generated value in production. +# Generate one with: openssl rand -base64 32 +DATABASE_PASSWORD=ZD4202FeSinavtDFX9ynPQcisPQqMcB -# PostgreSQL database name (default: trustpoint_db) +# --------------------------------------------------------------------------- +# Optional overrides (defaults shown as comments) +# --------------------------------------------------------------------------- + +# PostgreSQL database name. +# Default: trustpoint_db # POSTGRES_DB=trustpoint_db -# Hostnames / IPs at which Trustpoint is reachable, comma-separated. -# Do not include the protocol (http:// or https://). -# Only include a port if it differs from the default (80/443). -# Example: TP_URLS=10.10.0.2,mytrustpoint.local,trustpoint.example.com -TP_URLS=trustpoint.local, +# Hostnames / IP addresses at which Trustpoint is reachable, comma-separated. +# Used for TLS certificate SAN generation and Django ALLOWED_HOSTS. +# Do NOT include the protocol (http:// or https://). +# Only include a port if it differs from the default (80 for HTTP, 443 for HTTPS). +# Default: localhost +# TP_URLS=trustpoint.local,10.0.0.1,trustpoint.example.com From 092979ed5db51c7e5ae117e8455e228b4359658c Mon Sep 17 00:00:00 2001 From: florianhandke Date: Mon, 15 Jun 2026 13:56:30 +0200 Subject: [PATCH 05/18] Fix input validation in ask_yes_no function and improve array expansion in start_app --- tp_wizard.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tp_wizard.sh b/tp_wizard.sh index da9c5bd71..e921e2900 100755 --- a/tp_wizard.sh +++ b/tp_wizard.sh @@ -113,7 +113,7 @@ sftpgo_web_port(){ # -------------------------- Input helpers ------------------------------------ ask(){ local prompt="$1" def="${2:-}"; if [[ -n "$def" ]]; then read -r -p "$(bold)${prompt}$(rst) [default: ${def}] > " REPLY || true; REPLY="${REPLY:-$def}"; else read -r -p "$(bold)${prompt}$(rst) > " REPLY || true; fi; } -ask_yes_no(){ local prompt="$1" def="${2:-y}" a; case "${def}" in y|yes) a="[Y/n]";; n|no) a="[y/N]";; *) a="[y/n]";; esac; read -r -p "$(bold)${prompt} ${a}$(rst) > " resp || true; resp="${resp:-$def}"; [[ "${resp}" =~ ^y ]]; } +ask_yes_no(){ local prompt="$1" def="${2:-y}" a; case "${def}" in y|yes) a="[Y/n]";; n|no) a="[y/N]";; *) a="[y/n]";; esac; read -r -p "$(bold)${prompt} ${a}$(rst) > " resp || true; resp="${resp:-$def}"; [[ "${resp}" =~ ^[yY] ]]; } ask_port(){ local prompt="$1" def="$2" p; while true; do ask "$prompt" "$def"; p="$REPLY"; [[ "$p" =~ ^[0-9]{1,5}$ ]] && (( p>0 && p<65536 )) && { echo "$p"; return; } ; warn "Invalid port. Enter 1..65535."; done; } ask_free_port(){ local prompt="$1" def="$2" p; while true; do p="$(ask_port "$prompt" "$def")"; if port_in_use "$p"; then warn "Port ${p} is already in use on this host. Pick another."; else echo "$p"; return; fi; done; } ask_user(){ local prompt="$1" def="$2" u; while true; do ask "$prompt" "$def"; u="$REPLY"; [[ "$u" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*$ ]] && { echo "$u"; return; } ; warn "Invalid username."; done; } @@ -481,7 +481,7 @@ start_app(){ -e "DATABASE_PASSWORD=$APP_DB_PASS" \ -e "DATABASE_HOST=$APP_DB_HOST" \ -e "DATABASE_PORT=$APP_DB_PORT" \ - "${smtp_env[@]}" \ + ${smtp_env[@]+"${smtp_env[@]}"} \ "$APP_IMAGE" >/dev/null } From be50559ceda8cf0b2d0e20dbb8024703d39031d3 Mon Sep 17 00:00:00 2001 From: BytesWelder Date: Mon, 15 Jun 2026 23:15:49 +0200 Subject: [PATCH 06/18] draft ideas from comment --- .env | 54 ++++------- .env.example | 51 +++++----- docker-compose.softhsm.yml | 14 ++- docker-compose.yml | 34 +++---- .../getting_started/quickstart_setup.rst | 32 +++++-- docs/source/security/pkcs11.rst | 4 +- tp_wizard.sh | 93 +++++++++++++++++-- trustpoint/trustpoint/settings.py | 92 ++++++++++-------- 8 files changed, 231 insertions(+), 143 deletions(-) diff --git a/.env b/.env index d91bbad16..23c172277 100644 --- a/.env +++ b/.env @@ -1,37 +1,23 @@ -# ── Copy this file to .env and fill in the values before running docker compose ── -# -# Usage: -# cp .env.example .env -# $EDITOR .env -# docker compose up -d -# -# The .env file must NEVER be committed to version control. -# It is already listed in .gitignore. +# Development defaults used by docker compose and tp_wizard.sh. +# Override these values locally for production-like deployments. -# --------------------------------------------------------------------------- -# Required – docker compose will refuse to start if these are left empty. -# --------------------------------------------------------------------------- - -# Database username. -# Avoid generic defaults such as 'admin' or 'postgres' in production. -DATABASE_USER=trustpoint - -# Database password. -# Use a long, randomly generated value in production. -# Generate one with: openssl rand -base64 32 -DATABASE_PASSWORD=ZD4202FeSinavtDFX9ynPQcisPQqMcB - -# --------------------------------------------------------------------------- -# Optional overrides (defaults shown as comments) -# --------------------------------------------------------------------------- - -# PostgreSQL database name. -# Default: trustpoint_db -# POSTGRES_DB=trustpoint_db +# PostgreSQL connection used by the Trustpoint containers. +POSTGRES_DB=trustpoint_db +DATABASE_USER=admin +DATABASE_PASSWORD=testing321 +DATABASE_HOST=postgres +DATABASE_PORT=5432 # Hostnames / IP addresses at which Trustpoint is reachable, comma-separated. -# Used for TLS certificate SAN generation and Django ALLOWED_HOSTS. -# Do NOT include the protocol (http:// or https://). -# Only include a port if it differs from the default (80 for HTTP, 443 for HTTPS). -# Default: localhost -# TP_URLS=trustpoint.local,10.0.0.1,trustpoint.example.com +# Do not include the protocol. Include a port only when it differs from 80/443. +TP_URLS=trustpoint.local + +# Optional mail settings. Leave EMAIL_HOST empty to use Django's console backend. +DEFAULT_FROM_EMAIL=no-reply@trustpoint.de +EMAIL_HOST= +EMAIL_PORT=587 +EMAIL_USE_TLS= +EMAIL_USE_SSL= +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +EMAIL_TIMEOUT=10 diff --git a/.env.example b/.env.example index ea9a4ad6d..0aef1e4e4 100644 --- a/.env.example +++ b/.env.example @@ -1,37 +1,30 @@ -# ── Copy this file to .env and fill in the values before running docker compose ── +# Copy this file to .env and adjust it before running Docker Compose. # # Usage: # cp .env.example .env # $EDITOR .env # docker compose up -d -# -# The .env file must NEVER be committed to version control. -# It is already listed in .gitignore. - -# --------------------------------------------------------------------------- -# Required – docker compose will refuse to start if these are left empty. -# --------------------------------------------------------------------------- - -# Database username. -# Avoid generic defaults such as 'admin' or 'postgres' in production. -DATABASE_USER= -# Database password. -# Use a long, randomly generated value in production. -# Generate one with: openssl rand -base64 32 -DATABASE_PASSWORD= - -# --------------------------------------------------------------------------- -# Optional overrides (defaults shown as comments) -# --------------------------------------------------------------------------- - -# PostgreSQL database name. -# Default: trustpoint_db -# POSTGRES_DB=trustpoint_db +# PostgreSQL connection used by the Trustpoint containers. +# Development default: admin / testing321 +# Use a long, randomly generated password outside local testing: +# openssl rand -base64 32 +POSTGRES_DB=trustpoint_db +DATABASE_USER=admin +DATABASE_PASSWORD=testing321 +DATABASE_HOST=postgres +DATABASE_PORT=5432 # Hostnames / IP addresses at which Trustpoint is reachable, comma-separated. -# Used for TLS certificate SAN generation and Django ALLOWED_HOSTS. -# Do NOT include the protocol (http:// or https://). -# Only include a port if it differs from the default (80 for HTTP, 443 for HTTPS). -# Default: localhost -# TP_URLS=trustpoint.local,10.0.0.1,trustpoint.example.com +# Do not include the protocol. Include a port only when it differs from 80/443. +TP_URLS=trustpoint.local + +# Optional mail settings. Leave EMAIL_HOST empty to use Django's console backend. +DEFAULT_FROM_EMAIL=no-reply@trustpoint.de +EMAIL_HOST= +EMAIL_PORT=587 +EMAIL_USE_TLS= +EMAIL_USE_SSL= +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +EMAIL_TIMEOUT=10 diff --git a/docker-compose.softhsm.yml b/docker-compose.softhsm.yml index b159f3be1..92ba5192a 100644 --- a/docker-compose.softhsm.yml +++ b/docker-compose.softhsm.yml @@ -10,18 +10,16 @@ services: - "80:80" - "443:443" depends_on: - postgres: - condition: service_healthy softhsm: condition: service_healthy volumes: - ./trustpoint/settings:/var/www/html/trustpoint/trustpoint/settings environment: - POSTGRES_DB: "trustpoint_db" - POSTGRES_USER_FILE: /run/secrets/db_user - POSTGRES_PASSWORD_FILE: /run/secrets/db_password - DATABASE_HOST: "postgres" - DATABASE_PORT: "5432" + POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" + DATABASE_USER_FILE: /run/secrets/db_user + DATABASE_PASSWORD_FILE: /run/secrets/db_password + DATABASE_HOST: "${DATABASE_HOST:-postgres}" + DATABASE_PORT: "${DATABASE_PORT:-5432}" HSM_PIN_FILE: /run/secrets/hsm_pin HSM_SO_PIN_FILE: /run/secrets/hsm_so_pin secrets: @@ -51,7 +49,7 @@ services: environment: POSTGRES_USER_FILE: /run/secrets/db_user POSTGRES_PASSWORD_FILE: /run/secrets/db_password - POSTGRES_DB: "trustpoint_db" + POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" secrets: - db_user - db_password diff --git a/docker-compose.yml b/docker-compose.yml index 005d242a6..c708bcf89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,16 +9,15 @@ services: ports: - "80:80" - "443:443" - depends_on: - postgres: - condition: service_healthy + env_file: + - .env environment: POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" - DATABASE_USER: "${DATABASE_USER:?DATABASE_USER env var must be set}" - DATABASE_PASSWORD: "${DATABASE_PASSWORD:?DATABASE_PASSWORD env var must be set}" - DATABASE_HOST: "postgres" - DATABASE_PORT: "5432" - TP_URLS: ${TP_URLS} + DATABASE_USER: "${DATABASE_USER:-admin}" + DATABASE_PASSWORD: "${DATABASE_PASSWORD:-testing321}" + DATABASE_HOST: "${DATABASE_HOST:-postgres}" + DATABASE_PORT: "${DATABASE_PORT:-5432}" + TP_URLS: "${TP_URLS:-trustpoint.local}" healthcheck: test: ["CMD-SHELL", "curl -fsk --max-time 5 https://localhost/ > /dev/null"] interval: 30s @@ -33,16 +32,17 @@ services: image: trustpointproject/trustpoint:latest restart: on-failure depends_on: - postgres: - condition: service_healthy trustpoint: condition: service_healthy + env_file: + - .env environment: POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" - DATABASE_USER: "${DATABASE_USER:?DATABASE_USER env var must be set}" - DATABASE_PASSWORD: "${DATABASE_PASSWORD:?DATABASE_PASSWORD env var must be set}" - DATABASE_HOST: "postgres" - DATABASE_PORT: "5432" + DATABASE_USER: "${DATABASE_USER:-admin}" + DATABASE_PASSWORD: "${DATABASE_PASSWORD:-testing321}" + DATABASE_HOST: "${DATABASE_HOST:-postgres}" + DATABASE_PORT: "${DATABASE_PORT:-5432}" + TP_URLS: "${TP_URLS:-trustpoint.local}" TRUSTPOINT_SERVICE_ROLE: "worker" postgres: @@ -58,8 +58,8 @@ services: volumes: - postgres_data:/var/lib/postgresql environment: - POSTGRES_USER: "${DATABASE_USER:?DATABASE_USER env var must be set}" - POSTGRES_PASSWORD: "${DATABASE_PASSWORD:?DATABASE_PASSWORD env var must be set}" + POSTGRES_USER: "${DATABASE_USER:-admin}" + POSTGRES_PASSWORD: "${DATABASE_PASSWORD:-testing321}" POSTGRES_DB: "${POSTGRES_DB:-trustpoint_db}" healthcheck: test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432"] @@ -69,4 +69,4 @@ services: start_period: 10s volumes: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/docs/source/getting_started/quickstart_setup.rst b/docs/source/getting_started/quickstart_setup.rst index 18f650207..a918b8b8b 100644 --- a/docs/source/getting_started/quickstart_setup.rst +++ b/docs/source/getting_started/quickstart_setup.rst @@ -37,8 +37,9 @@ Getting started with Docker Compose The .env file ^^^^^^^^^^^^^ -All deployments require a ``.env`` file in the project root. -Copy the provided template and fill in the required values: +Docker Compose and ``tp_wizard.sh`` read the ``.env`` file in the project root. +The repository contains development defaults. For production-like deployments, +copy the example and replace at least the database password: .. code-block:: bash @@ -54,24 +55,39 @@ The following variables are supported: - Required - Description * - ``DATABASE_USER`` - - Yes - - PostgreSQL username. Avoid generic defaults such as ``admin``. + - No + - PostgreSQL username. Defaults to ``admin`` for local testing. * - ``DATABASE_PASSWORD`` - - Yes - - PostgreSQL password. Generate a strong value: ``openssl rand -base64 32`` + - No + - PostgreSQL password. Defaults to ``testing321`` for local testing. Generate a strong value for production-like deployments: ``openssl rand -base64 32`` * - ``POSTGRES_DB`` - No - Database name. Defaults to ``trustpoint_db``. + * - ``DATABASE_HOST`` + - No + - Database host used by the Trustpoint containers. Defaults to ``postgres``. + * - ``DATABASE_PORT`` + - No + - Database port used by the Trustpoint containers. Defaults to ``5432``. * - ``TP_URLS`` - No - - Comma-separated hostnames or IPs at which Trustpoint is reachable (no protocol prefix). Defaults to ``localhost``. + - Comma-separated hostnames or IPs at which Trustpoint is reachable (no protocol prefix). Defaults to ``trustpoint.local``. + * - ``DEFAULT_FROM_EMAIL`` + - No + - Sender address for Trustpoint emails. Defaults to ``no-reply@trustpoint.de``. + * - ``EMAIL_HOST`` + - No + - SMTP host. Leave empty to use Django's console backend. Minimal ``.env`` example: .. code-block:: bash - DATABASE_USER=trustpoint + POSTGRES_DB=trustpoint_db + DATABASE_USER=admin DATABASE_PASSWORD=correct-horse-battery-staple + DATABASE_HOST=postgres + DATABASE_PORT=5432 TP_URLS=trustpoint.myfactory.local,10.0.0.5 Setup (Load from Docker Hub) diff --git a/docs/source/security/pkcs11.rst b/docs/source/security/pkcs11.rst index 67f93ae9f..8dfba7a76 100644 --- a/docs/source/security/pkcs11.rst +++ b/docs/source/security/pkcs11.rst @@ -240,7 +240,7 @@ The Trustpoint container includes pre-configured SoftHSM support with token dire file: hsm_so_pin.txt environment: - POSTGRES_USER_FILE: /run/secrets/db_user - POSTGRES_PASSWORD_FILE: /run/secrets/db_password + DATABASE_USER_FILE: /run/secrets/db_user + DATABASE_PASSWORD_FILE: /run/secrets/db_password HSM_PIN_FILE: /run/secrets/hsm_pin HSM_SO_PIN_FILE: /run/secrets/hsm_so_pin diff --git a/tp_wizard.sh b/tp_wizard.sh index e921e2900..84525b2d9 100755 --- a/tp_wizard.sh +++ b/tp_wizard.sh @@ -6,6 +6,14 @@ set -euo pipefail PROJECT="trustpoint" NET="${PROJECT}-net" VOL_DB="${PROJECT}_postgres_data" +ENV_FILE="${ENV_FILE:-${PWD}/.env}" + +if [[ -f "$ENV_FILE" ]]; then + set -a + # shellcheck source=/dev/null + source "$ENV_FILE" + set +a +fi # trustpoint image handling TP_DOCKERFILE="docker/trustpoint/Dockerfile" @@ -24,11 +32,13 @@ APP_HTTP_HOST=80 APP_HTTPS_HOST=443 # PostgreSQL defaults -DEF_DB_NAME="trustpoint_db" -DEF_DB_USER="admin" -DEF_DB_PASS="testing321" -DEF_DB_PORT=5432 +DEF_DB_NAME="${POSTGRES_DB:-trustpoint_db}" +DEF_DB_USER="${DATABASE_USER:-admin}" +DEF_DB_PASS="${DATABASE_PASSWORD:-testing321}" +DEF_DB_PORT="${DATABASE_PORT:-5432}" +DEF_DB_HOST="${DATABASE_HOST:-postgres}" DEF_DB_HOST_INTERNAL="postgres" # container name/hostname +DEF_TP_URLS="${TP_URLS:-trustpoint.local}" # Mailpit defaults DEF_MAILPIT_SMTP_PORT=1025 @@ -121,11 +131,62 @@ ask_dbname(){ local prompt="$1" def="$2" d; while true; do ask "$prompt" "$def"; ask_password(){ local prompt="$1" def="$2" pw; while true; do ask "$prompt" "$def"; pw="$REPLY"; (( ${#pw} >= 6 )) && { echo "$pw"; return; } ; warn "Password too short (min 6)."; done; } mask(){ local s="$1" n=${#1}; (( n<=2 )) && { printf '%s' '**'; return; }; printf '%*s' $((n-2)) '' | tr ' ' '*'; printf '%s' "${s: -2}"; } +# -------------------------- .env helpers ------------------------------------- +upsert_env_var(){ + local key="$1" value="$2" tmp + touch "$ENV_FILE" + tmp="$(mktemp)" + awk -v key="$key" -v value="$value" ' + BEGIN { done = 0 } + $0 ~ "^[[:space:]]*" key "=" { + if (!done) { + print key "=" value + done = 1 + } + next + } + { print } + END { + if (!done) { + print key "=" value + } + } + ' "$ENV_FILE" > "$tmp" + mv "$tmp" "$ENV_FILE" +} + +sync_env_file(){ + local env_db_host env_db_port env_db_name env_db_user env_db_pass + + if $EN_APP || $EN_WF2_WORKER || $ONLY_APP || $ONLY_WF2_WORKER; then + env_db_host="$APP_DB_HOST" + env_db_port="$APP_DB_PORT" + env_db_name="$APP_DB_NAME" + env_db_user="$APP_DB_USER" + env_db_pass="$APP_DB_PASS" + else + env_db_host="$DB_HOST" + env_db_port="$DB_PORT" + env_db_name="$DB_NAME" + env_db_user="$DB_USER" + env_db_pass="$DB_PASS" + fi + + upsert_env_var "POSTGRES_DB" "$env_db_name" + upsert_env_var "DATABASE_USER" "$env_db_user" + upsert_env_var "DATABASE_PASSWORD" "$env_db_pass" + upsert_env_var "DATABASE_HOST" "$env_db_host" + upsert_env_var "DATABASE_PORT" "$env_db_port" + upsert_env_var "TP_URLS" "$TP_URLS_VALUE" + chmod 600 "$ENV_FILE" 2>/dev/null || true + ok "Updated ${ENV_FILE}" +} + # -------------------------- Wizard state ------------------------------------- EN_APP=false; EN_PG=false; EN_MAILPIT=false; EN_SFTPGO=false; EN_WF2_WORKER=false DB_INTERNAL=true -DB_HOST="$DEF_DB_HOST_INTERNAL" # default host when internal +DB_HOST="$DEF_DB_HOST" DB_PORT="$DEF_DB_PORT" # host-mapped port for convenience access DB_NAME="$DEF_DB_NAME" DB_USER="$DEF_DB_USER" @@ -136,6 +197,7 @@ APP_DB_PORT="$DB_PORT" APP_DB_NAME="$DB_NAME" APP_DB_USER="$DB_USER" APP_DB_PASS="$DEF_DB_PASS" +TP_URLS_VALUE="$DEF_TP_URLS" MAILPIT_SMTP_PORT="$DEF_MAILPIT_SMTP_PORT" MAILPIT_UI_PORT="$DEF_MAILPIT_UI_PORT" @@ -186,7 +248,7 @@ step_postgres_config(){ # Immediate check: host port must be free to publish DB_PORT="$(ask_free_port 'PostgreSQL host port (mapped)' "$DB_PORT")" else - DB_HOST="$(ask 'External DB host/IP' '127.0.0.1'; echo "$REPLY")" + DB_HOST="$(ask 'External DB host/IP' "$DB_HOST"; echo "$REPLY")" DB_PORT="$(ask_port 'External DB port' "$DB_PORT")" DB_NAME="$(ask_dbname 'External DB database name' "$DB_NAME")" DB_USER="$(ask_user 'External DB username' "$DB_USER")" @@ -224,6 +286,12 @@ step_app_db_binding(){ fi } +step_trustpoint_urls(){ + $EN_APP || return 0 + ask "Trustpoint reachable hostnames/IPs (comma-separated, no protocol)" "$TP_URLS_VALUE" + TP_URLS_VALUE="$REPLY" +} + step_helpers(){ EN_MAILPIT=$(ask_yes_no "Enable Mailpit (demo SMTP inbox)?" "n" && echo true || echo false) if $EN_MAILPIT; then @@ -258,6 +326,7 @@ show_plan(){ echo "==================== Configuration Summary (Planned) ====================" printf "%-22s %s\n" "Network:" "$NET" printf "%-22s %s\n" "DB Volume:" "$VOL_DB" + printf "%-22s %s\n" ".env file:" "$ENV_FILE" echo printf "%-22s %s\n" "trustpoint enabled:" "$EN_APP" if $EN_APP; then @@ -282,6 +351,7 @@ show_plan(){ printf "%-22s %s\n" "trustpoint DB name:" "$APP_DB_NAME" printf "%-22s %s\n" "trustpoint DB user:" "$APP_DB_USER" printf "%-22s %s\n" "trustpoint DB pass:" "$(mask "$APP_DB_PASS")" + printf "%-22s %s\n" "trustpoint URLs:" "$TP_URLS_VALUE" fi echo printf "%-22s %s\n" "Mailpit enabled:" "$EN_MAILPIT" @@ -441,6 +511,7 @@ DATABASE_USER=${APP_DB_USER} DATABASE_PASSWORD=${APP_DB_PASS} DATABASE_HOST=${APP_DB_HOST} DATABASE_PORT=${APP_DB_PORT} +TP_URLS=${TP_URLS_VALUE} TRUSTPOINT_SERVICE_ROLE=worker WORKFLOWS2_WORKER_ID=${WF2_WORKER_NAME} WORKFLOWS2_WORKER_LEASE=${WF2_WORKER_LEASE} @@ -481,6 +552,7 @@ start_app(){ -e "DATABASE_PASSWORD=$APP_DB_PASS" \ -e "DATABASE_HOST=$APP_DB_HOST" \ -e "DATABASE_PORT=$APP_DB_PORT" \ + -e "TP_URLS=$TP_URLS_VALUE" \ ${smtp_env[@]+"${smtp_env[@]}"} \ "$APP_IMAGE" >/dev/null } @@ -719,7 +791,7 @@ show_runtime_status(){ echo if exists trustpoint; then - local http_port https_port db_host db_port db_name db_user db_pass + local http_port https_port db_host db_port db_name db_user db_pass tp_urls http_port="$(container_host_port trustpoint 80/tcp)" https_port="$(container_host_port trustpoint 443/tcp)" db_host="$(container_env trustpoint DATABASE_HOST)" @@ -727,9 +799,11 @@ show_runtime_status(){ db_name="$(container_env trustpoint POSTGRES_DB)" db_user="$(container_env trustpoint DATABASE_USER)" db_pass="$(container_env trustpoint DATABASE_PASSWORD)" + tp_urls="$(container_env trustpoint TP_URLS)" [[ -n "$http_port" ]] && printf "%-22s %s\n" "trustpoint HTTP:" "http://localhost:${http_port}" [[ -n "$https_port" ]] && printf "%-22s %s\n" "trustpoint HTTPS:" "https://localhost:${https_port}" + [[ -n "$tp_urls" ]] && printf "%-22s %s\n" "trustpoint URLs:" "${tp_urls}" printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings" if [[ -n "$db_host" || -n "$db_port" || -n "$db_name" || -n "$db_user" ]]; then printf "%-22s %s\n" "DB connect:" "host=${db_host:-?} port=${db_port:-?} db=${db_name:-?} user=${db_user:-?} pass=$(mask "${db_pass:-}")" @@ -781,10 +855,12 @@ final_summary(){ echo echo "========================= Runtime Summary (Actual) =======================" printf "%-22s %s\n" "Network:" "$NET" + printf "%-22s %s\n" ".env file:" "$ENV_FILE" printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker)$' || true)" echo if $EN_APP; then printf "%-22s %s\n" "trustpoint:" "http://localhost:80 | https://localhost:443" + printf "%-22s %s\n" "trustpoint URLs:" "$TP_URLS_VALUE" printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings (default: auto)" fi if $DB_INTERNAL; then @@ -831,10 +907,12 @@ wizard(){ step_enable_postgres step_postgres_config step_app_db_binding + step_trustpoint_urls step_helpers step_workflows2_worker show_plan ask_yes_no "Proceed with these settings?" "y" || { warn "Aborted by user."; exit 1; } + sync_env_file resolve_app_image $DB_INTERNAL && ensure_volumes start_postgres @@ -894,6 +972,7 @@ set_targets_from_args(){ start_selected(){ configure_selected ensure_network + sync_env_file resolve_app_image $ONLY_DB && { EN_PG=true; ensure_volumes; start_postgres; } $ONLY_MAIL && { EN_MAILPIT=true; start_mailpit; } diff --git a/trustpoint/trustpoint/settings.py b/trustpoint/trustpoint/settings.py index dc30f40a1..0e9ce3765 100644 --- a/trustpoint/trustpoint/settings.py +++ b/trustpoint/trustpoint/settings.py @@ -92,6 +92,31 @@ def app_version(_request: Any) -> dict[str, str]: # ------------- Functions -------------- +def _env_bool(name: str, *, default: bool) -> bool: + """Return a boolean setting from an environment variable.""" + raw_value = os.getenv(name) + if raw_value is None or raw_value.strip() == '': + return default + return raw_value.strip().lower() in {'1', 'true', 'yes', 'on'} + + +def _env_value(name: str, default: str, *, file_var: str | None = None) -> str: + """Return a setting from an environment variable or Docker secret file.""" + value = os.getenv(name) + if value is not None: + return value + + if file_var: + file_path = os.getenv(file_var) + if file_path is not None: + try: + return Path(file_path).read_text().strip() + except OSError: + return default + + return default + + def is_postgre_available() -> bool: """Checks whether PostgreSQL is available and issues differentiated error messages. @@ -105,11 +130,11 @@ def is_postgre_available() -> bool: print('PostgreSQL is disabled. Set POSTGRESQL=True in settings.') return False - host = os.environ.get('DATABASE_HOST', DATABASE_HOST) - port = int(os.environ.get('DATABASE_PORT', DATABASE_PORT)) - user = os.environ.get('DATABASE_USER', DATABASE_USER) - password = os.environ.get('DATABASE_PASSWORD', DATABASE_PASSWORD) - db_name = os.environ.get('POSTGRES_DB', POSTGRES_DB) + host = DATABASE_HOST + port = int(DATABASE_PORT) + user = DATABASE_USER + password = DATABASE_PASSWORD + db_name = POSTGRES_DB try: print(f'Trying to connect to {host}:{port}...') @@ -163,7 +188,8 @@ def is_postgre_available() -> bool: ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]'] CSRF_TRUSTED_ORIGINS = ['http://localhost:8000', 'http://127.0.0.1:8000'] -raw_urls = os.getenv('TP_URLS', '') +TP_URLS = _env_value('TP_URLS', '') +raw_urls = TP_URLS if raw_urls: # Split by comma and clean up whitespace @@ -191,48 +217,38 @@ def is_postgre_available() -> bool: CSRF_TRUSTED_ORIGINS.append(exact_origin) -# Basic SMTP backend -if DEBUG: - EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -else: - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', default='no-reply@trustpoint.ai') - - -# Settings for postgreql database -POSTGRESQL = True -DATABASE_ENGINE = 'django.db.backends.postgresql' -DATABASE_HOST = 'localhost' -DATABASE_PORT = '5432' -POSTGRES_DB = 'trustpoint_db' -DATABASE_USER = 'admin' -DATABASE_PASSWORD = 'testing321' # noqa: S105 +# Settings for PostgreSQL database +POSTGRESQL = _env_bool('POSTGRESQL', default=True) +DATABASE_ENGINE = _env_value('DATABASE_ENGINE', 'django.db.backends.postgresql') +DATABASE_HOST = _env_value('DATABASE_HOST', 'localhost') +DATABASE_PORT = _env_value('DATABASE_PORT', '5432') +POSTGRES_DB = _env_value('POSTGRES_DB', 'trustpoint_db') +DATABASE_USER = _env_value('DATABASE_USER', 'admin', file_var='DATABASE_USER_FILE') +DATABASE_PASSWORD = _env_value('DATABASE_PASSWORD', 'testing321', file_var='DATABASE_PASSWORD_FILE') -# Settomg for email backend -DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'no-reply@trustpoint.de') +# Setting for email backend +DEFAULT_FROM_EMAIL = _env_value('DEFAULT_FROM_EMAIL', 'no-reply@trustpoint.de') # Default: console (safe for dev/showcases) EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # If EMAIL_HOST is present, switch to SMTP -_email_host = os.getenv('EMAIL_HOST') # e.g. "smtp.customer.tld" or "mailpit" +_email_host = _env_value('EMAIL_HOST', '') # e.g. "smtp.customer.tld" or "mailpit" if _email_host: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = _email_host - EMAIL_PORT = int(os.getenv('EMAIL_PORT', '587')) + EMAIL_PORT = int(_env_value('EMAIL_PORT', '587')) # Sensible defaults based on port; env can override _SMTP_TLS_PORT = 587 _SMTP_SSL_PORT = 465 - _use_tls_env = os.getenv('EMAIL_USE_TLS') - _use_ssl_env = os.getenv('EMAIL_USE_SSL') - EMAIL_USE_TLS = (_use_tls_env.lower() in ('1', 'true', 'yes')) if _use_tls_env else (EMAIL_PORT == _SMTP_TLS_PORT) - EMAIL_USE_SSL = (_use_ssl_env.lower() in ('1', 'true', 'yes')) if _use_ssl_env else (EMAIL_PORT == _SMTP_SSL_PORT) + EMAIL_USE_TLS = _env_bool('EMAIL_USE_TLS', default=EMAIL_PORT == _SMTP_TLS_PORT) + EMAIL_USE_SSL = _env_bool('EMAIL_USE_SSL', default=EMAIL_PORT == _SMTP_SSL_PORT) - EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') # auth only if both non-empty - EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') - EMAIL_TIMEOUT = int(os.getenv('EMAIL_TIMEOUT', '10')) + EMAIL_HOST_USER = _env_value('EMAIL_HOST_USER', '') # auth only if both non-empty + EMAIL_HOST_PASSWORD = _env_value('EMAIL_HOST_PASSWORD', '') + EMAIL_TIMEOUT = int(_env_value('EMAIL_TIMEOUT', '10')) STORAGES = { @@ -380,11 +396,11 @@ def is_postgre_available() -> bool: DATABASES = { 'default': { 'ENGINE': DATABASE_ENGINE, - 'NAME': os.environ.get('POSTGRES_DB', POSTGRES_DB), - 'USER': os.environ.get('DATABASE_USER', DATABASE_USER), - 'PASSWORD': os.environ.get('DATABASE_PASSWORD', DATABASE_PASSWORD), - 'HOST': os.environ.get('DATABASE_HOST', DATABASE_HOST), - 'PORT': os.environ.get('DATABASE_PORT', DATABASE_PORT), + 'NAME': POSTGRES_DB, + 'USER': DATABASE_USER, + 'PASSWORD': DATABASE_PASSWORD, + 'HOST': DATABASE_HOST, + 'PORT': DATABASE_PORT, } } else: From e4916c1c13011d2d3e2b1e02e24ba98ee2614740 Mon Sep 17 00:00:00 2001 From: BytesWelder Date: Tue, 16 Jun 2026 09:05:21 +0200 Subject: [PATCH 07/18] Add tests for codecov --- .../pki/tests/test_views_issuing_cas.py | 58 ++++++++++++++++ .../request/tests/test_authentication.py | 22 +++++- trustpoint/trustpoint/tests/test_settings.py | 69 +++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 trustpoint/pki/tests/test_views_issuing_cas.py diff --git a/trustpoint/pki/tests/test_views_issuing_cas.py b/trustpoint/pki/tests/test_views_issuing_cas.py new file mode 100644 index 000000000..4d504a8d1 --- /dev/null +++ b/trustpoint/pki/tests/test_views_issuing_cas.py @@ -0,0 +1,58 @@ +"""Tests for PKI issuing CA views.""" + +from unittest.mock import Mock, call, patch + +from pki.views.issuing_cas import IssuingCaRequestCertCmpView + + +def _mock_certificate(subject: bytes = b'subject', issuer: bytes = b'issuer') -> Mock: + """Return a certificate-like mock with public subject and issuer bytes.""" + cert = Mock() + cert.subject.public_bytes.return_value = subject + cert.issuer.public_bytes.return_value = issuer + return cert + + +def test_find_existing_ca_for_certificate_prefers_keyless_match() -> None: + """The lookup should return the keyless CA match before checking credential CAs.""" + cert = _mock_certificate() + keyless_ca = Mock() + keyless_queryset = Mock() + keyless_queryset.first.return_value = keyless_ca + + with patch('pki.views.issuing_cas.CaModel.objects.filter', return_value=keyless_queryset) as filter_mock: + result = IssuingCaRequestCertCmpView()._find_existing_ca_for_certificate(cert) + + assert result is keyless_ca + filter_mock.assert_called_once_with( + certificate__subject_public_bytes=b'subject'.hex().upper(), + certificate__issuer_public_bytes=b'issuer'.hex().upper(), + ) + + +def test_find_existing_ca_for_certificate_falls_back_to_credential_match() -> None: + """The lookup should check credential-backed CAs when no keyless CA matches.""" + cert = _mock_certificate() + credential_ca = Mock() + keyless_queryset = Mock() + keyless_queryset.first.return_value = None + credential_queryset = Mock() + credential_queryset.first.return_value = credential_ca + + with patch( + 'pki.views.issuing_cas.CaModel.objects.filter', + side_effect=[keyless_queryset, credential_queryset], + ) as filter_mock: + result = IssuingCaRequestCertCmpView()._find_existing_ca_for_certificate(cert) + + assert result is credential_ca + assert filter_mock.call_args_list == [ + call( + certificate__subject_public_bytes=b'subject'.hex().upper(), + certificate__issuer_public_bytes=b'issuer'.hex().upper(), + ), + call( + credential__certificate__subject_public_bytes=b'subject'.hex().upper(), + credential__certificate__issuer_public_bytes=b'issuer'.hex().upper(), + ), + ] diff --git a/trustpoint/request/tests/test_authentication.py b/trustpoint/request/tests/test_authentication.py index e60d428a0..80986b20e 100644 --- a/trustpoint/request/tests/test_authentication.py +++ b/trustpoint/request/tests/test_authentication.py @@ -8,7 +8,8 @@ from request.authentication.base import ClientCertificateAuthentication from request.authentication.est import UsernamePasswordAuthentication -from request.request_context import BaseRequestContext, EstBaseRequestContext +from request.authentication.rest import RestUsernamePasswordAuthentication +from request.request_context import BaseRequestContext, EstBaseRequestContext, RestBaseRequestContext class TestUsernamePasswordAuthentication: @@ -83,6 +84,25 @@ def test_authenticate_missing_password(self, device_instance): assert result is None +class TestRestUsernamePasswordAuthentication: + """Test cases for REST username/password authentication.""" + + def setup_method(self): + """Set up test fixtures.""" + self.auth = RestUsernamePasswordAuthentication() + self.context = Mock(spec=RestBaseRequestContext) + + def test_authenticate_success(self, est_device_without_onboarding): + """Test successful REST username/password authentication.""" + device = est_device_without_onboarding['device'] + self.context.rest_username = device.common_name + self.context.rest_password = device.no_onboarding_config.est_password + + self.auth.authenticate(self.context) + + assert self.context.device == device + + class TestClientCertificateAuthentication: """Test cases for ClientCertificateAuthentication.""" diff --git a/trustpoint/trustpoint/tests/test_settings.py b/trustpoint/trustpoint/tests/test_settings.py index 34f617be9..5fcbe66f0 100644 --- a/trustpoint/trustpoint/tests/test_settings.py +++ b/trustpoint/trustpoint/tests/test_settings.py @@ -70,6 +70,75 @@ def test_tp_urls_deduplicates_hosts_and_origins(monkeypatch): assert settings.CSRF_TRUSTED_ORIGINS.count('https://dup.local:9443') == 1 +def test_env_bool_uses_default_when_variable_is_missing(monkeypatch): + """Ensure boolean environment settings keep their default when unset.""" + monkeypatch.delenv('POSTGRESQL', raising=False) + + assert settings._env_bool('POSTGRESQL', default=True) is True + assert settings._env_bool('POSTGRESQL', default=False) is False + + +def test_env_bool_uses_default_when_variable_is_blank(monkeypatch): + """Ensure blank boolean environment settings keep their default.""" + monkeypatch.setenv('EMAIL_USE_TLS', '') + + assert settings._env_bool('EMAIL_USE_TLS', default=True) is True + assert settings._env_bool('EMAIL_USE_TLS', default=False) is False + + +def test_env_bool_parses_truthy_values(monkeypatch): + """Ensure common truthy strings enable boolean settings.""" + for value in ('1', 'true', 'yes', 'on', ' TRUE '): + monkeypatch.setenv('POSTGRESQL', value) + + assert settings._env_bool('POSTGRESQL', default=False) is True + + +def test_env_bool_treats_other_values_as_false(monkeypatch): + """Ensure non-truthy strings disable boolean settings.""" + for value in ('0', 'false', 'no', 'off', 'unexpected'): + monkeypatch.setenv('POSTGRESQL', value) + + assert settings._env_bool('POSTGRESQL', default=True) is False + + +def test_env_value_prefers_direct_environment_variable(monkeypatch, tmp_path): + """Ensure direct environment variables win over Docker secret files.""" + secret_file = tmp_path / 'db_user' + secret_file.write_text('secret-user\n') + monkeypatch.setenv('DATABASE_USER', 'env-user') + monkeypatch.setenv('DATABASE_USER_FILE', str(secret_file)) + + assert settings._env_value('DATABASE_USER', 'admin', file_var='DATABASE_USER_FILE') == 'env-user' + + +def test_env_value_reads_docker_secret_file(monkeypatch, tmp_path): + """Ensure settings can be loaded from Docker secret files.""" + secret_file = tmp_path / 'db_password' + secret_file.write_text('secret-password\n') + monkeypatch.delenv('DATABASE_PASSWORD', raising=False) + monkeypatch.setenv('DATABASE_PASSWORD_FILE', str(secret_file)) + + assert settings._env_value( + 'DATABASE_PASSWORD', + 'testing321', + file_var='DATABASE_PASSWORD_FILE', + ) == 'secret-password' + + +def test_env_value_falls_back_when_secret_file_is_unreadable(monkeypatch, tmp_path): + """Ensure unreadable Docker secret paths do not break settings import.""" + missing_file = tmp_path / 'missing_secret' + monkeypatch.delenv('DATABASE_PASSWORD', raising=False) + monkeypatch.setenv('DATABASE_PASSWORD_FILE', str(missing_file)) + + assert settings._env_value( + 'DATABASE_PASSWORD', + 'testing321', + file_var='DATABASE_PASSWORD_FILE', + ) == 'testing321' + + def test_database_settings(monkeypatch): """Ensure database settings are set correctly.""" with mock.patch('socket.create_connection') as mock_socket_conn: From 4378d7a35b20ef7bfdccabb8390c1c999e30539f Mon Sep 17 00:00:00 2001 From: florianhandke Date: Tue, 16 Jun 2026 14:21:58 +0200 Subject: [PATCH 08/18] No default .env file + add .env to gitignore --- .env | 23 ----------------------- .gitignore | 2 ++ 2 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 23c172277..000000000 --- a/.env +++ /dev/null @@ -1,23 +0,0 @@ -# Development defaults used by docker compose and tp_wizard.sh. -# Override these values locally for production-like deployments. - -# PostgreSQL connection used by the Trustpoint containers. -POSTGRES_DB=trustpoint_db -DATABASE_USER=admin -DATABASE_PASSWORD=testing321 -DATABASE_HOST=postgres -DATABASE_PORT=5432 - -# Hostnames / IP addresses at which Trustpoint is reachable, comma-separated. -# Do not include the protocol. Include a port only when it differs from 80/443. -TP_URLS=trustpoint.local - -# Optional mail settings. Leave EMAIL_HOST empty to use Django's console backend. -DEFAULT_FROM_EMAIL=no-reply@trustpoint.de -EMAIL_HOST= -EMAIL_PORT=587 -EMAIL_USE_TLS= -EMAIL_USE_SSL= -EMAIL_HOST_USER= -EMAIL_HOST_PASSWORD= -EMAIL_TIMEOUT=10 diff --git a/.gitignore b/.gitignore index 7000fc282..3dcb0a258 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,5 @@ tests/client/* node_modules/ workflow2Folder/ + +.env From 4708b5602a524b6a1453d7b77133403e27bffc1f Mon Sep 17 00:00:00 2001 From: florianhandke Date: Wed, 17 Jun 2026 13:20:52 +0200 Subject: [PATCH 09/18] Enhance configuration and setup process with environment variables --- .env.example | 52 ++++- docker-compose.softhsm.yml | 4 +- docker-compose.yml | 6 +- .../getting_started/quickstart_setup.rst | 72 +++++- trustpoint/help_pages/devices_help_views.py | 10 +- trustpoint/help_pages/pki_help_views.py | 9 +- .../tests/test_devices_help_views.py | 10 +- .../help_pages/tests/test_pki_help_views.py | 25 ++- .../commands/auto_setup_from_env.py | 211 ++++++++++++++++++ .../management/commands/startup_manager.py | 40 +++- .../request/operation_processor/issue_cert.py | 8 +- .../test_certificate_request_processors.py | 4 +- trustpoint/trustpoint/settings.py | 77 ++++--- 13 files changed, 454 insertions(+), 74 deletions(-) create mode 100644 trustpoint/management/management/commands/auto_setup_from_env.py diff --git a/.env.example b/.env.example index 0aef1e4e4..840ea61cf 100644 --- a/.env.example +++ b/.env.example @@ -15,16 +15,46 @@ DATABASE_PASSWORD=testing321 DATABASE_HOST=postgres DATABASE_PORT=5432 -# Hostnames / IP addresses at which Trustpoint is reachable, comma-separated. -# Do not include the protocol. Include a port only when it differs from 80/443. -TP_URLS=trustpoint.local +# TLS Server Certificate Configuration +# Comma-separated lists of addresses/names where Trustpoint is reachable. +# These are used for: +# - Subject Alternative Names (SANs) in the TLS certificate +# - Django ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS +# At least one value must be specified for production deployments. +TP_TLS_IPV4_ADDRESSES=127.0.0.1 +TP_TLS_IPV6_ADDRESSES=::1 +TP_TLS_DNS_NAMES=localhost + +# HTTP and HTTPS ports for external access +# Only include non-standard ports in CSRF_TRUSTED_ORIGINS (default: 80 for HTTP, 443 for HTTPS) +# TP_HTTP_PORT=80 +# TP_HTTPS_PORT=443 # Optional mail settings. Leave EMAIL_HOST empty to use Django's console backend. -DEFAULT_FROM_EMAIL=no-reply@trustpoint.de -EMAIL_HOST= -EMAIL_PORT=587 -EMAIL_USE_TLS= -EMAIL_USE_SSL= -EMAIL_HOST_USER= -EMAIL_HOST_PASSWORD= -EMAIL_TIMEOUT=10 +# DEFAULT_FROM_EMAIL=no-reply@trustpoint.de +# EMAIL_HOST= +# EMAIL_PORT=587 +# EMAIL_USE_TLS= +# EMAIL_USE_SSL= +# EMAIL_HOST_USER= +# EMAIL_HOST_PASSWORD= +# EMAIL_TIMEOUT=10 + +# ======================================================================== +# Auto-Setup Configuration (Skip Setup Wizard) +# ======================================================================== +# If TP_AUTO_SETUP=true, Trustpoint will automatically configure itself +# from environment variables, bypassing the interactive setup wizard. + +# Enable automatic setup from environment variables (true/false) +# TP_AUTO_SETUP=false + +# Superuser credentials (REQUIRED if TP_AUTO_SETUP=true) +# TP_ADMIN_USERNAME=admin +# TP_ADMIN_PASSWORD=testing321 + +# Inject demo data for testing (true/false) +# TP_INJECT_DEMO_DATA=false + +# Note: When auto-setup is enabled, the TLS certificate will be automatically +# generated using the TP_TLS_* variables specified above. diff --git a/docker-compose.softhsm.yml b/docker-compose.softhsm.yml index 92ba5192a..4da4ef409 100644 --- a/docker-compose.softhsm.yml +++ b/docker-compose.softhsm.yml @@ -7,8 +7,8 @@ services: container_name: trustpoint restart: unless-stopped ports: - - "80:80" - - "443:443" + - "${TP_HTTP_PORT:-80}:80" + - "${TP_HTTPS_PORT:-443}:443" depends_on: softhsm: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index c708bcf89..43b462619 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,8 @@ services: container_name: trustpoint restart: unless-stopped ports: - - "80:80" - - "443:443" + - "${TP_HTTP_PORT:-80}:80" + - "${TP_HTTPS_PORT:-443}:443" env_file: - .env environment: @@ -17,7 +17,6 @@ services: DATABASE_PASSWORD: "${DATABASE_PASSWORD:-testing321}" DATABASE_HOST: "${DATABASE_HOST:-postgres}" DATABASE_PORT: "${DATABASE_PORT:-5432}" - TP_URLS: "${TP_URLS:-trustpoint.local}" healthcheck: test: ["CMD-SHELL", "curl -fsk --max-time 5 https://localhost/ > /dev/null"] interval: 30s @@ -42,7 +41,6 @@ services: DATABASE_PASSWORD: "${DATABASE_PASSWORD:-testing321}" DATABASE_HOST: "${DATABASE_HOST:-postgres}" DATABASE_PORT: "${DATABASE_PORT:-5432}" - TP_URLS: "${TP_URLS:-trustpoint.local}" TRUSTPOINT_SERVICE_ROLE: "worker" postgres: diff --git a/docs/source/getting_started/quickstart_setup.rst b/docs/source/getting_started/quickstart_setup.rst index a918b8b8b..f08ebf6f1 100644 --- a/docs/source/getting_started/quickstart_setup.rst +++ b/docs/source/getting_started/quickstart_setup.rst @@ -37,6 +37,10 @@ Getting started with Docker Compose The .env file ^^^^^^^^^^^^^ +.. warning:: + + Trustpoint **requires** a ``.env`` file to start. If no ``.env`` file is present, startup will fail with an error. + Docker Compose and ``tp_wizard.sh`` read the ``.env`` file in the project root. The repository contains development defaults. For production-like deployments, copy the example and replace at least the database password: @@ -69,9 +73,21 @@ The following variables are supported: * - ``DATABASE_PORT`` - No - Database port used by the Trustpoint containers. Defaults to ``5432``. - * - ``TP_URLS`` + * - ``TP_TLS_IPV4_ADDRESSES`` + - No + - Comma-separated IPv4 addresses where Trustpoint is reachable. Used for TLS certificate SANs and Django ``ALLOWED_HOSTS``. Defaults to ``127.0.0.1``. + * - ``TP_TLS_IPV6_ADDRESSES`` + - No + - Comma-separated IPv6 addresses where Trustpoint is reachable. Used for TLS certificate SANs and Django ``ALLOWED_HOSTS``. Defaults to ``::1``. + * - ``TP_TLS_DNS_NAMES`` + - No + - Comma-separated DNS names where Trustpoint is reachable. Used for TLS certificate SANs and Django ``ALLOWED_HOSTS``. Defaults to ``localhost``. + * - ``TP_HTTP_PORT`` + - No + - HTTP port for external access. Used in ``CSRF_TRUSTED_ORIGINS`` and Docker port mapping. Defaults to ``80``. + * - ``TP_HTTPS_PORT`` - No - - Comma-separated hostnames or IPs at which Trustpoint is reachable (no protocol prefix). Defaults to ``trustpoint.local``. + - HTTPS port for external access. Used in ``CSRF_TRUSTED_ORIGINS`` and Docker port mapping. Defaults to ``443``. * - ``DEFAULT_FROM_EMAIL`` - No - Sender address for Trustpoint emails. Defaults to ``no-reply@trustpoint.de``. @@ -79,16 +95,64 @@ The following variables are supported: - No - SMTP host. Leave empty to use Django's console backend. -Minimal ``.env`` example: +Auto-Setup from Environment Variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Trustpoint can be configured to skip the interactive setup wizard and auto-configure from environment variables. +This is useful for automated deployments, CI/CD pipelines, or headless installations. + +.. important:: + + When ``TP_AUTO_SETUP=true``, the ``TP_ADMIN_USERNAME`` and ``TP_ADMIN_PASSWORD`` variables become **required**. + +To enable auto-setup, add the following variables to your ``.env`` file: + +.. list-table:: + :widths: 30 10 60 + :header-rows: 1 + + * - Variable + - Required + - Description + * - ``TP_AUTO_SETUP`` + - Yes + - Set to ``true`` to enable automatic setup. Default: ``false`` + * - ``TP_ADMIN_USERNAME`` + - Yes* + - Initial superuser username. *Required when auto-setup is enabled. + * - ``TP_ADMIN_PASSWORD`` + - Yes* + - Initial superuser password. *Required when auto-setup is enabled. + * - ``TP_INJECT_DEMO_DATA`` + - No + - Set to ``true`` to inject demo domains and devices for testing. Default: ``false`` + +.. note:: + + The TLS certificate SANs (``TP_TLS_IPV4_ADDRESSES``, ``TP_TLS_IPV6_ADDRESSES``, ``TP_TLS_DNS_NAMES``) + are also used to configure Django's ``ALLOWED_HOSTS`` and ``CSRF_TRUSTED_ORIGINS``, ensuring consistency + between your TLS certificate and Django security settings. + +Auto-setup ``.env`` example: .. code-block:: bash + # Database configuration POSTGRES_DB=trustpoint_db DATABASE_USER=admin DATABASE_PASSWORD=correct-horse-battery-staple DATABASE_HOST=postgres DATABASE_PORT=5432 - TP_URLS=trustpoint.myfactory.local,10.0.0.5 + + # TLS/Network configuration + TP_TLS_IPV4_ADDRESSES=10.0.0.5 + TP_TLS_DNS_NAMES=trustpoint.local + + # Auto-setup configuration + TP_AUTO_SETUP=true + TP_ADMIN_USERNAME=admin + TP_ADMIN_PASSWORD=secure_admin_password_here + TP_INJECT_DEMO_DATA=false Setup (Load from Docker Hub) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/trustpoint/help_pages/devices_help_views.py b/trustpoint/help_pages/devices_help_views.py index d24a8d646..57f396e1c 100644 --- a/trustpoint/help_pages/devices_help_views.py +++ b/trustpoint/help_pages/devices_help_views.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, override from cryptography import x509 +from django.conf import settings from django.contrib import messages from django.core.management import call_command from django.http import FileResponse, Http404, HttpResponseRedirect @@ -81,7 +82,8 @@ def _make_context(self, host_ip: str = '127.0.0.1') -> HelpContext: if not domain: raise Http404(_('No domain is configured for this device.')) - host_base = f'https://{host_ip}:{self.request.META.get("SERVER_PORT", "443")}' + https_port = settings.TP_HTTPS_PORT or '443' + host_base = f'https://{host_ip}:{https_port}' if https_port != '443' else f'https://{host_ip}' cred_count = IssuedCredentialModel.objects.filter(device=device).count() public_key_info = domain.public_key_info @@ -1876,7 +1878,8 @@ def _make_context(self, host_ip: str = '127.0.0.1') -> HelpContext: except DomainModel.DoesNotExist as exc: raise Http404(_('No domains configured in the system.')) from exc - host_base = f'https://{host_ip}:{self.request.META.get("SERVER_PORT", "443")}' + https_port = settings.TP_HTTPS_PORT or '443' + host_base = f'https://{host_ip}:{https_port}' if https_port != '443' else f'https://{host_ip}' public_key_info = domain.public_key_info if not public_key_info: @@ -1960,7 +1963,8 @@ def _make_context(self, host_ip: str = '127.0.0.1') -> HelpContext: except DomainModel.DoesNotExist as exc: raise Http404(_('No domains configured in the system.')) from exc - host_base = f'https://{host_ip}:{self.request.META.get("SERVER_PORT", "443")}' + https_port = settings.TP_HTTPS_PORT or '443' + host_base = f'https://{host_ip}:{https_port}' if https_port != '443' else f'https://{host_ip}' public_key_info = domain.public_key_info if not public_key_info: diff --git a/trustpoint/help_pages/pki_help_views.py b/trustpoint/help_pages/pki_help_views.py index 1a63cab51..9658fb101 100644 --- a/trustpoint/help_pages/pki_help_views.py +++ b/trustpoint/help_pages/pki_help_views.py @@ -4,6 +4,7 @@ from typing import Any, override +from django.conf import settings from django.http import Http404 from django.urls import reverse from django.utils.html import format_html @@ -55,7 +56,9 @@ def _make_context(self) -> HelpContext: if not domain: raise Http404(_('Failed to get domain from DevidRegistration.')) - host_base = f'https://{TlsSettings.get_first_ipv4_address()}:{self.request.META.get("SERVER_PORT", "443")}' + https_port = settings.TP_HTTPS_PORT or '443' + first_ip = TlsSettings.get_first_ipv4_address() + host_base = f'https://{first_ip}:{https_port}' if https_port != '443' else f'https://{first_ip}' public_key_info = domain.public_key_info if not public_key_info: @@ -261,7 +264,9 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context = super().get_context_data(**kwargs) ca = self.object - host_base = f'https://{TlsSettings.get_first_ipv4_address()}:{self.request.META.get("SERVER_PORT", "443")}' + https_port = settings.TP_HTTPS_PORT or '443' + first_ip = TlsSettings.get_first_ipv4_address() + host_base = f'https://{first_ip}:{https_port}' if https_port != '443' else f'https://{first_ip}' crl_endpoint = f'{host_base}/crl/{ca.pk}/' has_crl = bool(ca.crl_pem) diff --git a/trustpoint/help_pages/tests/test_devices_help_views.py b/trustpoint/help_pages/tests/test_devices_help_views.py index 70d0eabad..1809297a3 100644 --- a/trustpoint/help_pages/tests/test_devices_help_views.py +++ b/trustpoint/help_pages/tests/test_devices_help_views.py @@ -54,10 +54,11 @@ def test_make_context_success( self.view.object = mock_device request = self.factory.get('/') - request.META['SERVER_PORT'] = '8443' self.view.request = request - context = self.view._make_context('192.168.1.1') + with patch('help_pages.devices_help_views.settings') as mock_settings: + mock_settings.TP_HTTPS_PORT = '8443' + context = self.view._make_context('192.168.1.1') assert context.domain == mock_domain assert context.domain_unique_name == 'test-domain' @@ -135,10 +136,11 @@ def test_get_context_data_success( self.view.page_category = 'devices' self.view.page_name = 'devices' request = self.factory.get('/') - request.META['SERVER_PORT'] = '443' self.view.request = request - context = self.view.get_context_data() + with patch('help_pages.devices_help_views.settings') as mock_settings: + mock_settings.TP_HTTPS_PORT = '443' + context = self.view.get_context_data() assert 'help_page' in context assert context['help_page'].heading == 'Test Heading' diff --git a/trustpoint/help_pages/tests/test_pki_help_views.py b/trustpoint/help_pages/tests/test_pki_help_views.py index 83256abfe..c6c89ab75 100644 --- a/trustpoint/help_pages/tests/test_pki_help_views.py +++ b/trustpoint/help_pages/tests/test_pki_help_views.py @@ -39,10 +39,11 @@ def test_make_context_success(self, mock_get_ip: Mock) -> None: self.view.object = mock_registration request = self.factory.get('/') - request.META['SERVER_PORT'] = '8443' self.view.request = request - context = self.view._make_context() + with patch('help_pages.pki_help_views.settings') as mock_settings: + mock_settings.TP_HTTPS_PORT = '8443' + context = self.view._make_context() assert context.domain == mock_domain assert context.domain_unique_name == 'test-domain' @@ -99,10 +100,11 @@ def test_get_context_data_success(self, mock_get_ip: Mock) -> None: self.view.page_category = 'pki' self.view.page_name = 'domains' request = self.factory.get('/') - request.META['SERVER_PORT'] = '443' self.view.request = request - context = self.view.get_context_data() + with patch('help_pages.pki_help_views.settings') as mock_settings: + mock_settings.TP_HTTPS_PORT = '443' + context = self.view.get_context_data() assert 'help_page' in context assert context['help_page'].heading == 'Test Heading' @@ -246,10 +248,11 @@ def test_get_context_data_with_crl( self.view.page_category = 'pki' self.view.page_name = 'issuing_cas' request = self.factory.get('/') - request.META['SERVER_PORT'] = '443' self.view.request = request - context = self.view.get_context_data() + with patch('help_pages.pki_help_views.settings') as mock_settings: + mock_settings.TP_HTTPS_PORT = '443' + context = self.view.get_context_data() assert 'help_page' in context help_page = context['help_page'] @@ -283,10 +286,11 @@ def test_get_context_data_without_crl( self.view.page_category = 'pki' self.view.page_name = 'issuing_cas' request = self.factory.get('/') - request.META['SERVER_PORT'] = '443' self.view.request = request - context = self.view.get_context_data() + with patch('help_pages.pki_help_views.settings') as mock_settings: + mock_settings.TP_HTTPS_PORT = '443' + context = self.view.get_context_data() help_page = context['help_page'] status_section = help_page.sections[1] @@ -314,10 +318,11 @@ def test_get_context_data_curl_commands( self.view.page_category = 'pki' self.view.page_name = 'issuing_cas' request = self.factory.get('/') - request.META['SERVER_PORT'] = '443' self.view.request = request - context = self.view.get_context_data() + with patch('help_pages.pki_help_views.settings') as mock_settings: + mock_settings.TP_HTTPS_PORT = '443' + context = self.view.get_context_data() help_page = context['help_page'] download_section = help_page.sections[3] # Download CRL section diff --git a/trustpoint/management/management/commands/auto_setup_from_env.py b/trustpoint/management/management/commands/auto_setup_from_env.py new file mode 100644 index 000000000..1a2f16208 --- /dev/null +++ b/trustpoint/management/management/commands/auto_setup_from_env.py @@ -0,0 +1,211 @@ +"""Django management command to auto-configure Trustpoint from environment variables.""" + +import ipaddress +import os +import sys +from pathlib import Path +from typing import Any + +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.management import call_command +from django.core.management.base import BaseCommand, CommandError +from django.db import DatabaseError, transaction +from django.db.models import ProtectedError + +from management.models import KeyStorageConfig +from management.nginx_paths import NGINX_CERT_CHAIN_PATH, NGINX_CERT_PATH, NGINX_KEY_PATH +from pki.models import CredentialModel +from pki.models.truststore import ActiveTrustpointTlsServerCredentialModel +from setup_wizard.models import SetupWizardCompletedModel +from setup_wizard.tls_credential import TlsServerCredentialGenerator +from setup_wizard.views import execute_shell_script + + +UPDATE_TLS_NGINX = Path('/etc/trustpoint/wizard/update_tls_nginx.sh') + + +class Command(BaseCommand): + """Auto-configure Trustpoint from environment variables.""" + + help = 'Auto-configure Trustpoint from environment variables, bypassing the setup wizard' + + def _env_value(self, name: str, *, required: bool = True, default: str | None = None) -> str | None: + """Get an environment variable value.""" + value = os.getenv(name) + if value is None or value.strip() == '': + if required: + err_msg = f'Required environment variable {name} is not set' + raise CommandError(err_msg) + return default + return value.strip() + + def _env_bool(self, name: str, *, default: bool = False) -> bool: + """Get a boolean environment variable.""" + raw_value = os.getenv(name) + if raw_value is None or raw_value.strip() == '': + return default + return raw_value.strip().lower() in {'1', 'true', 'yes', 'on'} + + def _create_superuser(self, username: str, password: str, email: str) -> None: + """Create the superuser account.""" + self.stdout.write('Creating superuser...') + try: + if User.objects.filter(username=username).exists(): + self.stdout.write(self.style.WARNING(f'User {username} already exists, skipping creation')) + return + + call_command('createsuperuser', interactive=False, username=username, email=email) + user = User.objects.get(username=username) + user.set_password(password) + user.save() + self.stdout.write(self.style.SUCCESS(f'Superuser {username} created successfully')) + except Exception as e: + err_msg = f'Failed to create superuser: {e}' + raise CommandError(err_msg) from e + + def _configure_storage(self) -> None: + """Configure cryptographic storage (currently only SOFTWARE supported).""" + self.stdout.write('Configuring crypto storage...') + try: + key_storage_config = KeyStorageConfig.get_or_create_default() + key_storage_config.storage_type = KeyStorageConfig.StorageType.SOFTWARE + key_storage_config.save(update_fields=['storage_type']) + self.stdout.write(self.style.SUCCESS('Crypto storage configured')) + except Exception as e: + err_msg = f'Failed to configure storage: {e}' + raise CommandError(err_msg) from e + + def _parse_csv_list(self, value: str | None) -> list[str]: + """Parse comma-separated values into a list.""" + if not value: + return [] + return [item.strip() for item in value.split(',') if item.strip()] + + def _generate_tls_credential( + self, + ipv4_addresses: list[str], + ipv6_addresses: list[str], + dns_names: list[str], + ) -> CredentialModel: + """Generate TLS server credential.""" + self.stdout.write('Generating TLS server credential...') + try: + parsed_ipv4 = [ipaddress.IPv4Address(addr) for addr in ipv4_addresses] + parsed_ipv6 = [ipaddress.IPv6Address(addr) for addr in ipv6_addresses] + + generator = TlsServerCredentialGenerator( + ipv4_addresses=parsed_ipv4, + ipv6_addresses=parsed_ipv6, + domain_names=dns_names, + ) + tls_credential_serializer = generator.generate_tls_server_credential() + + with transaction.atomic(): + credential_model = CredentialModel.save_credential_serializer( + credential_serializer=tls_credential_serializer, + credential_type=CredentialModel.CredentialTypeChoice.TRUSTPOINT_TLS_SERVER, + ) + + self.stdout.write(self.style.SUCCESS('TLS server credential generated')) + return credential_model + + except (ValueError, DjangoValidationError, ProtectedError, TypeError) as e: + err_msg = f'Failed to generate TLS credential: {e}' + raise CommandError(err_msg) from e + + def _apply_tls_credential(self, credential_model: CredentialModel) -> None: + """Apply TLS credential to nginx.""" + self.stdout.write('Applying TLS credential...') + try: + active_tls, _ = ActiveTrustpointTlsServerCredentialModel.objects.get_or_create(id=1) + active_tls.credential = credential_model + active_tls.save() + + self._write_pem_files(credential_model) + execute_shell_script(UPDATE_TLS_NGINX, 'no_hsm') + + self.stdout.write(self.style.SUCCESS('TLS credential applied')) + except Exception as e: + err_msg = f'Failed to apply TLS credential: {e}' + raise CommandError(err_msg) from e + + def _write_pem_files(self, credential_model: CredentialModel) -> None: + """Write TLS certificate and key files to disk.""" + private_key_pem = credential_model.get_private_key_serializer().as_pkcs8_pem().decode() + certificate_pem = credential_model.get_certificate_serializer().as_pem().decode() + trust_store_pem = credential_model.get_certificate_chain_serializer().as_pem().decode() + + NGINX_KEY_PATH.write_text(private_key_pem) + NGINX_CERT_PATH.write_text(certificate_pem) + + if trust_store_pem.strip(): + NGINX_CERT_CHAIN_PATH.write_text(trust_store_pem) + elif NGINX_CERT_CHAIN_PATH.exists(): + NGINX_CERT_CHAIN_PATH.unlink() + + def handle(self, *args: Any, **options: Any) -> None: + """Execute the auto-setup command.""" + del args + del options + + self.stdout.write(self.style.WARNING('=== Trustpoint Auto-Setup from Environment Variables ===')) + + if SetupWizardCompletedModel.setup_wizard_completed(): + self.stdout.write(self.style.WARNING('Setup wizard already completed, skipping auto-setup')) + return + + try: + username = self._env_value('TP_ADMIN_USERNAME', required=True) + password = self._env_value('TP_ADMIN_PASSWORD', required=True) + email = self._env_value('TP_ADMIN_EMAIL', required=False, default='') or '' + + inject_demo_data = self._env_bool('TP_INJECT_DEMO_DATA', default=False) + + tls_ipv4_raw = self._env_value('TP_TLS_IPV4_ADDRESSES', required=False, default='') or '' + tls_ipv6_raw = self._env_value('TP_TLS_IPV6_ADDRESSES', required=False, default='') or '' + tls_dns_raw = self._env_value('TP_TLS_DNS_NAMES', required=False, default='') or '' + + tls_ipv4 = self._parse_csv_list(tls_ipv4_raw) + tls_ipv6 = self._parse_csv_list(tls_ipv6_raw) + tls_dns = self._parse_csv_list(tls_dns_raw) + + if not tls_ipv4 and not tls_ipv6 and not tls_dns: + tls_ipv4 = ['127.0.0.1'] + tls_ipv6 = ['::1'] + tls_dns = ['localhost'] + + with transaction.atomic(): + if not isinstance(username, str) or not isinstance(password, str): + err_msg = 'Username and password must be strings' + raise CommandError(err_msg) + self._create_superuser(username, password, email) + + self._configure_storage() + + self.stdout.write('Creating default certificate profiles...') + call_command('create_default_cert_profiles') + self.stdout.write(self.style.SUCCESS('Certificate profiles created')) + + if inject_demo_data: + self.stdout.write('Injecting demo data...') + call_command('add_domains_and_devices') + self.stdout.write(self.style.SUCCESS('Demo data injected')) + + self.stdout.write('Executing notifications...') + call_command('execute_all_notifications') + self.stdout.write(self.style.SUCCESS('Notifications executed')) + + credential_model = self._generate_tls_credential(tls_ipv4, tls_ipv6, tls_dns) + self._apply_tls_credential(credential_model) + + SetupWizardCompletedModel.mark_setup_complete_once() + self.stdout.write(self.style.SUCCESS('Setup marked as complete')) + + self.stdout.write(self.style.SUCCESS('=== Auto-setup completed successfully ===')) + + except CommandError: + raise + except (DatabaseError, FileNotFoundError, OSError, ProtectedError, RuntimeError, TypeError, ValueError) as e: + err_msg = f'Auto-setup failed: {e}' + raise CommandError(err_msg) from e diff --git a/trustpoint/management/management/commands/startup_manager.py b/trustpoint/management/management/commands/startup_manager.py index 6f8f67116..32e06794e 100644 --- a/trustpoint/management/management/commands/startup_manager.py +++ b/trustpoint/management/management/commands/startup_manager.py @@ -4,8 +4,12 @@ Copy the relevant parts into managestartup.py to complete the refactoring. """ +import os +import sys +from pathlib import Path + from django.conf import settings as django_settings -from django.core.management import CommandError +from django.core.management import CommandError, call_command from django.core.management.base import BaseCommand from django.db.utils import OperationalError, ProgrammingError from packaging.version import InvalidVersion, Version @@ -14,6 +18,7 @@ from management.util.output_wrapper import CommandOutputWrapper from management.util.startup_context import StartupContextBuilder from management.util.startup_strategies import StartupStrategySelector +from setup_wizard.models import SetupWizardCompletedModel class Command(BaseCommand): @@ -23,7 +28,40 @@ class Command(BaseCommand): def handle(self, **_options: dict[str, str]) -> None: """Entrypoint for the command.""" + self._check_env_file_exists() self.manage_startup() + self._check_auto_setup() + + def _check_env_file_exists(self) -> None: + """Check if .env file exists, fail startup if it does not.""" + env_file = Path('/var/www/html/trustpoint/.env') + if not env_file.exists(): + self.stdout.write( + self.style.ERROR( + 'FATAL: No .env file found. Trustpoint requires a .env file to start.\n' + 'Please create /var/www/html/trustpoint/.env with the required configuration.\n' + 'See .env.example for reference.' + ) + ) + sys.exit(1) + + def _check_auto_setup(self) -> None: + """Check if auto-setup should be performed from environment variables.""" + auto_setup = os.getenv('TP_AUTO_SETUP', '').strip().lower() in {'1', 'true', 'yes', 'on'} + + if not auto_setup: + return + + if SetupWizardCompletedModel.setup_wizard_completed(): + self.stdout.write(self.style.WARNING('TP_AUTO_SETUP is enabled but setup already completed, skipping')) + return + + self.stdout.write(self.style.WARNING('TP_AUTO_SETUP is enabled, running auto-setup from environment...')) + try: + call_command('auto_setup_from_env') + except CommandError as e: + self.stdout.write(self.style.ERROR(f'Auto-setup failed: {e}')) + raise def manage_startup(self) -> None: """Checks current state of trustpoint and acts accordingly.""" diff --git a/trustpoint/request/operation_processor/issue_cert.py b/trustpoint/request/operation_processor/issue_cert.py index 4884b48ec..120f63c92 100644 --- a/trustpoint/request/operation_processor/issue_cert.py +++ b/trustpoint/request/operation_processor/issue_cert.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, cast, get_args from cryptography import x509 +from django.conf import settings from trustpoint_core.crypto_types import AllowedCertSignHashAlgos from trustpoint_core.oid import SignatureSuite from trustpoint_core.serializer import CredentialSerializer @@ -107,10 +108,9 @@ def _get_crl_distribution_point_url(self, context: BaseRequestContext, ca_id: in request_meta = getattr(request, 'META', {}) if request else {} if not isinstance(request_meta, dict): request_meta = {} - port = request_meta.get('SERVER_PORT', '') - if port == '443': # CRL always served via HTTP - port = '' - port_str = f':{port}' if port else '' + + http_port = settings.TP_HTTP_PORT or '80' + port_str = f':{http_port}' if http_port != '80' else '' return f'http://{TlsSettings.get_first_ipv4_address()}{port_str}/crl/{ca_id}' def _save_credential( diff --git a/trustpoint/request/tests/test_certificate_request_processors.py b/trustpoint/request/tests/test_certificate_request_processors.py index c4d477316..769594da3 100644 --- a/trustpoint/request/tests/test_certificate_request_processors.py +++ b/trustpoint/request/tests/test_certificate_request_processors.py @@ -53,6 +53,8 @@ class _StoredRequest: operation='certification', ) - url = LocalCaCertificateIssueProcessor()._get_crl_distribution_point_url(context, ca_id=7) # noqa: SLF001 + with patch('request.operation_processor.issue_cert.settings') as mock_settings: + mock_settings.TP_HTTP_PORT = '80' + url = LocalCaCertificateIssueProcessor()._get_crl_distribution_point_url(context, ca_id=7) # noqa: SLF001 assert url.endswith('/crl/7') diff --git a/trustpoint/trustpoint/settings.py b/trustpoint/trustpoint/settings.py index 6e307ba75..cad6fdc2b 100644 --- a/trustpoint/trustpoint/settings.py +++ b/trustpoint/trustpoint/settings.py @@ -18,7 +18,6 @@ from importlib.metadata import PackageNotFoundError, version from pathlib import Path from typing import Any, ClassVar -from urllib.parse import urlparse import django_stubs_ext import psycopg @@ -189,33 +188,55 @@ def is_postgre_available() -> bool: ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]'] CSRF_TRUSTED_ORIGINS = ['http://localhost:8000', 'http://127.0.0.1:8000'] -TP_URLS = _env_value('TP_URLS', '') -raw_urls = TP_URLS - -if raw_urls: - # Split by comma and clean up whitespace - url_list = [url.strip() for url in raw_urls.split(',') if url.strip()] - - for url in url_list: - # Ensure scheme is present (fallback to https) - parsed = urlparse(url) if url.startswith(('http://', 'https://')) else urlparse(f'https://{url}') - - host_with_port = parsed.netloc - - # 1. Extract just host/IP for ALLOWED_HOSTS (strip port) - host_only = host_with_port.split(':')[0] - if host_only not in ALLOWED_HOSTS: - ALLOWED_HOSTS.append(host_only) - - # If mDNS domain, trust subdomains too - if host_only.endswith('.local') and f'.{host_only}' not in ALLOWED_HOSTS: - ALLOWED_HOSTS.append(f'.{host_only}') - - # 2. Extract the exact origin (scheme + host + port) for CSRF - # e.g., "https://trustpoint.local:8443" or "http://10.10.0.2" - exact_origin = f'{parsed.scheme}://{host_with_port}' - if exact_origin not in CSRF_TRUSTED_ORIGINS: - CSRF_TRUSTED_ORIGINS.append(exact_origin) +TP_TLS_IPV4_ADDRESSES = _env_value('TP_TLS_IPV4_ADDRESSES', '') +TP_TLS_IPV6_ADDRESSES = _env_value('TP_TLS_IPV6_ADDRESSES', '') +TP_TLS_DNS_NAMES = _env_value('TP_TLS_DNS_NAMES', '') + +TP_HTTP_PORT = _env_value('TP_HTTP_PORT', '80') +TP_HTTPS_PORT = _env_value('TP_HTTPS_PORT', '443') + +tls_ipv4_list = [addr.strip() for addr in TP_TLS_IPV4_ADDRESSES.split(',') if addr.strip()] +tls_ipv6_list = [addr.strip() for addr in TP_TLS_IPV6_ADDRESSES.split(',') if addr.strip()] +tls_dns_list = [name.strip() for name in TP_TLS_DNS_NAMES.split(',') if name.strip()] + +def _format_origin(scheme: str, host: str, port: str) -> str: + """Format an origin URL with optional port.""" + default_port = '80' if scheme == 'http' else '443' + if port == default_port: + return f'{scheme}://{host}' + return f'{scheme}://{host}:{port}' + +for ipv4 in tls_ipv4_list: + if ipv4 not in ALLOWED_HOSTS: + ALLOWED_HOSTS.append(ipv4) + http_origin = _format_origin('http', ipv4, TP_HTTP_PORT) + https_origin = _format_origin('https', ipv4, TP_HTTPS_PORT) + if http_origin not in CSRF_TRUSTED_ORIGINS: + CSRF_TRUSTED_ORIGINS.append(http_origin) + if https_origin not in CSRF_TRUSTED_ORIGINS: + CSRF_TRUSTED_ORIGINS.append(https_origin) + +for ipv6 in tls_ipv6_list: + if ipv6 not in ALLOWED_HOSTS: + ALLOWED_HOSTS.append(ipv6) + http_origin = _format_origin('http', f'[{ipv6}]', TP_HTTP_PORT) + https_origin = _format_origin('https', f'[{ipv6}]', TP_HTTPS_PORT) + if http_origin not in CSRF_TRUSTED_ORIGINS: + CSRF_TRUSTED_ORIGINS.append(http_origin) + if https_origin not in CSRF_TRUSTED_ORIGINS: + CSRF_TRUSTED_ORIGINS.append(https_origin) + +for dns_name in tls_dns_list: + if dns_name not in ALLOWED_HOSTS: + ALLOWED_HOSTS.append(dns_name) + if dns_name.endswith('.local') and f'.{dns_name}' not in ALLOWED_HOSTS: + ALLOWED_HOSTS.append(f'.{dns_name}') + http_origin = _format_origin('http', dns_name, TP_HTTP_PORT) + https_origin = _format_origin('https', dns_name, TP_HTTPS_PORT) + if http_origin not in CSRF_TRUSTED_ORIGINS: + CSRF_TRUSTED_ORIGINS.append(http_origin) + if https_origin not in CSRF_TRUSTED_ORIGINS: + CSRF_TRUSTED_ORIGINS.append(https_origin) # Settings for PostgreSQL database From a32477442396cf69340eccef4deab37d7b90d640 Mon Sep 17 00:00:00 2001 From: florianhandke Date: Wed, 17 Jun 2026 13:36:48 +0200 Subject: [PATCH 10/18] fix missing .env --- .github/workflows/backup-restore.yml | 3 +++ .github/workflows/docker-test-compose.yml | 3 +++ .github/workflows/r_200_feature_test.yml | 3 +++ .github/workflows/zap.yml | 3 +++ .../management/management/commands/auto_setup_from_env.py | 1 - 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backup-restore.yml b/.github/workflows/backup-restore.yml index 635f70a30..28f018d7c 100644 --- a/.github/workflows/backup-restore.yml +++ b/.github/workflows/backup-restore.yml @@ -17,6 +17,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Setup .env file + run: cp .env.example .env + - name: Build and start PostgreSQL run: | docker build -t trustpointproject/postgres:latest -f docker/db/Dockerfile . diff --git a/.github/workflows/docker-test-compose.yml b/.github/workflows/docker-test-compose.yml index e5c6ebee0..28998702c 100644 --- a/.github/workflows/docker-test-compose.yml +++ b/.github/workflows/docker-test-compose.yml @@ -15,6 +15,9 @@ jobs: - name: Checkout code uses: actions/checkout@v6 + - name: Setup .env file + run: cp .env.example .env + # Step: Run Docker Compose to start services. # Uses the hoverkraft-tech/compose-action to manage Docker Compose commands. # "compose-file": path to the docker-compose file. diff --git a/.github/workflows/r_200_feature_test.yml b/.github/workflows/r_200_feature_test.yml index 00956cb79..b81158b85 100644 --- a/.github/workflows/r_200_feature_test.yml +++ b/.github/workflows/r_200_feature_test.yml @@ -19,6 +19,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + + - name: Setup .env file + run: cp .env.example .env # Build and start Docker containers with docker-compose - name: Build and start Docker containers diff --git a/.github/workflows/zap.yml b/.github/workflows/zap.yml index 30c3eb526..b8c914824 100644 --- a/.github/workflows/zap.yml +++ b/.github/workflows/zap.yml @@ -20,6 +20,9 @@ jobs: - name: Checkout Code uses: actions/checkout@v6 + - name: Setup .env file + run: cp .env.example .env + - name: Start Trustpoint via docker-compose run: | docker compose -f docker-compose.yml up -d diff --git a/trustpoint/management/management/commands/auto_setup_from_env.py b/trustpoint/management/management/commands/auto_setup_from_env.py index 1a2f16208..44d46bce5 100644 --- a/trustpoint/management/management/commands/auto_setup_from_env.py +++ b/trustpoint/management/management/commands/auto_setup_from_env.py @@ -2,7 +2,6 @@ import ipaddress import os -import sys from pathlib import Path from typing import Any From 1417ea1664829eb8c18600ee8b1451bb0cc1dee2 Mon Sep 17 00:00:00 2001 From: florianhandke Date: Wed, 17 Jun 2026 13:58:38 +0200 Subject: [PATCH 11/18] fix failing tests --- trustpoint/management/tests/test_commands.py | 18 +++- trustpoint/trustpoint/tests/test_settings.py | 94 ++++++++++++++++---- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/trustpoint/management/tests/test_commands.py b/trustpoint/management/tests/test_commands.py index 85155f01f..48226ee3a 100644 --- a/trustpoint/management/tests/test_commands.py +++ b/trustpoint/management/tests/test_commands.py @@ -156,14 +156,21 @@ def test_inittrustpoint_creates_app_version( class StartupManagerCommandTest(TestCase): """Test suite for startup_manager command.""" + @patch('management.management.commands.startup_manager.Path') @patch('management.management.commands.startup_manager.StartupStrategySelector') @patch('management.management.commands.startup_manager.StartupContextBuilder') def test_startup_manager_db_not_initialized( self, mock_builder: MagicMock, - mock_selector: MagicMock + mock_selector: MagicMock, + mock_path_class: MagicMock ) -> None: """Test startup_manager when database is not initialized.""" + # Mock .env file existence check + mock_env_path = Mock() + mock_env_path.exists.return_value = True + mock_path_class.return_value = mock_env_path + # Simulate ProgrammingError when querying AppVersion with patch('management.management.commands.startup_manager.AppVersion.objects.first') as mock_first: from django.db.utils import ProgrammingError @@ -186,14 +193,21 @@ def test_startup_manager_db_not_initialized( ) mock_strategy.execute.assert_called_once_with(mock_context) + @patch('management.management.commands.startup_manager.Path') @patch('management.management.commands.startup_manager.StartupStrategySelector') @patch('management.management.commands.startup_manager.StartupContextBuilder') def test_startup_manager_db_initialized_no_version( self, mock_builder: MagicMock, - mock_selector: MagicMock + mock_selector: MagicMock, + mock_path_class: MagicMock ) -> None: """Test startup_manager when database is initialized but no version record.""" + # Mock .env file existence check + mock_env_path = Mock() + mock_env_path.exists.return_value = True + mock_path_class.return_value = mock_env_path + with patch('management.management.commands.startup_manager.AppVersion.objects.first') as mock_first: mock_first.return_value = None diff --git a/trustpoint/trustpoint/tests/test_settings.py b/trustpoint/trustpoint/tests/test_settings.py index 110f66f20..13f5c5429 100644 --- a/trustpoint/trustpoint/tests/test_settings.py +++ b/trustpoint/trustpoint/tests/test_settings.py @@ -27,9 +27,11 @@ def test_debug_setting(): assert settings.DEBUG is (not settings.DOCKER_CONTAINER), 'DEBUG should be the inverse of DOCKER_CONTAINER.' -def test_tp_urls_not_set_keeps_default_hosts_and_origins(monkeypatch): - """Ensure defaults remain unchanged when TP_URLS is not set.""" - monkeypatch.delenv('TP_URLS', raising=False) +def test_tls_addresses_not_set_keeps_default_hosts_and_origins(monkeypatch): + """Ensure defaults remain unchanged when TLS address variables are not set.""" + monkeypatch.delenv('TP_TLS_IPV4_ADDRESSES', raising=False) + monkeypatch.delenv('TP_TLS_IPV6_ADDRESSES', raising=False) + monkeypatch.delenv('TP_TLS_DNS_NAMES', raising=False) importlib.reload(settings) @@ -40,34 +42,94 @@ def test_tp_urls_not_set_keeps_default_hosts_and_origins(monkeypatch): assert 'http://127.0.0.1:8000' in settings.CSRF_TRUSTED_ORIGINS -def test_tp_urls_derives_allowed_hosts_and_csrf_origins(monkeypatch): - """Ensure TP_URLS entries are parsed into ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS.""" - monkeypatch.setenv('TP_URLS', 'trustpoint.local:8443, http://10.10.0.2, https://example.org') +def test_tls_ipv4_addresses_derives_allowed_hosts_and_csrf_origins(monkeypatch): + """Ensure TP_TLS_IPV4_ADDRESSES entries are parsed into ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS.""" + monkeypatch.setenv('TP_TLS_IPV4_ADDRESSES', '10.10.0.2, 192.168.1.100') + monkeypatch.setenv('TP_HTTP_PORT', '8080') + monkeypatch.setenv('TP_HTTPS_PORT', '8443') + + importlib.reload(settings) + + assert '10.10.0.2' in settings.ALLOWED_HOSTS + assert '192.168.1.100' in settings.ALLOWED_HOSTS + + assert 'http://10.10.0.2:8080' in settings.CSRF_TRUSTED_ORIGINS + assert 'https://10.10.0.2:8443' in settings.CSRF_TRUSTED_ORIGINS + assert 'http://192.168.1.100:8080' in settings.CSRF_TRUSTED_ORIGINS + assert 'https://192.168.1.100:8443' in settings.CSRF_TRUSTED_ORIGINS + + +def test_tls_ipv6_addresses_derives_allowed_hosts_and_csrf_origins(monkeypatch): + """Ensure TP_TLS_IPV6_ADDRESSES entries are parsed into ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS.""" + monkeypatch.setenv('TP_TLS_IPV6_ADDRESSES', 'fe80::1, 2001:db8::1') + monkeypatch.setenv('TP_HTTP_PORT', '80') + monkeypatch.setenv('TP_HTTPS_PORT', '443') + + importlib.reload(settings) + + assert 'fe80::1' in settings.ALLOWED_HOSTS + assert '2001:db8::1' in settings.ALLOWED_HOSTS + + assert 'http://[fe80::1]' in settings.CSRF_TRUSTED_ORIGINS + assert 'https://[fe80::1]' in settings.CSRF_TRUSTED_ORIGINS + assert 'http://[2001:db8::1]' in settings.CSRF_TRUSTED_ORIGINS + assert 'https://[2001:db8::1]' in settings.CSRF_TRUSTED_ORIGINS + + +def test_tls_dns_names_derives_allowed_hosts_and_csrf_origins(monkeypatch): + """Ensure TP_TLS_DNS_NAMES entries are parsed into ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS.""" + monkeypatch.setenv('TP_TLS_DNS_NAMES', 'trustpoint.local, example.org') + monkeypatch.setenv('TP_HTTP_PORT', '8080') + monkeypatch.setenv('TP_HTTPS_PORT', '8443') importlib.reload(settings) assert 'trustpoint.local' in settings.ALLOWED_HOSTS assert '.trustpoint.local' in settings.ALLOWED_HOSTS - assert '10.10.0.2' in settings.ALLOWED_HOSTS assert 'example.org' in settings.ALLOWED_HOSTS + assert 'http://trustpoint.local:8080' in settings.CSRF_TRUSTED_ORIGINS assert 'https://trustpoint.local:8443' in settings.CSRF_TRUSTED_ORIGINS - assert 'http://10.10.0.2' in settings.CSRF_TRUSTED_ORIGINS - assert 'https://example.org' in settings.CSRF_TRUSTED_ORIGINS + assert 'http://example.org:8080' in settings.CSRF_TRUSTED_ORIGINS + assert 'https://example.org:8443' in settings.CSRF_TRUSTED_ORIGINS -def test_tp_urls_deduplicates_hosts_and_origins(monkeypatch): - """Ensure repeated TP_URLS values do not create duplicate entries.""" - monkeypatch.setenv( - 'TP_URLS', - ' https://dup.local:9443,https://dup.local:9443 , dup.local:9443 ', - ) +def test_tls_dns_names_adds_wildcard_for_local_domains(monkeypatch): + """Ensure .local domains get wildcard subdomain entries in ALLOWED_HOSTS.""" + monkeypatch.setenv('TP_TLS_DNS_NAMES', 'trustpoint.local, other.local') + + importlib.reload(settings) + + assert 'trustpoint.local' in settings.ALLOWED_HOSTS + assert '.trustpoint.local' in settings.ALLOWED_HOSTS + assert 'other.local' in settings.ALLOWED_HOSTS + assert '.other.local' in settings.ALLOWED_HOSTS + + +def test_tls_addresses_deduplicates_hosts_and_origins(monkeypatch): + """Ensure repeated TLS address values do not create duplicate entries.""" + monkeypatch.setenv('TP_TLS_IPV4_ADDRESSES', '10.10.0.2, 10.10.0.2') + monkeypatch.setenv('TP_TLS_DNS_NAMES', 'dup.local, dup.local') importlib.reload(settings) + assert settings.ALLOWED_HOSTS.count('10.10.0.2') == 1 assert settings.ALLOWED_HOSTS.count('dup.local') == 1 assert settings.ALLOWED_HOSTS.count('.dup.local') == 1 - assert settings.CSRF_TRUSTED_ORIGINS.count('https://dup.local:9443') == 1 + + +def test_tls_addresses_handles_default_ports(monkeypatch): + """Ensure default ports (80/443) are omitted from CSRF_TRUSTED_ORIGINS.""" + monkeypatch.setenv('TP_TLS_IPV4_ADDRESSES', '10.10.0.2') + monkeypatch.setenv('TP_HTTP_PORT', '80') + monkeypatch.setenv('TP_HTTPS_PORT', '443') + + importlib.reload(settings) + + assert 'http://10.10.0.2' in settings.CSRF_TRUSTED_ORIGINS + assert 'https://10.10.0.2' in settings.CSRF_TRUSTED_ORIGINS + assert 'http://10.10.0.2:80' not in settings.CSRF_TRUSTED_ORIGINS + assert 'https://10.10.0.2:443' not in settings.CSRF_TRUSTED_ORIGINS def test_env_bool_uses_default_when_variable_is_missing(monkeypatch): From 3a498a20c4b9fe9ae0fa8175880159f67f9d6b37 Mon Sep 17 00:00:00 2001 From: Christian Schwinne Date: Fri, 19 Jun 2026 08:43:57 +0200 Subject: [PATCH 12/18] Replace TP_URLS with TLS URL env vars in tp_setup.sh --- tp_wizard.sh | 70 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/tp_wizard.sh b/tp_wizard.sh index 84525b2d9..6e4f823e6 100755 --- a/tp_wizard.sh +++ b/tp_wizard.sh @@ -27,9 +27,9 @@ MAILPIT_IMAGE="axllent/mailpit:v1.27" SFTPGO_IMAGE="drakkan/sftpgo:2.6.x-slim" WF2_WORKER_NAME="trustpoint-worker" -# Fixed trustpoint ports -APP_HTTP_HOST=80 -APP_HTTPS_HOST=443 +# Trustpoint ports +DEF_TP_HTTP_PORT=80 +DEF_TP_HTTPS_PORT=443 # PostgreSQL defaults DEF_DB_NAME="${POSTGRES_DB:-trustpoint_db}" @@ -38,7 +38,9 @@ DEF_DB_PASS="${DATABASE_PASSWORD:-testing321}" DEF_DB_PORT="${DATABASE_PORT:-5432}" DEF_DB_HOST="${DATABASE_HOST:-postgres}" DEF_DB_HOST_INTERNAL="postgres" # container name/hostname -DEF_TP_URLS="${TP_URLS:-trustpoint.local}" +DEF_TP_TLS_DNS_NAMES="${TP_TLS_DNS_NAMES:-trustpoint.local}" +DEF_TP_TLS_IPV4_ADDRESSES="${TP_TLS_IPV4_ADDRESSES:-}" +DEF_TP_TLS_IPV6_ADDRESSES="${TP_TLS_IPV6_ADDRESSES:-}" # Mailpit defaults DEF_MAILPIT_SMTP_PORT=1025 @@ -177,7 +179,9 @@ sync_env_file(){ upsert_env_var "DATABASE_PASSWORD" "$env_db_pass" upsert_env_var "DATABASE_HOST" "$env_db_host" upsert_env_var "DATABASE_PORT" "$env_db_port" - upsert_env_var "TP_URLS" "$TP_URLS_VALUE" + upsert_env_var "TP_TLS_DNS_NAMES" "$TP_TLS_DNS_NAMES_VALUE" + upsert_env_var "TP_TLS_IPV4_ADDRESSES" "$TP_TLS_IPV4_ADDRESSES_VALUE" + upsert_env_var "TP_TLS_IPV6_ADDRESSES" "$TP_TLS_IPV6_ADDRESSES_VALUE" chmod 600 "$ENV_FILE" 2>/dev/null || true ok "Updated ${ENV_FILE}" } @@ -197,7 +201,9 @@ APP_DB_PORT="$DB_PORT" APP_DB_NAME="$DB_NAME" APP_DB_USER="$DB_USER" APP_DB_PASS="$DEF_DB_PASS" -TP_URLS_VALUE="$DEF_TP_URLS" +TP_TLS_DNS_NAMES_VALUE="$DEF_TP_TLS_DNS_NAMES" +TP_TLS_IPV4_ADDRESSES_VALUE="$DEF_TP_TLS_IPV4_ADDRESSES" +TP_TLS_IPV6_ADDRESSES_VALUE="$DEF_TP_TLS_IPV6_ADDRESSES" MAILPIT_SMTP_PORT="$DEF_MAILPIT_SMTP_PORT" MAILPIT_UI_PORT="$DEF_MAILPIT_UI_PORT" @@ -286,10 +292,16 @@ step_app_db_binding(){ fi } -step_trustpoint_urls(){ +step_trustpoint_tls_identifiers(){ $EN_APP || return 0 - ask "Trustpoint reachable hostnames/IPs (comma-separated, no protocol)" "$TP_URLS_VALUE" - TP_URLS_VALUE="$REPLY" + ask "Trustpoint TLS DNS names (comma-separated, no protocol)" "$TP_TLS_DNS_NAMES_VALUE" + TP_TLS_DNS_NAMES_VALUE="$REPLY" + + ask "Trustpoint TLS IPv4 addresses (comma-separated, optional)" "$TP_TLS_IPV4_ADDRESSES_VALUE" + TP_TLS_IPV4_ADDRESSES_VALUE="$REPLY" + + ask "Trustpoint TLS IPv6 addresses (comma-separated, optional)" "$TP_TLS_IPV6_ADDRESSES_VALUE" + TP_TLS_IPV6_ADDRESSES_VALUE="$REPLY" } step_helpers(){ @@ -351,7 +363,9 @@ show_plan(){ printf "%-22s %s\n" "trustpoint DB name:" "$APP_DB_NAME" printf "%-22s %s\n" "trustpoint DB user:" "$APP_DB_USER" printf "%-22s %s\n" "trustpoint DB pass:" "$(mask "$APP_DB_PASS")" - printf "%-22s %s\n" "trustpoint URLs:" "$TP_URLS_VALUE" + printf "%-22s %s\n" "TLS DNS names:" "$TP_TLS_DNS_NAMES_VALUE" + printf "%-22s %s\n" "TLS IPv4 addresses:" "${TP_TLS_IPV4_ADDRESSES_VALUE:-(none)}" + printf "%-22s %s\n" "TLS IPv6 addresses:" "${TP_TLS_IPV6_ADDRESSES_VALUE:-(none)}" fi echo printf "%-22s %s\n" "Mailpit enabled:" "$EN_MAILPIT" @@ -511,7 +525,9 @@ DATABASE_USER=${APP_DB_USER} DATABASE_PASSWORD=${APP_DB_PASS} DATABASE_HOST=${APP_DB_HOST} DATABASE_PORT=${APP_DB_PORT} -TP_URLS=${TP_URLS_VALUE} +TP_TLS_DNS_NAMES=${TP_TLS_DNS_NAMES_VALUE} +TP_TLS_IPV4_ADDRESSES=${TP_TLS_IPV4_ADDRESSES_VALUE} +TP_TLS_IPV6_ADDRESSES=${TP_TLS_IPV6_ADDRESSES_VALUE} TRUSTPOINT_SERVICE_ROLE=worker WORKFLOWS2_WORKER_ID=${WF2_WORKER_NAME} WORKFLOWS2_WORKER_LEASE=${WF2_WORKER_LEASE} @@ -536,8 +552,8 @@ start_app(){ local name="trustpoint" stop_one "$name" # die early if 80/443 are busy - if port_in_use "$APP_HTTP_HOST"; then die "Host port ${APP_HTTP_HOST} is in use (trustpoint HTTP)."; fi - if port_in_use "$APP_HTTPS_HOST"; then die "Host port ${APP_HTTPS_HOST} is in use (trustpoint HTTPS)."; fi + if port_in_use "$DEF_TP_HTTP_PORT"; then die "Host port ${DEF_TP_HTTP_PORT} is in use (trustpoint HTTP)."; fi + if port_in_use "$DEF_TP_HTTPS_PORT"; then die "Host port ${DEF_TP_HTTPS_PORT} is in use (trustpoint HTTPS)."; fi log "Starting trustpoint..." local smtp_env=() @@ -545,14 +561,16 @@ start_app(){ smtp_env+=( -e "EMAIL_HOST=mailpit" -e "EMAIL_PORT=1025" -e "EMAIL_USE_TLS=0" -e "EMAIL_USE_SSL=0" -e "DEFAULT_FROM_EMAIL=no-reply@trustpoint.local" ) fi docker run -d --name "$name" --network "$NET" \ - -p "${APP_HTTP_HOST}:80" \ - -p "${APP_HTTPS_HOST}:443" \ + -p "${DEF_TP_HTTP_PORT}:80" \ + -p "${DEF_TP_HTTPS_PORT}:443" \ -e "POSTGRES_DB=$APP_DB_NAME" \ -e "DATABASE_USER=$APP_DB_USER" \ -e "DATABASE_PASSWORD=$APP_DB_PASS" \ -e "DATABASE_HOST=$APP_DB_HOST" \ -e "DATABASE_PORT=$APP_DB_PORT" \ - -e "TP_URLS=$TP_URLS_VALUE" \ + -e "TP_TLS_DNS_NAMES=$TP_TLS_DNS_NAMES_VALUE" \ + -e "TP_TLS_IPV4_ADDRESSES=$TP_TLS_IPV4_ADDRESSES_VALUE" \ + -e "TP_TLS_IPV6_ADDRESSES=$TP_TLS_IPV6_ADDRESSES_VALUE" \ ${smtp_env[@]+"${smtp_env[@]}"} \ "$APP_IMAGE" >/dev/null } @@ -600,9 +618,9 @@ await_readiness(){ echo fi if $EN_APP; then - echo "Waiting (<= ${READINESS_TIMEOUT}s) for trustpoint HTTP on localhost:${APP_HTTP_HOST} ..." + echo "Waiting (<= ${READINESS_TIMEOUT}s) for trustpoint HTTP on localhost:${DEF_TP_HTTP_PORT} ..." while (( $(date +%s) < deadline )); do - if tcp_check 127.0.0.1 "$APP_HTTP_HOST" 1; then ok "trustpoint reachable on :$APP_HTTP_HOST"; break; fi + if tcp_check 127.0.0.1 "$DEF_TP_HTTP_PORT" 1; then ok "trustpoint reachable on :$DEF_TP_HTTP_PORT"; break; fi printf "."; sleep 1 done echo @@ -791,7 +809,7 @@ show_runtime_status(){ echo if exists trustpoint; then - local http_port https_port db_host db_port db_name db_user db_pass tp_urls + local http_port https_port db_host db_port db_name db_user db_pass tp_tls_dns_names tp_tls_ipv4_addresses tp_tls_ipv6_addresses http_port="$(container_host_port trustpoint 80/tcp)" https_port="$(container_host_port trustpoint 443/tcp)" db_host="$(container_env trustpoint DATABASE_HOST)" @@ -799,11 +817,15 @@ show_runtime_status(){ db_name="$(container_env trustpoint POSTGRES_DB)" db_user="$(container_env trustpoint DATABASE_USER)" db_pass="$(container_env trustpoint DATABASE_PASSWORD)" - tp_urls="$(container_env trustpoint TP_URLS)" + tp_tls_dns_names="$(container_env trustpoint TP_TLS_DNS_NAMES)" + tp_tls_ipv4_addresses="$(container_env trustpoint TP_TLS_IPV4_ADDRESSES)" + tp_tls_ipv6_addresses="$(container_env trustpoint TP_TLS_IPV6_ADDRESSES)" [[ -n "$http_port" ]] && printf "%-22s %s\n" "trustpoint HTTP:" "http://localhost:${http_port}" [[ -n "$https_port" ]] && printf "%-22s %s\n" "trustpoint HTTPS:" "https://localhost:${https_port}" - [[ -n "$tp_urls" ]] && printf "%-22s %s\n" "trustpoint URLs:" "${tp_urls}" + [[ -n "$tp_tls_dns_names" ]] && printf "%-22s %s\n" "TLS DNS names:" "${tp_tls_dns_names}" + [[ -n "$tp_tls_ipv4_addresses" ]] && printf "%-22s %s\n" "TLS IPv4 addresses:" "${tp_tls_ipv4_addresses}" + [[ -n "$tp_tls_ipv6_addresses" ]] && printf "%-22s %s\n" "TLS IPv6 addresses:" "${tp_tls_ipv6_addresses}" printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings" if [[ -n "$db_host" || -n "$db_port" || -n "$db_name" || -n "$db_user" ]]; then printf "%-22s %s\n" "DB connect:" "host=${db_host:-?} port=${db_port:-?} db=${db_name:-?} user=${db_user:-?} pass=$(mask "${db_pass:-}")" @@ -860,7 +882,9 @@ final_summary(){ echo if $EN_APP; then printf "%-22s %s\n" "trustpoint:" "http://localhost:80 | https://localhost:443" - printf "%-22s %s\n" "trustpoint URLs:" "$TP_URLS_VALUE" + printf "%-22s %s\n" "TLS DNS names:" "$TP_TLS_DNS_NAMES_VALUE" + printf "%-22s %s\n" "TLS IPv4 addresses:" "${TP_TLS_IPV4_ADDRESSES_VALUE:-(none)}" + printf "%-22s %s\n" "TLS IPv6 addresses:" "${TP_TLS_IPV6_ADDRESSES_VALUE:-(none)}" printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings (default: auto)" fi if $DB_INTERNAL; then @@ -907,7 +931,7 @@ wizard(){ step_enable_postgres step_postgres_config step_app_db_binding - step_trustpoint_urls + step_trustpoint_tls_identifiers step_helpers step_workflows2_worker show_plan From 611047601b55777ec61bfa2a6701b5a26841b4cb Mon Sep 17 00:00:00 2001 From: Christian Schwinne Date: Fri, 19 Jun 2026 08:46:23 +0200 Subject: [PATCH 13/18] Update tp_wizard.sh --- tp_wizard.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tp_wizard.sh b/tp_wizard.sh index 6e4f823e6..1b00db453 100755 --- a/tp_wizard.sh +++ b/tp_wizard.sh @@ -38,6 +38,8 @@ DEF_DB_PASS="${DATABASE_PASSWORD:-testing321}" DEF_DB_PORT="${DATABASE_PORT:-5432}" DEF_DB_HOST="${DATABASE_HOST:-postgres}" DEF_DB_HOST_INTERNAL="postgres" # container name/hostname + +# Trustpoint TLS URLs (also used for allowed hosts and CSRF origins) DEF_TP_TLS_DNS_NAMES="${TP_TLS_DNS_NAMES:-trustpoint.local}" DEF_TP_TLS_IPV4_ADDRESSES="${TP_TLS_IPV4_ADDRESSES:-}" DEF_TP_TLS_IPV6_ADDRESSES="${TP_TLS_IPV6_ADDRESSES:-}" @@ -292,7 +294,7 @@ step_app_db_binding(){ fi } -step_trustpoint_tls_identifiers(){ +step_trustpoint_tls_urls(){ $EN_APP || return 0 ask "Trustpoint TLS DNS names (comma-separated, no protocol)" "$TP_TLS_DNS_NAMES_VALUE" TP_TLS_DNS_NAMES_VALUE="$REPLY" @@ -931,7 +933,7 @@ wizard(){ step_enable_postgres step_postgres_config step_app_db_binding - step_trustpoint_tls_identifiers + step_trustpoint_tls_urls step_helpers step_workflows2_worker show_plan From 4ae11f263565d2b5b893978ba45006baa2b45519 Mon Sep 17 00:00:00 2001 From: BytesWelder Date: Mon, 22 Jun 2026 11:31:57 +0200 Subject: [PATCH 14/18] initial split --- scripts/tp_wizard/README.md | 49 + scripts/tp_wizard/bootstrap.sh | 30 + scripts/tp_wizard/cli.sh | 84 ++ scripts/tp_wizard/commands/down.sh | 16 + scripts/tp_wizard/commands/logs.sh | 15 + scripts/tp_wizard/commands/nuke.sh | 19 + scripts/tp_wizard/commands/status.sh | 4 + scripts/tp_wizard/commands/up.sh | 40 + scripts/tp_wizard/defaults.sh | 54 + scripts/tp_wizard/legacy.sh | 976 ++++++++++++++++++ scripts/tp_wizard/runtime.sh | 56 + scripts/tp_wizard/services/mailpit.sh | 71 ++ scripts/tp_wizard/services/postgres.sh | 40 + scripts/tp_wizard/services/sftpgo.sh | 155 +++ scripts/tp_wizard/services/trustpoint.sh | 126 +++ .../tp_wizard/services/workflows2_worker.sh | 61 ++ scripts/tp_wizard/state.sh | 33 + scripts/tp_wizard/summary.sh | 177 ++++ scripts/tp_wizard/wizard.sh | 15 + tp_wizard.sh | 976 +----------------- 20 files changed, 2026 insertions(+), 971 deletions(-) create mode 100644 scripts/tp_wizard/README.md create mode 100644 scripts/tp_wizard/bootstrap.sh create mode 100644 scripts/tp_wizard/cli.sh create mode 100644 scripts/tp_wizard/commands/down.sh create mode 100644 scripts/tp_wizard/commands/logs.sh create mode 100644 scripts/tp_wizard/commands/nuke.sh create mode 100644 scripts/tp_wizard/commands/status.sh create mode 100644 scripts/tp_wizard/commands/up.sh create mode 100644 scripts/tp_wizard/defaults.sh create mode 100755 scripts/tp_wizard/legacy.sh create mode 100644 scripts/tp_wizard/runtime.sh create mode 100644 scripts/tp_wizard/services/mailpit.sh create mode 100644 scripts/tp_wizard/services/postgres.sh create mode 100644 scripts/tp_wizard/services/sftpgo.sh create mode 100644 scripts/tp_wizard/services/trustpoint.sh create mode 100644 scripts/tp_wizard/services/workflows2_worker.sh create mode 100644 scripts/tp_wizard/state.sh create mode 100644 scripts/tp_wizard/summary.sh create mode 100644 scripts/tp_wizard/wizard.sh diff --git a/scripts/tp_wizard/README.md b/scripts/tp_wizard/README.md new file mode 100644 index 000000000..9569778c8 --- /dev/null +++ b/scripts/tp_wizard/README.md @@ -0,0 +1,49 @@ +# tp_wizard + +`tp_wizard.sh` is the developer-facing setup helper for the local trustpoint Docker stack. + +It can run the interactive setup wizard or manage selected services: trustpoint, PostgreSQL, Mailpit, SFTPGo, and the optional workflows2 worker. + +## Usage + +Run from the repository root: + +```bash +./tp_wizard.sh +./tp_wizard.sh up [demo|trustpoint|db|mail|sftp|worker] [--nowait] +./tp_wizard.sh down [demo|trustpoint|db|mail|sftp|worker] +./tp_wizard.sh logs [trustpoint|db|mail|sftp|worker] +./tp_wizard.sh status +./tp_wizard.sh nuke +``` + +## Design + +The root `tp_wizard.sh` is only the public entrypoint. The implementation lives in `scripts/tp_wizard/`. + +```text +defaults.sh constants and default values +state.sh mutable wizard/runtime state +cli.sh argument parsing and dispatch +wizard.sh interactive wizard flow +runtime.sh shared start/wait/provision/summary orchestration +summary.sh plan, status, and final summary output +lib/ generic helpers +services/ service-specific prompt/start/wait/provision logic +commands/ command handlers +``` + +Dependency direction: + +```text +cli -> commands -> runtime -> services -> lib +wizard -> runtime -> services -> lib +``` + +Rules: + +- `lib/` must not call service or command functions. +- `services/` may use `lib/`, but should not parse CLI arguments. +- `commands/` should stay thin and delegate shared work to `runtime.sh`. +- `runtime.sh` owns orchestration used by both wizard and CLI mode. +- The root `tp_wizard.sh` should remain small and stable. diff --git a/scripts/tp_wizard/bootstrap.sh b/scripts/tp_wizard/bootstrap.sh new file mode 100644 index 000000000..f40b392fd --- /dev/null +++ b/scripts/tp_wizard/bootstrap.sh @@ -0,0 +1,30 @@ +# shellcheck shell=bash +# Load tp_wizard modules in dependency order. + +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/defaults.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/state.sh" + +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/lib/ui.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/lib/validation.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/lib/ports.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/lib/docker.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/lib/input.sh" + +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/summary.sh" + +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/postgres.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/trustpoint.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/mailpit.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/sftpgo.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/workflows2_worker.sh" + +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/runtime.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/wizard.sh" + +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/up.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/down.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/logs.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/status.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/nuke.sh" + +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/cli.sh" diff --git a/scripts/tp_wizard/cli.sh b/scripts/tp_wizard/cli.sh new file mode 100644 index 000000000..89d208d9d --- /dev/null +++ b/scripts/tp_wizard/cli.sh @@ -0,0 +1,84 @@ +usage(){ + cat <<'EOF2' +Commands: + (no command) Run interactive wizard + up [demo|trustpoint|db|mail|sftp|worker] [--nowait] + down [demo|trustpoint|db|mail|sftp|worker] + logs [trustpoint|db|mail|sftp|worker] + status + nuke + help + +Also supported (legacy): --only trustpoint|db|mail|sftp|worker|demo +EOF2 +} + + +map_only_to_flags(){ + case "$1" in + demo) ONLY_APP=true; ONLY_DB=true; ONLY_MAIL=true; ONLY_SFTP=true ;; + trustpoint|app) ONLY_APP=true ;; + db) ONLY_DB=true ;; + mail) ONLY_MAIL=true ;; + sftp) ONLY_SFTP=true ;; + worker) ONLY_WF2_WORKER=true ;; + *) die "Unknown target: $1 (use trustpoint|db|mail|sftp|worker|demo)";; + esac +} + + +set_targets_from_args(){ + local any=false + while [[ $# -gt 0 ]]; do + case "$1" in + demo|trustpoint|app|db|mail|sftp|worker) map_only_to_flags "$1"; any=true; shift ;; + --only) map_only_to_flags "${2:-}"; any=true; shift 2 ;; + --nowait) NOWAIT=true; shift ;; + *) die "Unknown option/target: $1" ;; + esac + done + if ! $any; then ONLY_APP=true; ONLY_DB=true; fi +} + + +tp_main(){ + local cmd="${1:-}" + + case "$cmd" in + "" ) + preflight + wizard + ;; + help|-h|--help) + usage + ;; + up) + preflight + shift || true + cmd_up "$@" + ;; + down) + preflight + shift || true + cmd_down "$@" + ;; + logs) + preflight + shift || true + cmd_logs "$@" + ;; + status) + preflight + shift || true + cmd_status "$@" + ;; + nuke) + preflight + cmd_nuke + ;; + *) + usage + die "Unknown command: $cmd" + ;; + esac +} diff --git a/scripts/tp_wizard/commands/down.sh b/scripts/tp_wizard/commands/down.sh new file mode 100644 index 000000000..8c9813444 --- /dev/null +++ b/scripts/tp_wizard/commands/down.sh @@ -0,0 +1,16 @@ +down_selected(){ + local done=false + $ONLY_APP && { stop_one trustpoint; stop_one "$WF2_WORKER_NAME"; done=true; } + $ONLY_DB && stop_one postgres && done=true + $ONLY_MAIL && stop_one mailpit && done=true + $ONLY_SFTP && stop_one sftpgo && done=true + $ONLY_WF2_WORKER && stop_one "$WF2_WORKER_NAME" && done=true + $done || { stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME"; } + ok "Stopped." +} + + +cmd_down(){ + set_targets_from_args "$@" + down_selected +} diff --git a/scripts/tp_wizard/commands/logs.sh b/scripts/tp_wizard/commands/logs.sh new file mode 100644 index 000000000..56544ab00 --- /dev/null +++ b/scripts/tp_wizard/commands/logs.sh @@ -0,0 +1,15 @@ +logs_selected(){ + local target="trustpoint" + $ONLY_DB && target="postgres" + $ONLY_MAIL && target="mailpit" + $ONLY_SFTP && target="sftpgo" + $ONLY_WF2_WORKER && target="$WF2_WORKER_NAME" + exists "$target" || die "Container not found: $target" + docker logs -f "$target" +} + + +cmd_logs(){ + set_targets_from_args "$@" + logs_selected +} diff --git a/scripts/tp_wizard/commands/nuke.sh b/scripts/tp_wizard/commands/nuke.sh new file mode 100644 index 000000000..0ef235556 --- /dev/null +++ b/scripts/tp_wizard/commands/nuke.sh @@ -0,0 +1,19 @@ +nuke_cmd(){ + read -r -p "Remove ALL project containers, network, DB volume, ./sftpgo-data, and ./workflow2Folder? [y/N] " a; [[ "${a}" == "y" ]] || exit 0 + read -r -p "Are you sure? This is destructive. [y/N] " b; [[ "${b}" == "y" ]] || exit 0 + mapfile -t project_volumes < <(collect_project_volumes) + stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME" + docker network rm "$NET" >/dev/null 2>&1 || true + for v in "${project_volumes[@]}"; do + [[ -n "$v" ]] || continue + docker volume rm "$v" >/dev/null 2>&1 || true + done + if [[ -d "$SFTPGO_ROOT" ]]; then rm -rf "$SFTPGO_ROOT"; fi + if [[ -d "$WF2_FOLDER" ]]; then rm -rf "$WF2_FOLDER"; fi + ok "Project resources removed." +} + + +cmd_nuke(){ + nuke_cmd +} diff --git a/scripts/tp_wizard/commands/status.sh b/scripts/tp_wizard/commands/status.sh new file mode 100644 index 000000000..cc343115e --- /dev/null +++ b/scripts/tp_wizard/commands/status.sh @@ -0,0 +1,4 @@ +cmd_status(){ + [[ $# -eq 0 ]] || die "status does not take targets. Use it without arguments." + show_runtime_status +} diff --git a/scripts/tp_wizard/commands/up.sh b/scripts/tp_wizard/commands/up.sh new file mode 100644 index 000000000..7728aa816 --- /dev/null +++ b/scripts/tp_wizard/commands/up.sh @@ -0,0 +1,40 @@ +configure_selected(){ + if $ONLY_DB; then + EN_PG=true + DB_INTERNAL=true + fi + $ONLY_MAIL && EN_MAILPIT=true + $ONLY_SFTP && EN_SFTPGO=true + + if $ONLY_APP; then + EN_APP=true + configure_app_image_prompt + EN_WF2_WORKER=$( + ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false + ) + elif $ONLY_WF2_WORKER; then + EN_WF2_WORKER=true + configure_app_image_prompt + fi + + if $ONLY_APP || $ONLY_WF2_WORKER; then + if $DB_INTERNAL; then + APP_DB_HOST="$DEF_DB_HOST_INTERNAL" + APP_DB_PORT=5432 + else + APP_DB_HOST="$DB_HOST" + APP_DB_PORT="$DB_PORT" + fi + APP_DB_NAME="$DB_NAME" + APP_DB_USER="$DB_USER" + APP_DB_PASS="$DB_PASS" + fi +} + + +cmd_up(){ + set_targets_from_args "$@" + configure_selected + ensure_network + runtime_start_selected +} diff --git a/scripts/tp_wizard/defaults.sh b/scripts/tp_wizard/defaults.sh new file mode 100644 index 000000000..f58424a99 --- /dev/null +++ b/scripts/tp_wizard/defaults.sh @@ -0,0 +1,54 @@ +# -------------------------- Constants & defaults ------------------------------ +PROJECT="trustpoint" +NET="${PROJECT}-net" +VOL_DB="${PROJECT}_postgres_data" + +# trustpoint image handling +TP_DOCKERFILE="docker/trustpoint/Dockerfile" +TP_REPO="trustpointproject/trustpoint" +APP_IMAGE="${TP_REPO}:latest" # overridden to trustpoint:local when BUILD_LOCAL=true +BUILD_LOCAL=false + +# Fixed images +PG_IMAGE="postgres:15.14" +MAILPIT_IMAGE="axllent/mailpit:v1.27" +SFTPGO_IMAGE="drakkan/sftpgo:2.6.x-slim" +WF2_WORKER_NAME="trustpoint-worker" + +# Fixed trustpoint ports +APP_HTTP_HOST=80 +APP_HTTPS_HOST=443 + +# PostgreSQL defaults +DEF_DB_NAME="trustpoint_db" +DEF_DB_USER="admin" +DEF_DB_PASS="testing321" +DEF_DB_PORT=5432 +DEF_DB_HOST_INTERNAL="postgres" # container name/hostname + +# Mailpit defaults +DEF_MAILPIT_SMTP_PORT=1025 +DEF_MAILPIT_UI_PORT=8025 + +# SFTPGo defaults +DEF_SFTPGO_SFTP_PORT=2222 +DEF_SFTPGO_WEB_PORT=8080 +DEF_SFTPGO_ADMIN_USER="admin" +DEF_SFTPGO_ADMIN_PASS="testing321" +SFTPGO_ROOT="${PWD}/sftpgo-data" +WF2_FOLDER="${PWD}/workflow2Folder" +WF2_WORKER_ENV_FILE="${WF2_FOLDER}/worker.env" +WF2_WORKER_README="${WF2_FOLDER}/README.txt" +DEF_WF2_WORKER_LEASE=30 +DEF_WF2_WORKER_BATCH=10 +DEF_WF2_WORKER_SLEEP=1 +MAILPIT_PROBE_TIMEOUT=20 + +# Timeouts +READINESS_TIMEOUT=90 +TLS_FP_TIMEOUT=150 + +# Optional backup user provisioning +SFTPGO_BACKUP_USER="tpbackup" +SFTPGO_BACKUP_PASS="testing321" +SFTPGO_BACKUP_HOME="" diff --git a/scripts/tp_wizard/legacy.sh b/scripts/tp_wizard/legacy.sh new file mode 100755 index 000000000..da9c5bd71 --- /dev/null +++ b/scripts/tp_wizard/legacy.sh @@ -0,0 +1,976 @@ +#!/usr/bin/env bash +# tp_wizard.sh — single-file wizard for trustpoint stack +set -euo pipefail + +# -------------------------- Constants & defaults ------------------------------ +PROJECT="trustpoint" +NET="${PROJECT}-net" +VOL_DB="${PROJECT}_postgres_data" + +# trustpoint image handling +TP_DOCKERFILE="docker/trustpoint/Dockerfile" +TP_REPO="trustpointproject/trustpoint" +APP_IMAGE="${TP_REPO}:latest" # overridden to trustpoint:local when BUILD_LOCAL=true +BUILD_LOCAL=false + +# Fixed images +PG_IMAGE="postgres:15.14" +MAILPIT_IMAGE="axllent/mailpit:v1.27" +SFTPGO_IMAGE="drakkan/sftpgo:2.6.x-slim" +WF2_WORKER_NAME="trustpoint-worker" + +# Fixed trustpoint ports +APP_HTTP_HOST=80 +APP_HTTPS_HOST=443 + +# PostgreSQL defaults +DEF_DB_NAME="trustpoint_db" +DEF_DB_USER="admin" +DEF_DB_PASS="testing321" +DEF_DB_PORT=5432 +DEF_DB_HOST_INTERNAL="postgres" # container name/hostname + +# Mailpit defaults +DEF_MAILPIT_SMTP_PORT=1025 +DEF_MAILPIT_UI_PORT=8025 + +# SFTPGo defaults +DEF_SFTPGO_SFTP_PORT=2222 +DEF_SFTPGO_WEB_PORT=8080 +DEF_SFTPGO_ADMIN_USER="admin" +DEF_SFTPGO_ADMIN_PASS="testing321" +SFTPGO_ROOT="${PWD}/sftpgo-data" +WF2_FOLDER="${PWD}/workflow2Folder" +WF2_WORKER_ENV_FILE="${WF2_FOLDER}/worker.env" +WF2_WORKER_README="${WF2_FOLDER}/README.txt" +DEF_WF2_WORKER_LEASE=30 +DEF_WF2_WORKER_BATCH=10 +DEF_WF2_WORKER_SLEEP=1 +MAILPIT_PROBE_TIMEOUT=20 + +# Timeouts +READINESS_TIMEOUT=90 +TLS_FP_TIMEOUT=150 + +# Optional backup user provisioning +SFTPGO_BACKUP_USER="tpbackup" +SFTPGO_BACKUP_PASS="testing321" +SFTPGO_BACKUP_HOME="" + +# -------------------------- UI helpers --------------------------------------- +bold(){ tput bold 2>/dev/null || true; } +rst(){ tput sgr0 2>/dev/null || true; } +ylw(){ tput setaf 3 2>/dev/null || true; } +grn(){ tput setaf 2 2>/dev/null || true; } +red(){ tput setaf 1 2>/dev/null || true; } +log(){ printf "%s\n" "$*" >&2; } +ok(){ log "$(grn)✔$(rst) $*"; } +warn(){ log "$(ylw)⚠$(rst) $*"; } +err(){ log "$(red)✖$(rst) $*"; } +die(){ err "$*"; exit 1; } +have(){ command -v "$1" >/dev/null 2>&1; } + +# -------------------------- Docker helpers ----------------------------------- +exists(){ docker ps -a --format '{{.Names}}' | grep -Fxq "$1"; } +running(){ docker ps --format '{{.Names}}' | grep -Fxq "$1"; } +ensure_network(){ docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET" >/dev/null; } +ensure_volumes(){ docker volume inspect "$VOL_DB" >/dev/null 2>&1 || docker volume create --label "tp.project=${PROJECT}" "$VOL_DB" >/dev/null; } +stop_one(){ local n="$1"; exists "$n" || return 0; running "$n" && docker stop "$n" >/dev/null || true; docker rm "$n" >/dev/null || true; } +container_state(){ local n="$1"; exists "$n" || { echo "absent"; return; }; docker inspect -f '{{.State.Status}}' "$n" 2>/dev/null || echo "unknown"; } +container_health(){ local n="$1" h=""; exists "$n" || { echo "-"; return; }; h="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$n" 2>/dev/null || true)"; echo "${h:--}"; } +container_image(){ local n="$1"; exists "$n" || { echo "-"; return; }; docker inspect -f '{{.Config.Image}}' "$n" 2>/dev/null || echo "-"; } +container_host_port(){ local n="$1" spec="$2" p=""; exists "$n" || return 0; p="$(docker port "$n" "$spec" 2>/dev/null | awk -F: 'NR==1 {print $NF}')" || true; echo "${p}"; } +container_env(){ local n="$1" key="$2"; exists "$n" || return 0; docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$n" 2>/dev/null | sed -n "s/^${key}=//p" | head -n1; } +container_volume_names(){ + local n="$1" + exists "$n" || return 0 + docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{println .Name}}{{end}}{{end}}' "$n" 2>/dev/null | sed '/^$/d' +} +collect_project_volumes(){ + { + echo "$VOL_DB" + container_volume_names trustpoint + container_volume_names postgres + container_volume_names mailpit + container_volume_names sftpgo + container_volume_names "$WF2_WORKER_NAME" + } | sed '/^$/d' | sort -u +} +print_container_status_row(){ + local n="$1" + printf "%-20s %-10s %-10s %s\n" "$n" "$(container_state "$n")" "$(container_health "$n")" "$(container_image "$n")" +} + +# quick TCP connect test (true if something accepts on host:port) +tcp_check(){ local host="$1" port="$2" ts=$(( $(date +%s) + ${3:-5} )); while (( $(date +%s) < ts )); do (exec 3<>"/dev/tcp/$host/$port") >/dev/null 2>&1 && { exec 3>&- 3<&-; return 0; }; sleep 1; done; return 1; } +port_in_use(){ tcp_check 127.0.0.1 "$1" 1; } + +# SFTPGo host web port resolver (avoid NGINX :80) +sftpgo_web_port(){ + local p; p="$(docker port sftpgo 8080/tcp 2>/dev/null | awk -F: '{print $2}')" || true + echo "${p:-$SFTPGO_WEB_PORT}" +} + +# -------------------------- Input helpers ------------------------------------ +ask(){ local prompt="$1" def="${2:-}"; if [[ -n "$def" ]]; then read -r -p "$(bold)${prompt}$(rst) [default: ${def}] > " REPLY || true; REPLY="${REPLY:-$def}"; else read -r -p "$(bold)${prompt}$(rst) > " REPLY || true; fi; } +ask_yes_no(){ local prompt="$1" def="${2:-y}" a; case "${def}" in y|yes) a="[Y/n]";; n|no) a="[y/N]";; *) a="[y/n]";; esac; read -r -p "$(bold)${prompt} ${a}$(rst) > " resp || true; resp="${resp:-$def}"; [[ "${resp}" =~ ^y ]]; } +ask_port(){ local prompt="$1" def="$2" p; while true; do ask "$prompt" "$def"; p="$REPLY"; [[ "$p" =~ ^[0-9]{1,5}$ ]] && (( p>0 && p<65536 )) && { echo "$p"; return; } ; warn "Invalid port. Enter 1..65535."; done; } +ask_free_port(){ local prompt="$1" def="$2" p; while true; do p="$(ask_port "$prompt" "$def")"; if port_in_use "$p"; then warn "Port ${p} is already in use on this host. Pick another."; else echo "$p"; return; fi; done; } +ask_user(){ local prompt="$1" def="$2" u; while true; do ask "$prompt" "$def"; u="$REPLY"; [[ "$u" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*$ ]] && { echo "$u"; return; } ; warn "Invalid username."; done; } +ask_dbname(){ local prompt="$1" def="$2" d; while true; do ask "$prompt" "$def"; d="$REPLY"; [[ "$d" =~ ^[A-Za-z0-9_-]+$ ]] && { echo "$d"; return; } ; warn "Invalid DB name."; done; } +ask_password(){ local prompt="$1" def="$2" pw; while true; do ask "$prompt" "$def"; pw="$REPLY"; (( ${#pw} >= 6 )) && { echo "$pw"; return; } ; warn "Password too short (min 6)."; done; } +mask(){ local s="$1" n=${#1}; (( n<=2 )) && { printf '%s' '**'; return; }; printf '%*s' $((n-2)) '' | tr ' ' '*'; printf '%s' "${s: -2}"; } + +# -------------------------- Wizard state ------------------------------------- +EN_APP=false; EN_PG=false; EN_MAILPIT=false; EN_SFTPGO=false; EN_WF2_WORKER=false + +DB_INTERNAL=true +DB_HOST="$DEF_DB_HOST_INTERNAL" # default host when internal +DB_PORT="$DEF_DB_PORT" # host-mapped port for convenience access +DB_NAME="$DEF_DB_NAME" +DB_USER="$DEF_DB_USER" +DB_PASS="$DEF_DB_PASS" + +APP_DB_HOST="$DB_HOST" +APP_DB_PORT="$DB_PORT" +APP_DB_NAME="$DB_NAME" +APP_DB_USER="$DB_USER" +APP_DB_PASS="$DEF_DB_PASS" + +MAILPIT_SMTP_PORT="$DEF_MAILPIT_SMTP_PORT" +MAILPIT_UI_PORT="$DEF_MAILPIT_UI_PORT" + +SFTPGO_SFTP_PORT="$DEF_SFTPGO_SFTP_PORT" +SFTPGO_WEB_PORT="$DEF_SFTPGO_WEB_PORT" +SFTPGO_ADMIN_USER="$DEF_SFTPGO_ADMIN_USER" +SFTPGO_ADMIN_PASS="$DEF_SFTPGO_ADMIN_PASS" + +TLS_FP_FOUND="" +TLS_FP_ELAPSED=0 +WF2_WORKER_LEASE="$DEF_WF2_WORKER_LEASE" +WF2_WORKER_BATCH="$DEF_WF2_WORKER_BATCH" +WF2_WORKER_SLEEP="$DEF_WF2_WORKER_SLEEP" + +# CLI target flags +ONLY_APP=false; ONLY_DB=false; ONLY_MAIL=false; ONLY_SFTP=false; ONLY_WF2_WORKER=false +NOWAIT=false + +# -------------------------- Steps -------------------------------------------- +preflight(){ have docker || die "docker not found"; docker version >/dev/null || die "docker daemon not reachable"; } + +step_enable_trustpoint(){ EN_APP=$(ask_yes_no "Enable trustpoint application container?" "y" && echo true || echo false); } + +step_trustpoint_source(){ + $EN_APP || return 0 + if ask_yes_no "Build trustpoint locally from ${TP_DOCKERFILE}? (No = pull from Docker Hub)" "y"; then + BUILD_LOCAL=true + APP_IMAGE="trustpoint:local" + else + BUILD_LOCAL=false + ask "Docker Hub image tag to pull (repository ${TP_REPO})" "latest"; local tag="$REPLY" + APP_IMAGE="${TP_REPO}:${tag}" + fi +} + +step_enable_postgres(){ + EN_PG=$(ask_yes_no "Start PostgreSQL container?" "y" && echo true || echo false) + DB_INTERNAL=$EN_PG + if $DB_INTERNAL; then DB_HOST="$DEF_DB_HOST_INTERNAL"; fi +} + +step_postgres_config(){ + if $DB_INTERNAL; then + DB_NAME="$(ask_dbname 'PostgreSQL database name' "$DB_NAME")" + DB_USER="$(ask_user 'PostgreSQL username' "$DB_USER")" + DB_PASS="$(ask_password 'PostgreSQL password' "$DB_PASS")" + # Immediate check: host port must be free to publish + DB_PORT="$(ask_free_port 'PostgreSQL host port (mapped)' "$DB_PORT")" + else + DB_HOST="$(ask 'External DB host/IP' '127.0.0.1'; echo "$REPLY")" + DB_PORT="$(ask_port 'External DB port' "$DB_PORT")" + DB_NAME="$(ask_dbname 'External DB database name' "$DB_NAME")" + DB_USER="$(ask_user 'External DB username' "$DB_USER")" + DB_PASS="$(ask_password 'External DB password' "$DB_PASS")" + fi +} + +step_app_db_binding(){ + $EN_APP || return 0 + if ask_yes_no "Should trustpoint reuse the PostgreSQL settings configured above?" "y"; then + APP_DB_NAME="$DB_NAME" + APP_DB_USER="$DB_USER" + APP_DB_PASS="$DB_PASS" + if $DB_INTERNAL; then + # Internal DB: always connect to the container directly + APP_DB_HOST="$DEF_DB_HOST_INTERNAL" + APP_DB_PORT=5432 + else + # External DB: use exactly what you entered + APP_DB_HOST="$DB_HOST" + APP_DB_PORT="$DB_PORT" + fi + else + local def_host def_port + if $DB_INTERNAL; then + def_host="$DEF_DB_HOST_INTERNAL"; def_port=5432 + else + def_host="$DB_HOST"; def_port="$DB_PORT" + fi + APP_DB_HOST="$(ask 'trustpoint DB host' "$def_host"; echo "$REPLY")" + APP_DB_PORT="$(ask_port 'trustpoint DB port' "$def_port")" + APP_DB_NAME="$(ask_dbname 'trustpoint DB name' "$DB_NAME")" + APP_DB_USER="$(ask_user 'trustpoint DB user' "$DB_USER")" + APP_DB_PASS="$(ask_password 'trustpoint DB password' "$DB_PASS")" + fi +} + +step_helpers(){ + EN_MAILPIT=$(ask_yes_no "Enable Mailpit (demo SMTP inbox)?" "n" && echo true || echo false) + if $EN_MAILPIT; then + MAILPIT_SMTP_PORT="$(ask_free_port 'Mailpit SMTP host port' "$MAILPIT_SMTP_PORT")" + MAILPIT_UI_PORT="$(ask_free_port 'Mailpit UI host port' "$MAILPIT_UI_PORT")" + fi + + EN_SFTPGO=$(ask_yes_no "Enable SFTPGo (demo SFTP + Web UI)?" "n" && echo true || echo false) + if $EN_SFTPGO; then + SFTPGO_SFTP_PORT="$(ask_free_port 'SFTPGo SFTP host port' "$SFTPGO_SFTP_PORT")" + SFTPGO_WEB_PORT="$(ask_free_port 'SFTPGo Web UI host port' "$SFTPGO_WEB_PORT")" + SFTPGO_ADMIN_USER="$(ask_user 'SFTPGo admin user' "$SFTPGO_ADMIN_USER")" + SFTPGO_ADMIN_PASS="$(ask_password 'SFTPGo admin password' "$SFTPGO_ADMIN_PASS")" + + # Mandatory backup user + ask_user "SFTPGo backup username" "$SFTPGO_BACKUP_USER"; SFTPGO_BACKUP_USER="$REPLY" + ask_password "SFTPGo backup password" "$SFTPGO_BACKUP_PASS"; SFTPGO_BACKUP_PASS="$REPLY" + SFTPGO_BACKUP_HOME="/srv/sftpgo/data/${SFTPGO_BACKUP_USER}" + ask "SFTPGo backup home (inside container)" "$SFTPGO_BACKUP_HOME"; SFTPGO_BACKUP_HOME="$REPLY" + fi +} + +step_workflows2_worker(){ + $EN_APP || return 0 + EN_WF2_WORKER=$( + ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false + ) +} + +show_plan(){ + echo + echo "==================== Configuration Summary (Planned) ====================" + printf "%-22s %s\n" "Network:" "$NET" + printf "%-22s %s\n" "DB Volume:" "$VOL_DB" + echo + printf "%-22s %s\n" "trustpoint enabled:" "$EN_APP" + if $EN_APP; then + if $BUILD_LOCAL; then + printf "%-22s %s\n" "App image:" "Build local → trustpoint:local" + else + printf "%-22s %s\n" "App image:" "Pull → ${APP_IMAGE}" + fi + printf "%-22s %s\n" "Host ports:" "80→80 (HTTP), 443→443 (HTTPS)" + fi + echo + printf "%-22s %s\n" "Internal Postgres:" "$DB_INTERNAL" + printf "%-22s %s\n" "DB host:" "$DB_HOST" + printf "%-22s %s\n" "DB host port:" "$DB_PORT" + printf "%-22s %s\n" "DB name:" "$DB_NAME" + printf "%-22s %s\n" "DB user:" "$DB_USER" + printf "%-22s %s\n" "DB pass:" "$(mask "$DB_PASS")" + echo + if $EN_APP; then + printf "%-22s %s\n" "trustpoint DB host:" "$APP_DB_HOST" + printf "%-22s %s\n" "trustpoint DB port:" "$APP_DB_PORT" + printf "%-22s %s\n" "trustpoint DB name:" "$APP_DB_NAME" + printf "%-22s %s\n" "trustpoint DB user:" "$APP_DB_USER" + printf "%-22s %s\n" "trustpoint DB pass:" "$(mask "$APP_DB_PASS")" + fi + echo + printf "%-22s %s\n" "Mailpit enabled:" "$EN_MAILPIT" + $EN_MAILPIT && printf "%-22s %s\n" "Mailpit ports:" "SMTP ${MAILPIT_SMTP_PORT}, UI ${MAILPIT_UI_PORT}" + echo + printf "%-22s %s\n" "workflows2 worker:" "$EN_WF2_WORKER" + $EN_WF2_WORKER && { + printf "%-22s %s\n" "Worker container:" "${WF2_WORKER_NAME}" + printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" + } + echo + printf "%-22s %s\n" "SFTPGo enabled:" "$EN_SFTPGO" + $EN_SFTPGO && { + printf "%-22s %s\n" "SFTPGo ports:" "SFTP ${SFTPGO_SFTP_PORT}, Web ${SFTPGO_WEB_PORT}" + printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" + printf "%-22s %s\n" "SFTP backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" + printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" + } + echo "=========================================================================" +} + +# -------------------------- Build/Pull & Start ------------------------------- +build_trustpoint_image(){ [[ -f "$TP_DOCKERFILE" ]] || log "Dockerfile not found: $TP_DOCKERFILE"; log "Building trustpoint image..."; docker build -f "$TP_DOCKERFILE" -t "trustpoint:local" .; } +pull_trustpoint_image(){ log "Pulling ${APP_IMAGE} ..."; docker pull "${APP_IMAGE}" >/dev/null; } +configure_app_image_prompt(){ + if ask_yes_no "Build trustpoint locally from ${TP_DOCKERFILE}? (No = pull from Docker Hub)" "y"; then + BUILD_LOCAL=true + APP_IMAGE="trustpoint:local" + else + BUILD_LOCAL=false + ask "Docker Hub image tag to pull (repository ${TP_REPO})" "latest" + local tag="$REPLY" + APP_IMAGE="${TP_REPO}:${tag}" + fi +} +resolve_app_image(){ + if ! $EN_APP && ! $EN_WF2_WORKER; then + return 0 + fi + if $BUILD_LOCAL; then + build_trustpoint_image + else + pull_trustpoint_image + fi +} +configure_selected(){ + if $ONLY_DB; then + EN_PG=true + DB_INTERNAL=true + fi + $ONLY_MAIL && EN_MAILPIT=true + $ONLY_SFTP && EN_SFTPGO=true + + if $ONLY_APP; then + EN_APP=true + configure_app_image_prompt + EN_WF2_WORKER=$( + ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false + ) + elif $ONLY_WF2_WORKER; then + EN_WF2_WORKER=true + configure_app_image_prompt + fi + + if $ONLY_APP || $ONLY_WF2_WORKER; then + if $DB_INTERNAL; then + APP_DB_HOST="$DEF_DB_HOST_INTERNAL" + APP_DB_PORT=5432 + else + APP_DB_HOST="$DB_HOST" + APP_DB_PORT="$DB_PORT" + fi + APP_DB_NAME="$DB_NAME" + APP_DB_USER="$DB_USER" + APP_DB_PASS="$DB_PASS" + fi +} + +start_postgres(){ + $DB_INTERNAL || return 0 + ensure_volumes + local name="postgres" + stop_one "$name" + # safety: host port must still be free (non-interactive runs) + if port_in_use "$DB_PORT"; then die "Host port ${DB_PORT} is already in use. Choose another port or stop the process using it."; fi + log "Starting PostgreSQL..." + docker run -d --name "$name" --network "$NET" \ + -p "${DB_PORT}:5432" \ + -v "${VOL_DB}:/var/lib/postgresql/data" \ + -e "POSTGRES_DB=$DB_NAME" \ + -e "POSTGRES_USER=$DB_USER" \ + -e "POSTGRES_PASSWORD=$DB_PASS" \ + "$PG_IMAGE" >/dev/null +} + +start_mailpit(){ + $EN_MAILPIT || return 0 + local name="mailpit" + stop_one "$name" + if port_in_use "$MAILPIT_SMTP_PORT"; then die "Host port ${MAILPIT_SMTP_PORT} in use (Mailpit SMTP)."; fi + if port_in_use "$MAILPIT_UI_PORT"; then die "Host port ${MAILPIT_UI_PORT} in use (Mailpit UI)."; fi + log "Starting Mailpit..." + docker run -d --name "$name" --network "$NET" \ + -p "${MAILPIT_SMTP_PORT}:1025" \ + -p "${MAILPIT_UI_PORT}:8025" \ + "$MAILPIT_IMAGE" >/dev/null +} + +start_sftpgo(){ + $EN_SFTPGO || return 0 + local name="sftpgo" + stop_one "$name" + + if port_in_use "$SFTPGO_SFTP_PORT"; then die "Host port ${SFTPGO_SFTP_PORT} in use (SFTPGo SFTP)."; fi + if port_in_use "$SFTPGO_WEB_PORT"; then die "Host port ${SFTPGO_WEB_PORT} in use (SFTPGo Web)."; fi + + mkdir -p "${SFTPGO_ROOT}/data" + if [[ -n "$SFTPGO_BACKUP_USER" ]]; then + mkdir -p "${SFTPGO_ROOT}/data/${SFTPGO_BACKUP_USER}" + fi + chown -R 1000:1000 "${SFTPGO_ROOT}" 2>/dev/null || true + + log "Starting SFTPGo with auto-created admin..." + docker run -d --name "$name" --network "$NET" \ + -p "${SFTPGO_SFTP_PORT}:2022" \ + -p "${SFTPGO_WEB_PORT}:8080" \ + -v "${SFTPGO_ROOT}:/srv/sftpgo" \ + -e SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN=true \ + -e SFTPGO_DEFAULT_ADMIN_USERNAME="$SFTPGO_ADMIN_USER" \ + -e SFTPGO_DEFAULT_ADMIN_PASSWORD="$SFTPGO_ADMIN_PASS" \ + -e SFTPGO_HTTPD__BINDINGS__0__ADDRESS="0.0.0.0" \ + -e SFTPGO_HTTPD__BINDINGS__0__ENABLE_REST_API=true \ + -e SFTPGO_HTTPD__BINDINGS__0__ENABLE_WEB_ADMIN=true \ + -e SFTPGO_HTTPD__BINDINGS__0__PORT=8080 \ + -e SFTPGO_SFTPD__BINDINGS__0__PORT=2022 \ + "$SFTPGO_IMAGE" >/dev/null +} + +prepare_workflows2_worker_folder(){ + $EN_WF2_WORKER || return 0 + mkdir -p "$WF2_FOLDER" + chmod 700 "$WF2_FOLDER" 2>/dev/null || true + cat > "$WF2_WORKER_README" </dev/null || true + + cat > "$WF2_WORKER_ENV_FILE" <> "$WF2_WORKER_ENV_FILE" </dev/null || true +} + +start_app(){ + $EN_APP || return 0 + local name="trustpoint" + stop_one "$name" + # die early if 80/443 are busy + if port_in_use "$APP_HTTP_HOST"; then die "Host port ${APP_HTTP_HOST} is in use (trustpoint HTTP)."; fi + if port_in_use "$APP_HTTPS_HOST"; then die "Host port ${APP_HTTPS_HOST} is in use (trustpoint HTTPS)."; fi + + log "Starting trustpoint..." + local smtp_env=() + if $EN_MAILPIT; then + smtp_env+=( -e "EMAIL_HOST=mailpit" -e "EMAIL_PORT=1025" -e "EMAIL_USE_TLS=0" -e "EMAIL_USE_SSL=0" -e "DEFAULT_FROM_EMAIL=no-reply@trustpoint.local" ) + fi + docker run -d --name "$name" --network "$NET" \ + -p "${APP_HTTP_HOST}:80" \ + -p "${APP_HTTPS_HOST}:443" \ + -e "POSTGRES_DB=$APP_DB_NAME" \ + -e "DATABASE_USER=$APP_DB_USER" \ + -e "DATABASE_PASSWORD=$APP_DB_PASS" \ + -e "DATABASE_HOST=$APP_DB_HOST" \ + -e "DATABASE_PORT=$APP_DB_PORT" \ + "${smtp_env[@]}" \ + "$APP_IMAGE" >/dev/null +} + +start_workflows2_worker(){ + $EN_WF2_WORKER || return 0 + local name="$WF2_WORKER_NAME" + stop_one "$name" + prepare_workflows2_worker_folder + log "Starting dedicated workflows2 worker..." + docker run -d --name "$name" --network "$NET" \ + --env-file "$WF2_WORKER_ENV_FILE" \ + "$APP_IMAGE" >/dev/null +} + +# -------------------------- Readiness & Provision ----------------------------- +await_sftpgo_ready(){ + $EN_SFTPGO || return 0 + local PORT; PORT="$(sftpgo_web_port)" + echo "Waiting (<= ${READINESS_TIMEOUT}s) for SFTPGo API on localhost:${PORT} ..." + local until=$(( $(date +%s) + READINESS_TIMEOUT )) + while (( $(date +%s) < until )); do + if have curl && [[ "$(curl -fsS "http://127.0.0.1:${PORT}/healthz" 2>/dev/null || true)" == "ok" ]]; then + ok "SFTPGo API healthy on :${PORT}" + return 0 + fi + if tcp_check 127.0.0.1 "$PORT" 1; then + ok "SFTPGo API port open on :${PORT}" + return 0 + fi + printf "."; sleep 1 + done + echo + warn "SFTPGo API not confirmed after ${READINESS_TIMEOUT}s" +} + +await_readiness(){ + local deadline=$(( $(date +%s) + READINESS_TIMEOUT )) + if $DB_INTERNAL; then + echo "Waiting (<= ${READINESS_TIMEOUT}s) for PostgreSQL on localhost:${DB_PORT} ..." + while (( $(date +%s) < deadline )); do + if tcp_check 127.0.0.1 "$DB_PORT" 1; then ok "PostgreSQL ready on :$DB_PORT"; break; fi + printf "."; sleep 1 + done + echo + fi + if $EN_APP; then + echo "Waiting (<= ${READINESS_TIMEOUT}s) for trustpoint HTTP on localhost:${APP_HTTP_HOST} ..." + while (( $(date +%s) < deadline )); do + if tcp_check 127.0.0.1 "$APP_HTTP_HOST" 1; then ok "trustpoint reachable on :$APP_HTTP_HOST"; break; fi + printf "."; sleep 1 + done + echo + fi + await_sftpgo_ready +} + +# ---- SFTPGo provisioning via REST ------------------------------------------- +upsert_virtual_folder(){ + local vf_name="$1" mapped="$2" + read -r -d '' VF_PAYLOAD <&2 + return 1 + fi + ok "Virtual folder '${vf_name}' → '${mapped}' ready." +} + +provision_sftpgo_backup_user(){ + $EN_SFTPGO || return 0 + have curl || { warn "curl not found on host; skipping SFTPGo user provisioning."; return 0; } + + [[ -z "${SFTPGO_BACKUP_HOME:-}" ]] && SFTPGO_BACKUP_HOME="/srv/sftpgo/data/${SFTPGO_BACKUP_USER}" + + local host_home="${SFTPGO_ROOT}${SFTPGO_BACKUP_HOME#/srv/sftpgo}" + mkdir -p "$host_home" + chown -R 1000:1000 "$host_home" 2>/dev/null || true + + local PORT; PORT="$(sftpgo_web_port)" + API="http://127.0.0.1:${PORT}" + + for _ in {1..30}; do + [[ "$(curl -fsS "${API}/healthz" 2>/dev/null || true)" == "ok" ]] && break + sleep 1 + done + + local token="" + for _ in 1 2 3; do + token="$(curl -fsS -u "${SFTPGO_ADMIN_USER}:${SFTPGO_ADMIN_PASS}" "${API}/api/v2/token" \ + | sed -nE 's/.*"access_token":"([^"]+)".*/\1/p')" || true + [[ -n "$token" ]] && break + sleep 1 + done + [[ -n "$token" ]] || { warn "Could not obtain SFTPGo admin token; skipping user provisioning."; return 0; } + + HDR=(-H "Authorization: Bearer ${token}" -H "Content-Type: application/json") + upsert_virtual_folder "trustpoint" "${SFTPGO_BACKUP_HOME}" || return 0 + + local code method url http + code="$(curl -s -o /dev/null -w '%{http_code}' "${HDR[@]}" "${API}/api/v2/users/${SFTPGO_BACKUP_USER}")" + if [[ "$code" == "200" ]]; then + method=PUT; url="${API}/api/v2/users/${SFTPGO_BACKUP_USER}" + else + method=POST; url="${API}/api/v2/users" + fi + + read -r -d '' payload <&2 + else + ok "SFTPGo user '${SFTPGO_BACKUP_USER}' provisioned; VF mounted at /upload." + fi +} + +# ---- TLS fingerprint wait ---------------------------------------------------- +extract_tls_fingerprint_once(){ + local logs="$1" + if [[ "$logs" =~ ([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2} ]]; then TLS_FP_FOUND="${BASH_REMATCH[0]}"; return 0; fi + if [[ "$logs" =~ ([0-9A-Fa-f]{64}) ]]; then TLS_FP_FOUND="${BASH_REMATCH[1]}"; return 0; fi + if [[ "$logs" =~ [Ss][Hh][Aa]-?256[:\ ]([A-Za-z0-9+/=_:-]{43,}) ]]; then TLS_FP_FOUND="SHA256:${BASH_REMATCH[1]}"; return 0; fi + return 1 +} + +wait_tls_fingerprint(){ + $EN_APP || { TLS_FP_ELAPSED=0; return 0; } + local start; start="$(date +%s)" + local start_iso; start_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + local end=$(( start + TLS_FP_TIMEOUT )) + while (( $(date +%s) < end )); do + local chunk + chunk="$(docker logs --since "$start_iso" trustpoint 2>/dev/null || true)" + if extract_tls_fingerprint_once "$chunk"; then + TLS_FP_ELAPSED=$(( $(date +%s) - start )) + return 0 + fi + sleep 3 + done + TLS_FP_ELAPSED=$(( $(date +%s) - start )) + return 1 +} + +mailpit_has_subject(){ + local subject="$1" + have curl || return 2 + local until=$(( $(date +%s) + MAILPIT_PROBE_TIMEOUT )) + local api="http://127.0.0.1:${MAILPIT_UI_PORT}/api/v1/messages" + while (( $(date +%s) < until )); do + if curl -fsS "$api" 2>/dev/null | grep -Fq "$subject"; then + return 0 + fi + sleep 1 + done + return 1 +} + +probe_mailpit_from_container(){ + local container="$1" label="$2" + exists "$container" || return 0 + + local subject="trustpoint wizard ${label} mailpit probe $(date +%s)" + log "Sending Mailpit probe email from ${label} container..." + + if ! docker exec -e "PROBE_SUBJECT=${subject}" "$container" bash -lc \ + 'cd /var/www/html/trustpoint && uv run trustpoint/manage.py shell -c '\''import os; from django.conf import settings; from django.core.mail import send_mail; subject=os.environ["PROBE_SUBJECT"]; send_mail(subject, "Trustpoint Mailpit probe.", getattr(settings, "DEFAULT_FROM_EMAIL", None), ["demo@trustpoint.local"], fail_silently=False)'\''' \ + >/dev/null 2>&1; then + warn "Mailpit probe failed from ${label} container." + return 1 + fi + + if ! have curl; then + warn "curl not found on host; Mailpit probe from ${label} was sent but API verification was skipped." + return 0 + fi + + if mailpit_has_subject "$subject"; then + ok "Mailpit received the ${label} probe email." + return 0 + fi + + warn "Mailpit SMTP accepted the ${label} probe email, but it did not appear in the Mailpit UI within ${MAILPIT_PROBE_TIMEOUT}s." + return 1 +} + +verify_mailpit_delivery(){ + $EN_MAILPIT || return 0 + $EN_APP && probe_mailpit_from_container trustpoint "web" || true + $EN_WF2_WORKER && probe_mailpit_from_container "$WF2_WORKER_NAME" "workflows2-worker" || true +} + +show_runtime_status(){ + local net_state="absent" vol_state="absent" + docker network inspect "$NET" >/dev/null 2>&1 && net_state="present" + docker volume inspect "$VOL_DB" >/dev/null 2>&1 && vol_state="present" + + echo + echo "=========================== Runtime Status (Live) ========================" + printf "%-22s %s\n" "Network:" "${NET} (${net_state})" + printf "%-22s %s\n" "DB volume:" "${VOL_DB} (${vol_state})" + printf "%-22s %s\n" "workflow2 folder:" "$([ -d "$WF2_FOLDER" ] && echo "${WF2_FOLDER} (present)" || echo "${WF2_FOLDER} (absent)")" + echo + printf "%-20s %-10s %-10s %s\n" "Container" "State" "Health" "Image" + print_container_status_row trustpoint + print_container_status_row postgres + print_container_status_row mailpit + print_container_status_row sftpgo + print_container_status_row "$WF2_WORKER_NAME" + echo + + if exists trustpoint; then + local http_port https_port db_host db_port db_name db_user db_pass + http_port="$(container_host_port trustpoint 80/tcp)" + https_port="$(container_host_port trustpoint 443/tcp)" + db_host="$(container_env trustpoint DATABASE_HOST)" + db_port="$(container_env trustpoint DATABASE_PORT)" + db_name="$(container_env trustpoint POSTGRES_DB)" + db_user="$(container_env trustpoint DATABASE_USER)" + db_pass="$(container_env trustpoint DATABASE_PASSWORD)" + + [[ -n "$http_port" ]] && printf "%-22s %s\n" "trustpoint HTTP:" "http://localhost:${http_port}" + [[ -n "$https_port" ]] && printf "%-22s %s\n" "trustpoint HTTPS:" "https://localhost:${https_port}" + printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings" + if [[ -n "$db_host" || -n "$db_port" || -n "$db_name" || -n "$db_user" ]]; then + printf "%-22s %s\n" "DB connect:" "host=${db_host:-?} port=${db_port:-?} db=${db_name:-?} user=${db_user:-?} pass=$(mask "${db_pass:-}")" + fi + fi + + if exists postgres; then + local pg_port + pg_port="$(container_host_port postgres 5432/tcp)" + [[ -n "$pg_port" ]] && printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${pg_port}" + fi + + if exists mailpit; then + local mailpit_ui mailpit_smtp + mailpit_ui="$(container_host_port mailpit 8025/tcp)" + mailpit_smtp="$(container_host_port mailpit 1025/tcp)" + [[ -n "$mailpit_ui" ]] && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${mailpit_ui}" + [[ -n "$mailpit_smtp" ]] && printf "%-22s %s\n" "Mailpit SMTP:" "localhost:${mailpit_smtp}" + fi + + if exists "$WF2_WORKER_NAME"; then + local worker_db worker_lease worker_batch worker_sleep + worker_db="$(container_env "$WF2_WORKER_NAME" DATABASE_HOST)" + worker_lease="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_LEASE)" + worker_batch="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_BATCH)" + worker_sleep="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_SLEEP)" + printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" + [[ -n "$worker_db" ]] && printf "%-22s %s\n" "worker DB host:" "${worker_db}" + [[ -n "$worker_lease" || -n "$worker_batch" || -n "$worker_sleep" ]] && \ + printf "%-22s %s\n" "worker tuning:" "lease=${worker_lease:-?} batch=${worker_batch:-?} sleep=${worker_sleep:-?}" + fi + + if exists sftpgo; then + local sftpgo_web sftpgo_sftp sftpgo_admin + sftpgo_web="$(container_host_port sftpgo 8080/tcp)" + sftpgo_sftp="$(container_host_port sftpgo 2022/tcp)" + sftpgo_admin="$(container_env sftpgo SFTPGO_DEFAULT_ADMIN_USERNAME)" + [[ -n "$sftpgo_web" ]] && printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${sftpgo_web}/web/admin" + [[ -n "$sftpgo_sftp" ]] && printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${sftpgo_sftp}" + [[ -n "$sftpgo_admin" ]] && printf "%-22s %s\n" "SFTPGo admin:" "${sftpgo_admin}" + printf "%-22s %s\n" "SFTPGo data dir:" "${SFTPGO_ROOT}" + fi + + echo "=========================================================================" +} + +# -------------------------- Summary ------------------------------------------ +final_summary(){ + echo + echo "========================= Runtime Summary (Actual) =======================" + printf "%-22s %s\n" "Network:" "$NET" + printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker)$' || true)" + echo + if $EN_APP; then + printf "%-22s %s\n" "trustpoint:" "http://localhost:80 | https://localhost:443" + printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings (default: auto)" + fi + if $DB_INTERNAL; then + printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${DB_PORT} (container port 5432)" + fi + if $EN_APP; then + printf "%-22s %s\n" "DB connect:" "host=${APP_DB_HOST} port=${APP_DB_PORT} db=${APP_DB_NAME} user=${APP_DB_USER} pass=$(mask "$APP_DB_PASS")" + fi + $EN_MAILPIT && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${MAILPIT_UI_PORT} (SMTP :${MAILPIT_SMTP_PORT})" + if $EN_WF2_WORKER; then + printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" + printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" + fi + if $EN_SFTPGO; then + local PORT; PORT="$(sftpgo_web_port)" + printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${PORT}/web/admin" + printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${SFTPGO_SFTP_PORT}" + printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" + printf "%-22s %s\n" "Backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" + printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" + printf "%-22s %s\n" "Backup URL:" "sftp://${SFTPGO_BACKUP_USER}:***@127.0.0.1:${SFTPGO_SFTP_PORT}/" + printf "%-22s %s\n" "Data dir:" "${SFTPGO_ROOT}" + fi + if $EN_APP; then + if [[ -n "$TLS_FP_FOUND" ]]; then + printf "%-22s %s\n" "TLS fingerprint:" "$TLS_FP_FOUND" + else + if $NOWAIT; then + printf "%-22s %s\n" "TLS fingerprint:" "skipped (NOWAIT)" + else + printf "%-22s %s\n" "TLS fingerprint:" "not found yet (polled ${TLS_FP_ELAPSED}s; timeout ${TLS_FP_TIMEOUT}s)" + fi + fi + fi + echo "=========================================================================" +} + +# -------------------------- High-level orchestration ------------------------- +wizard(){ + echo "$(bold)trustpoint Setup Wizard$(rst)" + ensure_network + step_enable_trustpoint + step_trustpoint_source + step_enable_postgres + step_postgres_config + step_app_db_binding + step_helpers + step_workflows2_worker + show_plan + ask_yes_no "Proceed with these settings?" "y" || { warn "Aborted by user."; exit 1; } + resolve_app_image + $DB_INTERNAL && ensure_volumes + start_postgres + start_mailpit + start_sftpgo + $EN_WF2_WORKER || stop_one "$WF2_WORKER_NAME" + start_app + start_workflows2_worker + $NOWAIT || await_readiness + $NOWAIT || provision_sftpgo_backup_user + $NOWAIT || verify_mailpit_delivery + $NOWAIT || wait_tls_fingerprint || true + final_summary +} + +# -------------------------- Service selection & CLI -------------------------- +usage(){ + cat <<'EOF2' +Commands: + (no command) Run interactive wizard + up [demo|trustpoint|db|mail|sftp|worker] [--nowait] + down [demo|trustpoint|db|mail|sftp|worker] + logs [trustpoint|db|mail|sftp|worker] + status + nuke + help + +Also supported (legacy): --only trustpoint|db|mail|sftp|worker|demo +EOF2 +} + +map_only_to_flags(){ + case "$1" in + demo) ONLY_APP=true; ONLY_DB=true; ONLY_MAIL=true; ONLY_SFTP=true ;; + trustpoint|app) ONLY_APP=true ;; + db) ONLY_DB=true ;; + mail) ONLY_MAIL=true ;; + sftp) ONLY_SFTP=true ;; + worker) ONLY_WF2_WORKER=true ;; + *) die "Unknown target: $1 (use trustpoint|db|mail|sftp|worker|demo)";; + esac +} + +set_targets_from_args(){ + local any=false + while [[ $# -gt 0 ]]; do + case "$1" in + demo|trustpoint|app|db|mail|sftp|worker) map_only_to_flags "$1"; any=true; shift ;; + --only) map_only_to_flags "${2:-}"; any=true; shift 2 ;; + --nowait) NOWAIT=true; shift ;; + *) die "Unknown option/target: $1" ;; + esac + done + if ! $any; then ONLY_APP=true; ONLY_DB=true; fi +} + +start_selected(){ + configure_selected + ensure_network + resolve_app_image + $ONLY_DB && { EN_PG=true; ensure_volumes; start_postgres; } + $ONLY_MAIL && { EN_MAILPIT=true; start_mailpit; } + $ONLY_SFTP && { EN_SFTPGO=true; start_sftpgo; } + $EN_WF2_WORKER || { $ONLY_APP && stop_one "$WF2_WORKER_NAME"; } + $ONLY_APP && start_app + $EN_WF2_WORKER && start_workflows2_worker + + $NOWAIT || await_readiness + $NOWAIT || provision_sftpgo_backup_user + $NOWAIT || verify_mailpit_delivery + $NOWAIT || wait_tls_fingerprint || true + final_summary +} + +down_selected(){ + local done=false + $ONLY_APP && { stop_one trustpoint; stop_one "$WF2_WORKER_NAME"; done=true; } + $ONLY_DB && stop_one postgres && done=true + $ONLY_MAIL && stop_one mailpit && done=true + $ONLY_SFTP && stop_one sftpgo && done=true + $ONLY_WF2_WORKER && stop_one "$WF2_WORKER_NAME" && done=true + $done || { stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME"; } + ok "Stopped." +} + +logs_selected(){ + local target="trustpoint" + $ONLY_DB && target="postgres" + $ONLY_MAIL && target="mailpit" + $ONLY_SFTP && target="sftpgo" + $ONLY_WF2_WORKER && target="$WF2_WORKER_NAME" + exists "$target" || die "Container not found: $target" + docker logs -f "$target" +} + +nuke_cmd(){ + read -r -p "Remove ALL project containers, network, DB volume, ./sftpgo-data, and ./workflow2Folder? [y/N] " a; [[ "${a}" == "y" ]] || exit 0 + read -r -p "Are you sure? This is destructive. [y/N] " b; [[ "${b}" == "y" ]] || exit 0 + mapfile -t project_volumes < <(collect_project_volumes) + stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME" + docker network rm "$NET" >/dev/null 2>&1 || true + for v in "${project_volumes[@]}"; do + [[ -n "$v" ]] || continue + docker volume rm "$v" >/dev/null 2>&1 || true + done + if [[ -d "$SFTPGO_ROOT" ]]; then rm -rf "$SFTPGO_ROOT"; fi + if [[ -d "$WF2_FOLDER" ]]; then rm -rf "$WF2_FOLDER"; fi + ok "Project resources removed." +} + +# -------------------------- Arg parsing & dispatch ---------------------------- +cmd="${1:-}" +preflight +case "$cmd" in + "" ) wizard ;; + help) usage ;; + up) + shift || true + set_targets_from_args "$@" + start_selected + ;; + down) + shift || true + set_targets_from_args "$@" + down_selected + ;; + logs) + shift || true + set_targets_from_args "$@" + logs_selected + ;; + status) + shift || true + [[ $# -eq 0 ]] || die "status does not take targets. Use it without arguments." + show_runtime_status + ;; + nuke) nuke_cmd ;; + *) usage; die "Unknown command: $cmd" ;; +esac diff --git a/scripts/tp_wizard/runtime.sh b/scripts/tp_wizard/runtime.sh new file mode 100644 index 000000000..199cf5026 --- /dev/null +++ b/scripts/tp_wizard/runtime.sh @@ -0,0 +1,56 @@ +# Shared orchestration used by wizard mode and CLI up mode. + +await_readiness(){ + local deadline=$(( $(date +%s) + READINESS_TIMEOUT )) + if $DB_INTERNAL; then + echo "Waiting (<= ${READINESS_TIMEOUT}s) for PostgreSQL on localhost:${DB_PORT} ..." + while (( $(date +%s) < deadline )); do + if tcp_check 127.0.0.1 "$DB_PORT" 1; then ok "PostgreSQL ready on :$DB_PORT"; break; fi + printf "."; sleep 1 + done + echo + fi + if $EN_APP; then + echo "Waiting (<= ${READINESS_TIMEOUT}s) for trustpoint HTTP on localhost:${APP_HTTP_HOST} ..." + while (( $(date +%s) < deadline )); do + if tcp_check 127.0.0.1 "$APP_HTTP_HOST" 1; then ok "trustpoint reachable on :$APP_HTTP_HOST"; break; fi + printf "."; sleep 1 + done + echo + fi + await_sftpgo_ready +} + +# ---- SFTPGo provisioning via REST ------------------------------------------- + +runtime_after_start(){ + $NOWAIT || await_readiness + $NOWAIT || provision_sftpgo_backup_user + $NOWAIT || verify_mailpit_delivery + $NOWAIT || wait_tls_fingerprint || true + final_summary +} + +runtime_start_enabled(){ + resolve_app_image + $DB_INTERNAL && ensure_volumes + start_postgres + start_mailpit + start_sftpgo + $EN_WF2_WORKER || stop_one "$WF2_WORKER_NAME" + start_app + start_workflows2_worker + runtime_after_start +} + +runtime_start_selected(){ + resolve_app_image + $ONLY_DB && { EN_PG=true; ensure_volumes; start_postgres; } + $ONLY_MAIL && { EN_MAILPIT=true; start_mailpit; } + $ONLY_SFTP && { EN_SFTPGO=true; start_sftpgo; } + $EN_WF2_WORKER || { $ONLY_APP && stop_one "$WF2_WORKER_NAME"; } + $ONLY_APP && start_app + $EN_WF2_WORKER && start_workflows2_worker + + runtime_after_start +} diff --git a/scripts/tp_wizard/services/mailpit.sh b/scripts/tp_wizard/services/mailpit.sh new file mode 100644 index 000000000..31b9b796c --- /dev/null +++ b/scripts/tp_wizard/services/mailpit.sh @@ -0,0 +1,71 @@ +mailpit_prompt_config(){ + EN_MAILPIT=$(ask_yes_no "Enable Mailpit (demo SMTP inbox)?" "n" && echo true || echo false) + if $EN_MAILPIT; then + MAILPIT_SMTP_PORT="$(ask_free_port 'Mailpit SMTP host port' "$MAILPIT_SMTP_PORT")" + MAILPIT_UI_PORT="$(ask_free_port 'Mailpit UI host port' "$MAILPIT_UI_PORT")" + fi +} + +start_mailpit(){ + $EN_MAILPIT || return 0 + local name="mailpit" + stop_one "$name" + if port_in_use "$MAILPIT_SMTP_PORT"; then die "Host port ${MAILPIT_SMTP_PORT} in use (Mailpit SMTP)."; fi + if port_in_use "$MAILPIT_UI_PORT"; then die "Host port ${MAILPIT_UI_PORT} in use (Mailpit UI)."; fi + log "Starting Mailpit..." + docker run -d --name "$name" --network "$NET" \ + -p "${MAILPIT_SMTP_PORT}:1025" \ + -p "${MAILPIT_UI_PORT}:8025" \ + "$MAILPIT_IMAGE" >/dev/null +} + + +mailpit_has_subject(){ + local subject="$1" + have curl || return 2 + local until=$(( $(date +%s) + MAILPIT_PROBE_TIMEOUT )) + local api="http://127.0.0.1:${MAILPIT_UI_PORT}/api/v1/messages" + while (( $(date +%s) < until )); do + if curl -fsS "$api" 2>/dev/null | grep -Fq "$subject"; then + return 0 + fi + sleep 1 + done + return 1 +} + + +probe_mailpit_from_container(){ + local container="$1" label="$2" + exists "$container" || return 0 + + local subject="trustpoint wizard ${label} mailpit probe $(date +%s)" + log "Sending Mailpit probe email from ${label} container..." + + if ! docker exec -e "PROBE_SUBJECT=${subject}" "$container" bash -lc \ + 'cd /var/www/html/trustpoint && uv run trustpoint/manage.py shell -c '\''import os; from django.conf import settings; from django.core.mail import send_mail; subject=os.environ["PROBE_SUBJECT"]; send_mail(subject, "Trustpoint Mailpit probe.", getattr(settings, "DEFAULT_FROM_EMAIL", None), ["demo@trustpoint.local"], fail_silently=False)'\''' \ + >/dev/null 2>&1; then + warn "Mailpit probe failed from ${label} container." + return 1 + fi + + if ! have curl; then + warn "curl not found on host; Mailpit probe from ${label} was sent but API verification was skipped." + return 0 + fi + + if mailpit_has_subject "$subject"; then + ok "Mailpit received the ${label} probe email." + return 0 + fi + + warn "Mailpit SMTP accepted the ${label} probe email, but it did not appear in the Mailpit UI within ${MAILPIT_PROBE_TIMEOUT}s." + return 1 +} + + +verify_mailpit_delivery(){ + $EN_MAILPIT || return 0 + $EN_APP && probe_mailpit_from_container trustpoint "web" || true + $EN_WF2_WORKER && probe_mailpit_from_container "$WF2_WORKER_NAME" "workflows2-worker" || true +} diff --git a/scripts/tp_wizard/services/postgres.sh b/scripts/tp_wizard/services/postgres.sh new file mode 100644 index 000000000..85cb02676 --- /dev/null +++ b/scripts/tp_wizard/services/postgres.sh @@ -0,0 +1,40 @@ +step_enable_postgres(){ + EN_PG=$(ask_yes_no "Start PostgreSQL container?" "y" && echo true || echo false) + DB_INTERNAL=$EN_PG + if $DB_INTERNAL; then DB_HOST="$DEF_DB_HOST_INTERNAL"; fi +} + + +step_postgres_config(){ + if $DB_INTERNAL; then + DB_NAME="$(ask_dbname 'PostgreSQL database name' "$DB_NAME")" + DB_USER="$(ask_user 'PostgreSQL username' "$DB_USER")" + DB_PASS="$(ask_password 'PostgreSQL password' "$DB_PASS")" + # Immediate check: host port must be free to publish + DB_PORT="$(ask_free_port 'PostgreSQL host port (mapped)' "$DB_PORT")" + else + DB_HOST="$(ask 'External DB host/IP' '127.0.0.1'; echo "$REPLY")" + DB_PORT="$(ask_port 'External DB port' "$DB_PORT")" + DB_NAME="$(ask_dbname 'External DB database name' "$DB_NAME")" + DB_USER="$(ask_user 'External DB username' "$DB_USER")" + DB_PASS="$(ask_password 'External DB password' "$DB_PASS")" + fi +} + + +start_postgres(){ + $DB_INTERNAL || return 0 + ensure_volumes + local name="postgres" + stop_one "$name" + # safety: host port must still be free (non-interactive runs) + if port_in_use "$DB_PORT"; then die "Host port ${DB_PORT} is already in use. Choose another port or stop the process using it."; fi + log "Starting PostgreSQL..." + docker run -d --name "$name" --network "$NET" \ + -p "${DB_PORT}:5432" \ + -v "${VOL_DB}:/var/lib/postgresql/data" \ + -e "POSTGRES_DB=$DB_NAME" \ + -e "POSTGRES_USER=$DB_USER" \ + -e "POSTGRES_PASSWORD=$DB_PASS" \ + "$PG_IMAGE" >/dev/null +} diff --git a/scripts/tp_wizard/services/sftpgo.sh b/scripts/tp_wizard/services/sftpgo.sh new file mode 100644 index 000000000..11a1e15ef --- /dev/null +++ b/scripts/tp_wizard/services/sftpgo.sh @@ -0,0 +1,155 @@ +sftpgo_prompt_config(){ + EN_SFTPGO=$(ask_yes_no "Enable SFTPGo (demo SFTP + Web UI)?" "n" && echo true || echo false) + if $EN_SFTPGO; then + SFTPGO_SFTP_PORT="$(ask_free_port 'SFTPGo SFTP host port' "$SFTPGO_SFTP_PORT")" + SFTPGO_WEB_PORT="$(ask_free_port 'SFTPGo Web UI host port' "$SFTPGO_WEB_PORT")" + SFTPGO_ADMIN_USER="$(ask_user 'SFTPGo admin user' "$SFTPGO_ADMIN_USER")" + SFTPGO_ADMIN_PASS="$(ask_password 'SFTPGo admin password' "$SFTPGO_ADMIN_PASS")" + + # Mandatory backup user + ask_user "SFTPGo backup username" "$SFTPGO_BACKUP_USER"; SFTPGO_BACKUP_USER="$REPLY" + ask_password "SFTPGo backup password" "$SFTPGO_BACKUP_PASS"; SFTPGO_BACKUP_PASS="$REPLY" + SFTPGO_BACKUP_HOME="/srv/sftpgo/data/${SFTPGO_BACKUP_USER}" + ask "SFTPGo backup home (inside container)" "$SFTPGO_BACKUP_HOME"; SFTPGO_BACKUP_HOME="$REPLY" + fi +} + +start_sftpgo(){ + $EN_SFTPGO || return 0 + local name="sftpgo" + stop_one "$name" + + if port_in_use "$SFTPGO_SFTP_PORT"; then die "Host port ${SFTPGO_SFTP_PORT} in use (SFTPGo SFTP)."; fi + if port_in_use "$SFTPGO_WEB_PORT"; then die "Host port ${SFTPGO_WEB_PORT} in use (SFTPGo Web)."; fi + + mkdir -p "${SFTPGO_ROOT}/data" + if [[ -n "$SFTPGO_BACKUP_USER" ]]; then + mkdir -p "${SFTPGO_ROOT}/data/${SFTPGO_BACKUP_USER}" + fi + chown -R 1000:1000 "${SFTPGO_ROOT}" 2>/dev/null || true + + log "Starting SFTPGo with auto-created admin..." + docker run -d --name "$name" --network "$NET" \ + -p "${SFTPGO_SFTP_PORT}:2022" \ + -p "${SFTPGO_WEB_PORT}:8080" \ + -v "${SFTPGO_ROOT}:/srv/sftpgo" \ + -e SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN=true \ + -e SFTPGO_DEFAULT_ADMIN_USERNAME="$SFTPGO_ADMIN_USER" \ + -e SFTPGO_DEFAULT_ADMIN_PASSWORD="$SFTPGO_ADMIN_PASS" \ + -e SFTPGO_HTTPD__BINDINGS__0__ADDRESS="0.0.0.0" \ + -e SFTPGO_HTTPD__BINDINGS__0__ENABLE_REST_API=true \ + -e SFTPGO_HTTPD__BINDINGS__0__ENABLE_WEB_ADMIN=true \ + -e SFTPGO_HTTPD__BINDINGS__0__PORT=8080 \ + -e SFTPGO_SFTPD__BINDINGS__0__PORT=2022 \ + "$SFTPGO_IMAGE" >/dev/null +} + + +await_sftpgo_ready(){ + $EN_SFTPGO || return 0 + local PORT; PORT="$(sftpgo_web_port)" + echo "Waiting (<= ${READINESS_TIMEOUT}s) for SFTPGo API on localhost:${PORT} ..." + local until=$(( $(date +%s) + READINESS_TIMEOUT )) + while (( $(date +%s) < until )); do + if have curl && [[ "$(curl -fsS "http://127.0.0.1:${PORT}/healthz" 2>/dev/null || true)" == "ok" ]]; then + ok "SFTPGo API healthy on :${PORT}" + return 0 + fi + if tcp_check 127.0.0.1 "$PORT" 1; then + ok "SFTPGo API port open on :${PORT}" + return 0 + fi + printf "."; sleep 1 + done + echo + warn "SFTPGo API not confirmed after ${READINESS_TIMEOUT}s" +} + + +upsert_virtual_folder(){ + local vf_name="$1" mapped="$2" + read -r -d '' VF_PAYLOAD <&2 + return 1 + fi + ok "Virtual folder '${vf_name}' → '${mapped}' ready." +} + + +provision_sftpgo_backup_user(){ + $EN_SFTPGO || return 0 + have curl || { warn "curl not found on host; skipping SFTPGo user provisioning."; return 0; } + + [[ -z "${SFTPGO_BACKUP_HOME:-}" ]] && SFTPGO_BACKUP_HOME="/srv/sftpgo/data/${SFTPGO_BACKUP_USER}" + + local host_home="${SFTPGO_ROOT}${SFTPGO_BACKUP_HOME#/srv/sftpgo}" + mkdir -p "$host_home" + chown -R 1000:1000 "$host_home" 2>/dev/null || true + + local PORT; PORT="$(sftpgo_web_port)" + API="http://127.0.0.1:${PORT}" + + for _ in {1..30}; do + [[ "$(curl -fsS "${API}/healthz" 2>/dev/null || true)" == "ok" ]] && break + sleep 1 + done + + local token="" + for _ in 1 2 3; do + token="$(curl -fsS -u "${SFTPGO_ADMIN_USER}:${SFTPGO_ADMIN_PASS}" "${API}/api/v2/token" \ + | sed -nE 's/.*"access_token":"([^"]+)".*/\1/p')" || true + [[ -n "$token" ]] && break + sleep 1 + done + [[ -n "$token" ]] || { warn "Could not obtain SFTPGo admin token; skipping user provisioning."; return 0; } + + HDR=(-H "Authorization: Bearer ${token}" -H "Content-Type: application/json") + upsert_virtual_folder "trustpoint" "${SFTPGO_BACKUP_HOME}" || return 0 + + local code method url http + code="$(curl -s -o /dev/null -w '%{http_code}' "${HDR[@]}" "${API}/api/v2/users/${SFTPGO_BACKUP_USER}")" + if [[ "$code" == "200" ]]; then + method=PUT; url="${API}/api/v2/users/${SFTPGO_BACKUP_USER}" + else + method=POST; url="${API}/api/v2/users" + fi + + read -r -d '' payload <&2 + else + ok "SFTPGo user '${SFTPGO_BACKUP_USER}' provisioned; VF mounted at /upload." + fi +} + +# ---- TLS fingerprint wait ---------------------------------------------------- diff --git a/scripts/tp_wizard/services/trustpoint.sh b/scripts/tp_wizard/services/trustpoint.sh new file mode 100644 index 000000000..3478bf6b1 --- /dev/null +++ b/scripts/tp_wizard/services/trustpoint.sh @@ -0,0 +1,126 @@ +step_enable_trustpoint(){ EN_APP=$(ask_yes_no "Enable trustpoint application container?" "y" && echo true || echo false); } + + +step_trustpoint_source(){ + $EN_APP || return 0 + if ask_yes_no "Build trustpoint locally from ${TP_DOCKERFILE}? (No = pull from Docker Hub)" "y"; then + BUILD_LOCAL=true + APP_IMAGE="trustpoint:local" + else + BUILD_LOCAL=false + ask "Docker Hub image tag to pull (repository ${TP_REPO})" "latest"; local tag="$REPLY" + APP_IMAGE="${TP_REPO}:${tag}" + fi +} + + +step_app_db_binding(){ + $EN_APP || return 0 + if ask_yes_no "Should trustpoint reuse the PostgreSQL settings configured above?" "y"; then + APP_DB_NAME="$DB_NAME" + APP_DB_USER="$DB_USER" + APP_DB_PASS="$DB_PASS" + if $DB_INTERNAL; then + # Internal DB: always connect to the container directly + APP_DB_HOST="$DEF_DB_HOST_INTERNAL" + APP_DB_PORT=5432 + else + # External DB: use exactly what you entered + APP_DB_HOST="$DB_HOST" + APP_DB_PORT="$DB_PORT" + fi + else + local def_host def_port + if $DB_INTERNAL; then + def_host="$DEF_DB_HOST_INTERNAL"; def_port=5432 + else + def_host="$DB_HOST"; def_port="$DB_PORT" + fi + APP_DB_HOST="$(ask 'trustpoint DB host' "$def_host"; echo "$REPLY")" + APP_DB_PORT="$(ask_port 'trustpoint DB port' "$def_port")" + APP_DB_NAME="$(ask_dbname 'trustpoint DB name' "$DB_NAME")" + APP_DB_USER="$(ask_user 'trustpoint DB user' "$DB_USER")" + APP_DB_PASS="$(ask_password 'trustpoint DB password' "$DB_PASS")" + fi +} + + +build_trustpoint_image(){ [[ -f "$TP_DOCKERFILE" ]] || log "Dockerfile not found: $TP_DOCKERFILE"; log "Building trustpoint image..."; docker build -f "$TP_DOCKERFILE" -t "trustpoint:local" .; } + +pull_trustpoint_image(){ log "Pulling ${APP_IMAGE} ..."; docker pull "${APP_IMAGE}" >/dev/null; } + +configure_app_image_prompt(){ + if ask_yes_no "Build trustpoint locally from ${TP_DOCKERFILE}? (No = pull from Docker Hub)" "y"; then + BUILD_LOCAL=true + APP_IMAGE="trustpoint:local" + else + BUILD_LOCAL=false + ask "Docker Hub image tag to pull (repository ${TP_REPO})" "latest" + local tag="$REPLY" + APP_IMAGE="${TP_REPO}:${tag}" + fi +} + +resolve_app_image(){ + if ! $EN_APP && ! $EN_WF2_WORKER; then + return 0 + fi + if $BUILD_LOCAL; then + build_trustpoint_image + else + pull_trustpoint_image + fi +} + +start_app(){ + $EN_APP || return 0 + local name="trustpoint" + stop_one "$name" + # die early if 80/443 are busy + if port_in_use "$APP_HTTP_HOST"; then die "Host port ${APP_HTTP_HOST} is in use (trustpoint HTTP)."; fi + if port_in_use "$APP_HTTPS_HOST"; then die "Host port ${APP_HTTPS_HOST} is in use (trustpoint HTTPS)."; fi + + log "Starting trustpoint..." + local smtp_env=() + if $EN_MAILPIT; then + smtp_env+=( -e "EMAIL_HOST=mailpit" -e "EMAIL_PORT=1025" -e "EMAIL_USE_TLS=0" -e "EMAIL_USE_SSL=0" -e "DEFAULT_FROM_EMAIL=no-reply@trustpoint.local" ) + fi + docker run -d --name "$name" --network "$NET" \ + -p "${APP_HTTP_HOST}:80" \ + -p "${APP_HTTPS_HOST}:443" \ + -e "POSTGRES_DB=$APP_DB_NAME" \ + -e "DATABASE_USER=$APP_DB_USER" \ + -e "DATABASE_PASSWORD=$APP_DB_PASS" \ + -e "DATABASE_HOST=$APP_DB_HOST" \ + -e "DATABASE_PORT=$APP_DB_PORT" \ + "${smtp_env[@]}" \ + "$APP_IMAGE" >/dev/null +} + + +extract_tls_fingerprint_once(){ + local logs="$1" + if [[ "$logs" =~ ([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2} ]]; then TLS_FP_FOUND="${BASH_REMATCH[0]}"; return 0; fi + if [[ "$logs" =~ ([0-9A-Fa-f]{64}) ]]; then TLS_FP_FOUND="${BASH_REMATCH[1]}"; return 0; fi + if [[ "$logs" =~ [Ss][Hh][Aa]-?256[:\ ]([A-Za-z0-9+/=_:-]{43,}) ]]; then TLS_FP_FOUND="SHA256:${BASH_REMATCH[1]}"; return 0; fi + return 1 +} + + +wait_tls_fingerprint(){ + $EN_APP || { TLS_FP_ELAPSED=0; return 0; } + local start; start="$(date +%s)" + local start_iso; start_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + local end=$(( start + TLS_FP_TIMEOUT )) + while (( $(date +%s) < end )); do + local chunk + chunk="$(docker logs --since "$start_iso" trustpoint 2>/dev/null || true)" + if extract_tls_fingerprint_once "$chunk"; then + TLS_FP_ELAPSED=$(( $(date +%s) - start )) + return 0 + fi + sleep 3 + done + TLS_FP_ELAPSED=$(( $(date +%s) - start )) + return 1 +} diff --git a/scripts/tp_wizard/services/workflows2_worker.sh b/scripts/tp_wizard/services/workflows2_worker.sh new file mode 100644 index 000000000..b5dd5b14e --- /dev/null +++ b/scripts/tp_wizard/services/workflows2_worker.sh @@ -0,0 +1,61 @@ +step_workflows2_worker(){ + $EN_APP || return 0 + EN_WF2_WORKER=$( + ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false + ) +} + + +prepare_workflows2_worker_folder(){ + $EN_WF2_WORKER || return 0 + mkdir -p "$WF2_FOLDER" + chmod 700 "$WF2_FOLDER" 2>/dev/null || true + cat > "$WF2_WORKER_README" </dev/null || true + + cat > "$WF2_WORKER_ENV_FILE" <> "$WF2_WORKER_ENV_FILE" </dev/null || true +} + + +start_workflows2_worker(){ + $EN_WF2_WORKER || return 0 + local name="$WF2_WORKER_NAME" + stop_one "$name" + prepare_workflows2_worker_folder + log "Starting dedicated workflows2 worker..." + docker run -d --name "$name" --network "$NET" \ + --env-file "$WF2_WORKER_ENV_FILE" \ + "$APP_IMAGE" >/dev/null +} + +# -------------------------- Readiness & Provision ----------------------------- diff --git a/scripts/tp_wizard/state.sh b/scripts/tp_wizard/state.sh new file mode 100644 index 000000000..c67cf5277 --- /dev/null +++ b/scripts/tp_wizard/state.sh @@ -0,0 +1,33 @@ +# -------------------------- Wizard state ------------------------------------- +EN_APP=false; EN_PG=false; EN_MAILPIT=false; EN_SFTPGO=false; EN_WF2_WORKER=false + +DB_INTERNAL=true +DB_HOST="$DEF_DB_HOST_INTERNAL" # default host when internal +DB_PORT="$DEF_DB_PORT" # host-mapped port for convenience access +DB_NAME="$DEF_DB_NAME" +DB_USER="$DEF_DB_USER" +DB_PASS="$DEF_DB_PASS" + +APP_DB_HOST="$DB_HOST" +APP_DB_PORT="$DB_PORT" +APP_DB_NAME="$DB_NAME" +APP_DB_USER="$DB_USER" +APP_DB_PASS="$DEF_DB_PASS" + +MAILPIT_SMTP_PORT="$DEF_MAILPIT_SMTP_PORT" +MAILPIT_UI_PORT="$DEF_MAILPIT_UI_PORT" + +SFTPGO_SFTP_PORT="$DEF_SFTPGO_SFTP_PORT" +SFTPGO_WEB_PORT="$DEF_SFTPGO_WEB_PORT" +SFTPGO_ADMIN_USER="$DEF_SFTPGO_ADMIN_USER" +SFTPGO_ADMIN_PASS="$DEF_SFTPGO_ADMIN_PASS" + +TLS_FP_FOUND="" +TLS_FP_ELAPSED=0 +WF2_WORKER_LEASE="$DEF_WF2_WORKER_LEASE" +WF2_WORKER_BATCH="$DEF_WF2_WORKER_BATCH" +WF2_WORKER_SLEEP="$DEF_WF2_WORKER_SLEEP" + +# CLI target flags +ONLY_APP=false; ONLY_DB=false; ONLY_MAIL=false; ONLY_SFTP=false; ONLY_WF2_WORKER=false +NOWAIT=false diff --git a/scripts/tp_wizard/summary.sh b/scripts/tp_wizard/summary.sh new file mode 100644 index 000000000..379c6de0d --- /dev/null +++ b/scripts/tp_wizard/summary.sh @@ -0,0 +1,177 @@ +show_plan(){ + echo + echo "==================== Configuration Summary (Planned) ====================" + printf "%-22s %s\n" "Network:" "$NET" + printf "%-22s %s\n" "DB Volume:" "$VOL_DB" + echo + printf "%-22s %s\n" "trustpoint enabled:" "$EN_APP" + if $EN_APP; then + if $BUILD_LOCAL; then + printf "%-22s %s\n" "App image:" "Build local → trustpoint:local" + else + printf "%-22s %s\n" "App image:" "Pull → ${APP_IMAGE}" + fi + printf "%-22s %s\n" "Host ports:" "80→80 (HTTP), 443→443 (HTTPS)" + fi + echo + printf "%-22s %s\n" "Internal Postgres:" "$DB_INTERNAL" + printf "%-22s %s\n" "DB host:" "$DB_HOST" + printf "%-22s %s\n" "DB host port:" "$DB_PORT" + printf "%-22s %s\n" "DB name:" "$DB_NAME" + printf "%-22s %s\n" "DB user:" "$DB_USER" + printf "%-22s %s\n" "DB pass:" "$(mask "$DB_PASS")" + echo + if $EN_APP; then + printf "%-22s %s\n" "trustpoint DB host:" "$APP_DB_HOST" + printf "%-22s %s\n" "trustpoint DB port:" "$APP_DB_PORT" + printf "%-22s %s\n" "trustpoint DB name:" "$APP_DB_NAME" + printf "%-22s %s\n" "trustpoint DB user:" "$APP_DB_USER" + printf "%-22s %s\n" "trustpoint DB pass:" "$(mask "$APP_DB_PASS")" + fi + echo + printf "%-22s %s\n" "Mailpit enabled:" "$EN_MAILPIT" + $EN_MAILPIT && printf "%-22s %s\n" "Mailpit ports:" "SMTP ${MAILPIT_SMTP_PORT}, UI ${MAILPIT_UI_PORT}" + echo + printf "%-22s %s\n" "workflows2 worker:" "$EN_WF2_WORKER" + $EN_WF2_WORKER && { + printf "%-22s %s\n" "Worker container:" "${WF2_WORKER_NAME}" + printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" + } + echo + printf "%-22s %s\n" "SFTPGo enabled:" "$EN_SFTPGO" + $EN_SFTPGO && { + printf "%-22s %s\n" "SFTPGo ports:" "SFTP ${SFTPGO_SFTP_PORT}, Web ${SFTPGO_WEB_PORT}" + printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" + printf "%-22s %s\n" "SFTP backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" + printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" + } + echo "=========================================================================" +} + +# -------------------------- Build/Pull & Start ------------------------------- + +show_runtime_status(){ + local net_state="absent" vol_state="absent" + docker network inspect "$NET" >/dev/null 2>&1 && net_state="present" + docker volume inspect "$VOL_DB" >/dev/null 2>&1 && vol_state="present" + + echo + echo "=========================== Runtime Status (Live) ========================" + printf "%-22s %s\n" "Network:" "${NET} (${net_state})" + printf "%-22s %s\n" "DB volume:" "${VOL_DB} (${vol_state})" + printf "%-22s %s\n" "workflow2 folder:" "$([ -d "$WF2_FOLDER" ] && echo "${WF2_FOLDER} (present)" || echo "${WF2_FOLDER} (absent)")" + echo + printf "%-20s %-10s %-10s %s\n" "Container" "State" "Health" "Image" + print_container_status_row trustpoint + print_container_status_row postgres + print_container_status_row mailpit + print_container_status_row sftpgo + print_container_status_row "$WF2_WORKER_NAME" + echo + + if exists trustpoint; then + local http_port https_port db_host db_port db_name db_user db_pass + http_port="$(container_host_port trustpoint 80/tcp)" + https_port="$(container_host_port trustpoint 443/tcp)" + db_host="$(container_env trustpoint DATABASE_HOST)" + db_port="$(container_env trustpoint DATABASE_PORT)" + db_name="$(container_env trustpoint POSTGRES_DB)" + db_user="$(container_env trustpoint DATABASE_USER)" + db_pass="$(container_env trustpoint DATABASE_PASSWORD)" + + [[ -n "$http_port" ]] && printf "%-22s %s\n" "trustpoint HTTP:" "http://localhost:${http_port}" + [[ -n "$https_port" ]] && printf "%-22s %s\n" "trustpoint HTTPS:" "https://localhost:${https_port}" + printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings" + if [[ -n "$db_host" || -n "$db_port" || -n "$db_name" || -n "$db_user" ]]; then + printf "%-22s %s\n" "DB connect:" "host=${db_host:-?} port=${db_port:-?} db=${db_name:-?} user=${db_user:-?} pass=$(mask "${db_pass:-}")" + fi + fi + + if exists postgres; then + local pg_port + pg_port="$(container_host_port postgres 5432/tcp)" + [[ -n "$pg_port" ]] && printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${pg_port}" + fi + + if exists mailpit; then + local mailpit_ui mailpit_smtp + mailpit_ui="$(container_host_port mailpit 8025/tcp)" + mailpit_smtp="$(container_host_port mailpit 1025/tcp)" + [[ -n "$mailpit_ui" ]] && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${mailpit_ui}" + [[ -n "$mailpit_smtp" ]] && printf "%-22s %s\n" "Mailpit SMTP:" "localhost:${mailpit_smtp}" + fi + + if exists "$WF2_WORKER_NAME"; then + local worker_db worker_lease worker_batch worker_sleep + worker_db="$(container_env "$WF2_WORKER_NAME" DATABASE_HOST)" + worker_lease="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_LEASE)" + worker_batch="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_BATCH)" + worker_sleep="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_SLEEP)" + printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" + [[ -n "$worker_db" ]] && printf "%-22s %s\n" "worker DB host:" "${worker_db}" + [[ -n "$worker_lease" || -n "$worker_batch" || -n "$worker_sleep" ]] && \ + printf "%-22s %s\n" "worker tuning:" "lease=${worker_lease:-?} batch=${worker_batch:-?} sleep=${worker_sleep:-?}" + fi + + if exists sftpgo; then + local sftpgo_web sftpgo_sftp sftpgo_admin + sftpgo_web="$(container_host_port sftpgo 8080/tcp)" + sftpgo_sftp="$(container_host_port sftpgo 2022/tcp)" + sftpgo_admin="$(container_env sftpgo SFTPGO_DEFAULT_ADMIN_USERNAME)" + [[ -n "$sftpgo_web" ]] && printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${sftpgo_web}/web/admin" + [[ -n "$sftpgo_sftp" ]] && printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${sftpgo_sftp}" + [[ -n "$sftpgo_admin" ]] && printf "%-22s %s\n" "SFTPGo admin:" "${sftpgo_admin}" + printf "%-22s %s\n" "SFTPGo data dir:" "${SFTPGO_ROOT}" + fi + + echo "=========================================================================" +} + +# -------------------------- Summary ------------------------------------------ + +final_summary(){ + echo + echo "========================= Runtime Summary (Actual) =======================" + printf "%-22s %s\n" "Network:" "$NET" + printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker)$' || true)" + echo + if $EN_APP; then + printf "%-22s %s\n" "trustpoint:" "http://localhost:80 | https://localhost:443" + printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings (default: auto)" + fi + if $DB_INTERNAL; then + printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${DB_PORT} (container port 5432)" + fi + if $EN_APP; then + printf "%-22s %s\n" "DB connect:" "host=${APP_DB_HOST} port=${APP_DB_PORT} db=${APP_DB_NAME} user=${APP_DB_USER} pass=$(mask "$APP_DB_PASS")" + fi + $EN_MAILPIT && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${MAILPIT_UI_PORT} (SMTP :${MAILPIT_SMTP_PORT})" + if $EN_WF2_WORKER; then + printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" + printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" + fi + if $EN_SFTPGO; then + local PORT; PORT="$(sftpgo_web_port)" + printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${PORT}/web/admin" + printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${SFTPGO_SFTP_PORT}" + printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" + printf "%-22s %s\n" "Backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" + printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" + printf "%-22s %s\n" "Backup URL:" "sftp://${SFTPGO_BACKUP_USER}:***@127.0.0.1:${SFTPGO_SFTP_PORT}/" + printf "%-22s %s\n" "Data dir:" "${SFTPGO_ROOT}" + fi + if $EN_APP; then + if [[ -n "$TLS_FP_FOUND" ]]; then + printf "%-22s %s\n" "TLS fingerprint:" "$TLS_FP_FOUND" + else + if $NOWAIT; then + printf "%-22s %s\n" "TLS fingerprint:" "skipped (NOWAIT)" + else + printf "%-22s %s\n" "TLS fingerprint:" "not found yet (polled ${TLS_FP_ELAPSED}s; timeout ${TLS_FP_TIMEOUT}s)" + fi + fi + fi + echo "=========================================================================" +} + +# -------------------------- High-level orchestration ------------------------- diff --git a/scripts/tp_wizard/wizard.sh b/scripts/tp_wizard/wizard.sh new file mode 100644 index 000000000..8667e02b4 --- /dev/null +++ b/scripts/tp_wizard/wizard.sh @@ -0,0 +1,15 @@ +wizard(){ + echo "$(bold)trustpoint Setup Wizard$(rst)" + ensure_network + step_enable_trustpoint + step_trustpoint_source + step_enable_postgres + step_postgres_config + step_app_db_binding + mailpit_prompt_config + sftpgo_prompt_config + step_workflows2_worker + show_plan + ask_yes_no "Proceed with these settings?" "y" || { warn "Aborted by user."; exit 1; } + runtime_start_enabled +} diff --git a/tp_wizard.sh b/tp_wizard.sh index da9c5bd71..d6cb6894d 100755 --- a/tp_wizard.sh +++ b/tp_wizard.sh @@ -1,976 +1,10 @@ #!/usr/bin/env bash -# tp_wizard.sh — single-file wizard for trustpoint stack +# tp_wizard.sh — public entrypoint for the trustpoint setup wizard set -euo pipefail -# -------------------------- Constants & defaults ------------------------------ -PROJECT="trustpoint" -NET="${PROJECT}-net" -VOL_DB="${PROJECT}_postgres_data" +TP_WIZARD_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export TP_WIZARD_ROOT -# trustpoint image handling -TP_DOCKERFILE="docker/trustpoint/Dockerfile" -TP_REPO="trustpointproject/trustpoint" -APP_IMAGE="${TP_REPO}:latest" # overridden to trustpoint:local when BUILD_LOCAL=true -BUILD_LOCAL=false +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/bootstrap.sh" -# Fixed images -PG_IMAGE="postgres:15.14" -MAILPIT_IMAGE="axllent/mailpit:v1.27" -SFTPGO_IMAGE="drakkan/sftpgo:2.6.x-slim" -WF2_WORKER_NAME="trustpoint-worker" - -# Fixed trustpoint ports -APP_HTTP_HOST=80 -APP_HTTPS_HOST=443 - -# PostgreSQL defaults -DEF_DB_NAME="trustpoint_db" -DEF_DB_USER="admin" -DEF_DB_PASS="testing321" -DEF_DB_PORT=5432 -DEF_DB_HOST_INTERNAL="postgres" # container name/hostname - -# Mailpit defaults -DEF_MAILPIT_SMTP_PORT=1025 -DEF_MAILPIT_UI_PORT=8025 - -# SFTPGo defaults -DEF_SFTPGO_SFTP_PORT=2222 -DEF_SFTPGO_WEB_PORT=8080 -DEF_SFTPGO_ADMIN_USER="admin" -DEF_SFTPGO_ADMIN_PASS="testing321" -SFTPGO_ROOT="${PWD}/sftpgo-data" -WF2_FOLDER="${PWD}/workflow2Folder" -WF2_WORKER_ENV_FILE="${WF2_FOLDER}/worker.env" -WF2_WORKER_README="${WF2_FOLDER}/README.txt" -DEF_WF2_WORKER_LEASE=30 -DEF_WF2_WORKER_BATCH=10 -DEF_WF2_WORKER_SLEEP=1 -MAILPIT_PROBE_TIMEOUT=20 - -# Timeouts -READINESS_TIMEOUT=90 -TLS_FP_TIMEOUT=150 - -# Optional backup user provisioning -SFTPGO_BACKUP_USER="tpbackup" -SFTPGO_BACKUP_PASS="testing321" -SFTPGO_BACKUP_HOME="" - -# -------------------------- UI helpers --------------------------------------- -bold(){ tput bold 2>/dev/null || true; } -rst(){ tput sgr0 2>/dev/null || true; } -ylw(){ tput setaf 3 2>/dev/null || true; } -grn(){ tput setaf 2 2>/dev/null || true; } -red(){ tput setaf 1 2>/dev/null || true; } -log(){ printf "%s\n" "$*" >&2; } -ok(){ log "$(grn)✔$(rst) $*"; } -warn(){ log "$(ylw)⚠$(rst) $*"; } -err(){ log "$(red)✖$(rst) $*"; } -die(){ err "$*"; exit 1; } -have(){ command -v "$1" >/dev/null 2>&1; } - -# -------------------------- Docker helpers ----------------------------------- -exists(){ docker ps -a --format '{{.Names}}' | grep -Fxq "$1"; } -running(){ docker ps --format '{{.Names}}' | grep -Fxq "$1"; } -ensure_network(){ docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET" >/dev/null; } -ensure_volumes(){ docker volume inspect "$VOL_DB" >/dev/null 2>&1 || docker volume create --label "tp.project=${PROJECT}" "$VOL_DB" >/dev/null; } -stop_one(){ local n="$1"; exists "$n" || return 0; running "$n" && docker stop "$n" >/dev/null || true; docker rm "$n" >/dev/null || true; } -container_state(){ local n="$1"; exists "$n" || { echo "absent"; return; }; docker inspect -f '{{.State.Status}}' "$n" 2>/dev/null || echo "unknown"; } -container_health(){ local n="$1" h=""; exists "$n" || { echo "-"; return; }; h="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$n" 2>/dev/null || true)"; echo "${h:--}"; } -container_image(){ local n="$1"; exists "$n" || { echo "-"; return; }; docker inspect -f '{{.Config.Image}}' "$n" 2>/dev/null || echo "-"; } -container_host_port(){ local n="$1" spec="$2" p=""; exists "$n" || return 0; p="$(docker port "$n" "$spec" 2>/dev/null | awk -F: 'NR==1 {print $NF}')" || true; echo "${p}"; } -container_env(){ local n="$1" key="$2"; exists "$n" || return 0; docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$n" 2>/dev/null | sed -n "s/^${key}=//p" | head -n1; } -container_volume_names(){ - local n="$1" - exists "$n" || return 0 - docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{println .Name}}{{end}}{{end}}' "$n" 2>/dev/null | sed '/^$/d' -} -collect_project_volumes(){ - { - echo "$VOL_DB" - container_volume_names trustpoint - container_volume_names postgres - container_volume_names mailpit - container_volume_names sftpgo - container_volume_names "$WF2_WORKER_NAME" - } | sed '/^$/d' | sort -u -} -print_container_status_row(){ - local n="$1" - printf "%-20s %-10s %-10s %s\n" "$n" "$(container_state "$n")" "$(container_health "$n")" "$(container_image "$n")" -} - -# quick TCP connect test (true if something accepts on host:port) -tcp_check(){ local host="$1" port="$2" ts=$(( $(date +%s) + ${3:-5} )); while (( $(date +%s) < ts )); do (exec 3<>"/dev/tcp/$host/$port") >/dev/null 2>&1 && { exec 3>&- 3<&-; return 0; }; sleep 1; done; return 1; } -port_in_use(){ tcp_check 127.0.0.1 "$1" 1; } - -# SFTPGo host web port resolver (avoid NGINX :80) -sftpgo_web_port(){ - local p; p="$(docker port sftpgo 8080/tcp 2>/dev/null | awk -F: '{print $2}')" || true - echo "${p:-$SFTPGO_WEB_PORT}" -} - -# -------------------------- Input helpers ------------------------------------ -ask(){ local prompt="$1" def="${2:-}"; if [[ -n "$def" ]]; then read -r -p "$(bold)${prompt}$(rst) [default: ${def}] > " REPLY || true; REPLY="${REPLY:-$def}"; else read -r -p "$(bold)${prompt}$(rst) > " REPLY || true; fi; } -ask_yes_no(){ local prompt="$1" def="${2:-y}" a; case "${def}" in y|yes) a="[Y/n]";; n|no) a="[y/N]";; *) a="[y/n]";; esac; read -r -p "$(bold)${prompt} ${a}$(rst) > " resp || true; resp="${resp:-$def}"; [[ "${resp}" =~ ^y ]]; } -ask_port(){ local prompt="$1" def="$2" p; while true; do ask "$prompt" "$def"; p="$REPLY"; [[ "$p" =~ ^[0-9]{1,5}$ ]] && (( p>0 && p<65536 )) && { echo "$p"; return; } ; warn "Invalid port. Enter 1..65535."; done; } -ask_free_port(){ local prompt="$1" def="$2" p; while true; do p="$(ask_port "$prompt" "$def")"; if port_in_use "$p"; then warn "Port ${p} is already in use on this host. Pick another."; else echo "$p"; return; fi; done; } -ask_user(){ local prompt="$1" def="$2" u; while true; do ask "$prompt" "$def"; u="$REPLY"; [[ "$u" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]*$ ]] && { echo "$u"; return; } ; warn "Invalid username."; done; } -ask_dbname(){ local prompt="$1" def="$2" d; while true; do ask "$prompt" "$def"; d="$REPLY"; [[ "$d" =~ ^[A-Za-z0-9_-]+$ ]] && { echo "$d"; return; } ; warn "Invalid DB name."; done; } -ask_password(){ local prompt="$1" def="$2" pw; while true; do ask "$prompt" "$def"; pw="$REPLY"; (( ${#pw} >= 6 )) && { echo "$pw"; return; } ; warn "Password too short (min 6)."; done; } -mask(){ local s="$1" n=${#1}; (( n<=2 )) && { printf '%s' '**'; return; }; printf '%*s' $((n-2)) '' | tr ' ' '*'; printf '%s' "${s: -2}"; } - -# -------------------------- Wizard state ------------------------------------- -EN_APP=false; EN_PG=false; EN_MAILPIT=false; EN_SFTPGO=false; EN_WF2_WORKER=false - -DB_INTERNAL=true -DB_HOST="$DEF_DB_HOST_INTERNAL" # default host when internal -DB_PORT="$DEF_DB_PORT" # host-mapped port for convenience access -DB_NAME="$DEF_DB_NAME" -DB_USER="$DEF_DB_USER" -DB_PASS="$DEF_DB_PASS" - -APP_DB_HOST="$DB_HOST" -APP_DB_PORT="$DB_PORT" -APP_DB_NAME="$DB_NAME" -APP_DB_USER="$DB_USER" -APP_DB_PASS="$DEF_DB_PASS" - -MAILPIT_SMTP_PORT="$DEF_MAILPIT_SMTP_PORT" -MAILPIT_UI_PORT="$DEF_MAILPIT_UI_PORT" - -SFTPGO_SFTP_PORT="$DEF_SFTPGO_SFTP_PORT" -SFTPGO_WEB_PORT="$DEF_SFTPGO_WEB_PORT" -SFTPGO_ADMIN_USER="$DEF_SFTPGO_ADMIN_USER" -SFTPGO_ADMIN_PASS="$DEF_SFTPGO_ADMIN_PASS" - -TLS_FP_FOUND="" -TLS_FP_ELAPSED=0 -WF2_WORKER_LEASE="$DEF_WF2_WORKER_LEASE" -WF2_WORKER_BATCH="$DEF_WF2_WORKER_BATCH" -WF2_WORKER_SLEEP="$DEF_WF2_WORKER_SLEEP" - -# CLI target flags -ONLY_APP=false; ONLY_DB=false; ONLY_MAIL=false; ONLY_SFTP=false; ONLY_WF2_WORKER=false -NOWAIT=false - -# -------------------------- Steps -------------------------------------------- -preflight(){ have docker || die "docker not found"; docker version >/dev/null || die "docker daemon not reachable"; } - -step_enable_trustpoint(){ EN_APP=$(ask_yes_no "Enable trustpoint application container?" "y" && echo true || echo false); } - -step_trustpoint_source(){ - $EN_APP || return 0 - if ask_yes_no "Build trustpoint locally from ${TP_DOCKERFILE}? (No = pull from Docker Hub)" "y"; then - BUILD_LOCAL=true - APP_IMAGE="trustpoint:local" - else - BUILD_LOCAL=false - ask "Docker Hub image tag to pull (repository ${TP_REPO})" "latest"; local tag="$REPLY" - APP_IMAGE="${TP_REPO}:${tag}" - fi -} - -step_enable_postgres(){ - EN_PG=$(ask_yes_no "Start PostgreSQL container?" "y" && echo true || echo false) - DB_INTERNAL=$EN_PG - if $DB_INTERNAL; then DB_HOST="$DEF_DB_HOST_INTERNAL"; fi -} - -step_postgres_config(){ - if $DB_INTERNAL; then - DB_NAME="$(ask_dbname 'PostgreSQL database name' "$DB_NAME")" - DB_USER="$(ask_user 'PostgreSQL username' "$DB_USER")" - DB_PASS="$(ask_password 'PostgreSQL password' "$DB_PASS")" - # Immediate check: host port must be free to publish - DB_PORT="$(ask_free_port 'PostgreSQL host port (mapped)' "$DB_PORT")" - else - DB_HOST="$(ask 'External DB host/IP' '127.0.0.1'; echo "$REPLY")" - DB_PORT="$(ask_port 'External DB port' "$DB_PORT")" - DB_NAME="$(ask_dbname 'External DB database name' "$DB_NAME")" - DB_USER="$(ask_user 'External DB username' "$DB_USER")" - DB_PASS="$(ask_password 'External DB password' "$DB_PASS")" - fi -} - -step_app_db_binding(){ - $EN_APP || return 0 - if ask_yes_no "Should trustpoint reuse the PostgreSQL settings configured above?" "y"; then - APP_DB_NAME="$DB_NAME" - APP_DB_USER="$DB_USER" - APP_DB_PASS="$DB_PASS" - if $DB_INTERNAL; then - # Internal DB: always connect to the container directly - APP_DB_HOST="$DEF_DB_HOST_INTERNAL" - APP_DB_PORT=5432 - else - # External DB: use exactly what you entered - APP_DB_HOST="$DB_HOST" - APP_DB_PORT="$DB_PORT" - fi - else - local def_host def_port - if $DB_INTERNAL; then - def_host="$DEF_DB_HOST_INTERNAL"; def_port=5432 - else - def_host="$DB_HOST"; def_port="$DB_PORT" - fi - APP_DB_HOST="$(ask 'trustpoint DB host' "$def_host"; echo "$REPLY")" - APP_DB_PORT="$(ask_port 'trustpoint DB port' "$def_port")" - APP_DB_NAME="$(ask_dbname 'trustpoint DB name' "$DB_NAME")" - APP_DB_USER="$(ask_user 'trustpoint DB user' "$DB_USER")" - APP_DB_PASS="$(ask_password 'trustpoint DB password' "$DB_PASS")" - fi -} - -step_helpers(){ - EN_MAILPIT=$(ask_yes_no "Enable Mailpit (demo SMTP inbox)?" "n" && echo true || echo false) - if $EN_MAILPIT; then - MAILPIT_SMTP_PORT="$(ask_free_port 'Mailpit SMTP host port' "$MAILPIT_SMTP_PORT")" - MAILPIT_UI_PORT="$(ask_free_port 'Mailpit UI host port' "$MAILPIT_UI_PORT")" - fi - - EN_SFTPGO=$(ask_yes_no "Enable SFTPGo (demo SFTP + Web UI)?" "n" && echo true || echo false) - if $EN_SFTPGO; then - SFTPGO_SFTP_PORT="$(ask_free_port 'SFTPGo SFTP host port' "$SFTPGO_SFTP_PORT")" - SFTPGO_WEB_PORT="$(ask_free_port 'SFTPGo Web UI host port' "$SFTPGO_WEB_PORT")" - SFTPGO_ADMIN_USER="$(ask_user 'SFTPGo admin user' "$SFTPGO_ADMIN_USER")" - SFTPGO_ADMIN_PASS="$(ask_password 'SFTPGo admin password' "$SFTPGO_ADMIN_PASS")" - - # Mandatory backup user - ask_user "SFTPGo backup username" "$SFTPGO_BACKUP_USER"; SFTPGO_BACKUP_USER="$REPLY" - ask_password "SFTPGo backup password" "$SFTPGO_BACKUP_PASS"; SFTPGO_BACKUP_PASS="$REPLY" - SFTPGO_BACKUP_HOME="/srv/sftpgo/data/${SFTPGO_BACKUP_USER}" - ask "SFTPGo backup home (inside container)" "$SFTPGO_BACKUP_HOME"; SFTPGO_BACKUP_HOME="$REPLY" - fi -} - -step_workflows2_worker(){ - $EN_APP || return 0 - EN_WF2_WORKER=$( - ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false - ) -} - -show_plan(){ - echo - echo "==================== Configuration Summary (Planned) ====================" - printf "%-22s %s\n" "Network:" "$NET" - printf "%-22s %s\n" "DB Volume:" "$VOL_DB" - echo - printf "%-22s %s\n" "trustpoint enabled:" "$EN_APP" - if $EN_APP; then - if $BUILD_LOCAL; then - printf "%-22s %s\n" "App image:" "Build local → trustpoint:local" - else - printf "%-22s %s\n" "App image:" "Pull → ${APP_IMAGE}" - fi - printf "%-22s %s\n" "Host ports:" "80→80 (HTTP), 443→443 (HTTPS)" - fi - echo - printf "%-22s %s\n" "Internal Postgres:" "$DB_INTERNAL" - printf "%-22s %s\n" "DB host:" "$DB_HOST" - printf "%-22s %s\n" "DB host port:" "$DB_PORT" - printf "%-22s %s\n" "DB name:" "$DB_NAME" - printf "%-22s %s\n" "DB user:" "$DB_USER" - printf "%-22s %s\n" "DB pass:" "$(mask "$DB_PASS")" - echo - if $EN_APP; then - printf "%-22s %s\n" "trustpoint DB host:" "$APP_DB_HOST" - printf "%-22s %s\n" "trustpoint DB port:" "$APP_DB_PORT" - printf "%-22s %s\n" "trustpoint DB name:" "$APP_DB_NAME" - printf "%-22s %s\n" "trustpoint DB user:" "$APP_DB_USER" - printf "%-22s %s\n" "trustpoint DB pass:" "$(mask "$APP_DB_PASS")" - fi - echo - printf "%-22s %s\n" "Mailpit enabled:" "$EN_MAILPIT" - $EN_MAILPIT && printf "%-22s %s\n" "Mailpit ports:" "SMTP ${MAILPIT_SMTP_PORT}, UI ${MAILPIT_UI_PORT}" - echo - printf "%-22s %s\n" "workflows2 worker:" "$EN_WF2_WORKER" - $EN_WF2_WORKER && { - printf "%-22s %s\n" "Worker container:" "${WF2_WORKER_NAME}" - printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" - } - echo - printf "%-22s %s\n" "SFTPGo enabled:" "$EN_SFTPGO" - $EN_SFTPGO && { - printf "%-22s %s\n" "SFTPGo ports:" "SFTP ${SFTPGO_SFTP_PORT}, Web ${SFTPGO_WEB_PORT}" - printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" - printf "%-22s %s\n" "SFTP backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" - printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" - } - echo "=========================================================================" -} - -# -------------------------- Build/Pull & Start ------------------------------- -build_trustpoint_image(){ [[ -f "$TP_DOCKERFILE" ]] || log "Dockerfile not found: $TP_DOCKERFILE"; log "Building trustpoint image..."; docker build -f "$TP_DOCKERFILE" -t "trustpoint:local" .; } -pull_trustpoint_image(){ log "Pulling ${APP_IMAGE} ..."; docker pull "${APP_IMAGE}" >/dev/null; } -configure_app_image_prompt(){ - if ask_yes_no "Build trustpoint locally from ${TP_DOCKERFILE}? (No = pull from Docker Hub)" "y"; then - BUILD_LOCAL=true - APP_IMAGE="trustpoint:local" - else - BUILD_LOCAL=false - ask "Docker Hub image tag to pull (repository ${TP_REPO})" "latest" - local tag="$REPLY" - APP_IMAGE="${TP_REPO}:${tag}" - fi -} -resolve_app_image(){ - if ! $EN_APP && ! $EN_WF2_WORKER; then - return 0 - fi - if $BUILD_LOCAL; then - build_trustpoint_image - else - pull_trustpoint_image - fi -} -configure_selected(){ - if $ONLY_DB; then - EN_PG=true - DB_INTERNAL=true - fi - $ONLY_MAIL && EN_MAILPIT=true - $ONLY_SFTP && EN_SFTPGO=true - - if $ONLY_APP; then - EN_APP=true - configure_app_image_prompt - EN_WF2_WORKER=$( - ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false - ) - elif $ONLY_WF2_WORKER; then - EN_WF2_WORKER=true - configure_app_image_prompt - fi - - if $ONLY_APP || $ONLY_WF2_WORKER; then - if $DB_INTERNAL; then - APP_DB_HOST="$DEF_DB_HOST_INTERNAL" - APP_DB_PORT=5432 - else - APP_DB_HOST="$DB_HOST" - APP_DB_PORT="$DB_PORT" - fi - APP_DB_NAME="$DB_NAME" - APP_DB_USER="$DB_USER" - APP_DB_PASS="$DB_PASS" - fi -} - -start_postgres(){ - $DB_INTERNAL || return 0 - ensure_volumes - local name="postgres" - stop_one "$name" - # safety: host port must still be free (non-interactive runs) - if port_in_use "$DB_PORT"; then die "Host port ${DB_PORT} is already in use. Choose another port or stop the process using it."; fi - log "Starting PostgreSQL..." - docker run -d --name "$name" --network "$NET" \ - -p "${DB_PORT}:5432" \ - -v "${VOL_DB}:/var/lib/postgresql/data" \ - -e "POSTGRES_DB=$DB_NAME" \ - -e "POSTGRES_USER=$DB_USER" \ - -e "POSTGRES_PASSWORD=$DB_PASS" \ - "$PG_IMAGE" >/dev/null -} - -start_mailpit(){ - $EN_MAILPIT || return 0 - local name="mailpit" - stop_one "$name" - if port_in_use "$MAILPIT_SMTP_PORT"; then die "Host port ${MAILPIT_SMTP_PORT} in use (Mailpit SMTP)."; fi - if port_in_use "$MAILPIT_UI_PORT"; then die "Host port ${MAILPIT_UI_PORT} in use (Mailpit UI)."; fi - log "Starting Mailpit..." - docker run -d --name "$name" --network "$NET" \ - -p "${MAILPIT_SMTP_PORT}:1025" \ - -p "${MAILPIT_UI_PORT}:8025" \ - "$MAILPIT_IMAGE" >/dev/null -} - -start_sftpgo(){ - $EN_SFTPGO || return 0 - local name="sftpgo" - stop_one "$name" - - if port_in_use "$SFTPGO_SFTP_PORT"; then die "Host port ${SFTPGO_SFTP_PORT} in use (SFTPGo SFTP)."; fi - if port_in_use "$SFTPGO_WEB_PORT"; then die "Host port ${SFTPGO_WEB_PORT} in use (SFTPGo Web)."; fi - - mkdir -p "${SFTPGO_ROOT}/data" - if [[ -n "$SFTPGO_BACKUP_USER" ]]; then - mkdir -p "${SFTPGO_ROOT}/data/${SFTPGO_BACKUP_USER}" - fi - chown -R 1000:1000 "${SFTPGO_ROOT}" 2>/dev/null || true - - log "Starting SFTPGo with auto-created admin..." - docker run -d --name "$name" --network "$NET" \ - -p "${SFTPGO_SFTP_PORT}:2022" \ - -p "${SFTPGO_WEB_PORT}:8080" \ - -v "${SFTPGO_ROOT}:/srv/sftpgo" \ - -e SFTPGO_DATA_PROVIDER__CREATE_DEFAULT_ADMIN=true \ - -e SFTPGO_DEFAULT_ADMIN_USERNAME="$SFTPGO_ADMIN_USER" \ - -e SFTPGO_DEFAULT_ADMIN_PASSWORD="$SFTPGO_ADMIN_PASS" \ - -e SFTPGO_HTTPD__BINDINGS__0__ADDRESS="0.0.0.0" \ - -e SFTPGO_HTTPD__BINDINGS__0__ENABLE_REST_API=true \ - -e SFTPGO_HTTPD__BINDINGS__0__ENABLE_WEB_ADMIN=true \ - -e SFTPGO_HTTPD__BINDINGS__0__PORT=8080 \ - -e SFTPGO_SFTPD__BINDINGS__0__PORT=2022 \ - "$SFTPGO_IMAGE" >/dev/null -} - -prepare_workflows2_worker_folder(){ - $EN_WF2_WORKER || return 0 - mkdir -p "$WF2_FOLDER" - chmod 700 "$WF2_FOLDER" 2>/dev/null || true - cat > "$WF2_WORKER_README" </dev/null || true - - cat > "$WF2_WORKER_ENV_FILE" <> "$WF2_WORKER_ENV_FILE" </dev/null || true -} - -start_app(){ - $EN_APP || return 0 - local name="trustpoint" - stop_one "$name" - # die early if 80/443 are busy - if port_in_use "$APP_HTTP_HOST"; then die "Host port ${APP_HTTP_HOST} is in use (trustpoint HTTP)."; fi - if port_in_use "$APP_HTTPS_HOST"; then die "Host port ${APP_HTTPS_HOST} is in use (trustpoint HTTPS)."; fi - - log "Starting trustpoint..." - local smtp_env=() - if $EN_MAILPIT; then - smtp_env+=( -e "EMAIL_HOST=mailpit" -e "EMAIL_PORT=1025" -e "EMAIL_USE_TLS=0" -e "EMAIL_USE_SSL=0" -e "DEFAULT_FROM_EMAIL=no-reply@trustpoint.local" ) - fi - docker run -d --name "$name" --network "$NET" \ - -p "${APP_HTTP_HOST}:80" \ - -p "${APP_HTTPS_HOST}:443" \ - -e "POSTGRES_DB=$APP_DB_NAME" \ - -e "DATABASE_USER=$APP_DB_USER" \ - -e "DATABASE_PASSWORD=$APP_DB_PASS" \ - -e "DATABASE_HOST=$APP_DB_HOST" \ - -e "DATABASE_PORT=$APP_DB_PORT" \ - "${smtp_env[@]}" \ - "$APP_IMAGE" >/dev/null -} - -start_workflows2_worker(){ - $EN_WF2_WORKER || return 0 - local name="$WF2_WORKER_NAME" - stop_one "$name" - prepare_workflows2_worker_folder - log "Starting dedicated workflows2 worker..." - docker run -d --name "$name" --network "$NET" \ - --env-file "$WF2_WORKER_ENV_FILE" \ - "$APP_IMAGE" >/dev/null -} - -# -------------------------- Readiness & Provision ----------------------------- -await_sftpgo_ready(){ - $EN_SFTPGO || return 0 - local PORT; PORT="$(sftpgo_web_port)" - echo "Waiting (<= ${READINESS_TIMEOUT}s) for SFTPGo API on localhost:${PORT} ..." - local until=$(( $(date +%s) + READINESS_TIMEOUT )) - while (( $(date +%s) < until )); do - if have curl && [[ "$(curl -fsS "http://127.0.0.1:${PORT}/healthz" 2>/dev/null || true)" == "ok" ]]; then - ok "SFTPGo API healthy on :${PORT}" - return 0 - fi - if tcp_check 127.0.0.1 "$PORT" 1; then - ok "SFTPGo API port open on :${PORT}" - return 0 - fi - printf "."; sleep 1 - done - echo - warn "SFTPGo API not confirmed after ${READINESS_TIMEOUT}s" -} - -await_readiness(){ - local deadline=$(( $(date +%s) + READINESS_TIMEOUT )) - if $DB_INTERNAL; then - echo "Waiting (<= ${READINESS_TIMEOUT}s) for PostgreSQL on localhost:${DB_PORT} ..." - while (( $(date +%s) < deadline )); do - if tcp_check 127.0.0.1 "$DB_PORT" 1; then ok "PostgreSQL ready on :$DB_PORT"; break; fi - printf "."; sleep 1 - done - echo - fi - if $EN_APP; then - echo "Waiting (<= ${READINESS_TIMEOUT}s) for trustpoint HTTP on localhost:${APP_HTTP_HOST} ..." - while (( $(date +%s) < deadline )); do - if tcp_check 127.0.0.1 "$APP_HTTP_HOST" 1; then ok "trustpoint reachable on :$APP_HTTP_HOST"; break; fi - printf "."; sleep 1 - done - echo - fi - await_sftpgo_ready -} - -# ---- SFTPGo provisioning via REST ------------------------------------------- -upsert_virtual_folder(){ - local vf_name="$1" mapped="$2" - read -r -d '' VF_PAYLOAD <&2 - return 1 - fi - ok "Virtual folder '${vf_name}' → '${mapped}' ready." -} - -provision_sftpgo_backup_user(){ - $EN_SFTPGO || return 0 - have curl || { warn "curl not found on host; skipping SFTPGo user provisioning."; return 0; } - - [[ -z "${SFTPGO_BACKUP_HOME:-}" ]] && SFTPGO_BACKUP_HOME="/srv/sftpgo/data/${SFTPGO_BACKUP_USER}" - - local host_home="${SFTPGO_ROOT}${SFTPGO_BACKUP_HOME#/srv/sftpgo}" - mkdir -p "$host_home" - chown -R 1000:1000 "$host_home" 2>/dev/null || true - - local PORT; PORT="$(sftpgo_web_port)" - API="http://127.0.0.1:${PORT}" - - for _ in {1..30}; do - [[ "$(curl -fsS "${API}/healthz" 2>/dev/null || true)" == "ok" ]] && break - sleep 1 - done - - local token="" - for _ in 1 2 3; do - token="$(curl -fsS -u "${SFTPGO_ADMIN_USER}:${SFTPGO_ADMIN_PASS}" "${API}/api/v2/token" \ - | sed -nE 's/.*"access_token":"([^"]+)".*/\1/p')" || true - [[ -n "$token" ]] && break - sleep 1 - done - [[ -n "$token" ]] || { warn "Could not obtain SFTPGo admin token; skipping user provisioning."; return 0; } - - HDR=(-H "Authorization: Bearer ${token}" -H "Content-Type: application/json") - upsert_virtual_folder "trustpoint" "${SFTPGO_BACKUP_HOME}" || return 0 - - local code method url http - code="$(curl -s -o /dev/null -w '%{http_code}' "${HDR[@]}" "${API}/api/v2/users/${SFTPGO_BACKUP_USER}")" - if [[ "$code" == "200" ]]; then - method=PUT; url="${API}/api/v2/users/${SFTPGO_BACKUP_USER}" - else - method=POST; url="${API}/api/v2/users" - fi - - read -r -d '' payload <&2 - else - ok "SFTPGo user '${SFTPGO_BACKUP_USER}' provisioned; VF mounted at /upload." - fi -} - -# ---- TLS fingerprint wait ---------------------------------------------------- -extract_tls_fingerprint_once(){ - local logs="$1" - if [[ "$logs" =~ ([0-9A-Fa-f]{2}:){31}[0-9A-Fa-f]{2} ]]; then TLS_FP_FOUND="${BASH_REMATCH[0]}"; return 0; fi - if [[ "$logs" =~ ([0-9A-Fa-f]{64}) ]]; then TLS_FP_FOUND="${BASH_REMATCH[1]}"; return 0; fi - if [[ "$logs" =~ [Ss][Hh][Aa]-?256[:\ ]([A-Za-z0-9+/=_:-]{43,}) ]]; then TLS_FP_FOUND="SHA256:${BASH_REMATCH[1]}"; return 0; fi - return 1 -} - -wait_tls_fingerprint(){ - $EN_APP || { TLS_FP_ELAPSED=0; return 0; } - local start; start="$(date +%s)" - local start_iso; start_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - local end=$(( start + TLS_FP_TIMEOUT )) - while (( $(date +%s) < end )); do - local chunk - chunk="$(docker logs --since "$start_iso" trustpoint 2>/dev/null || true)" - if extract_tls_fingerprint_once "$chunk"; then - TLS_FP_ELAPSED=$(( $(date +%s) - start )) - return 0 - fi - sleep 3 - done - TLS_FP_ELAPSED=$(( $(date +%s) - start )) - return 1 -} - -mailpit_has_subject(){ - local subject="$1" - have curl || return 2 - local until=$(( $(date +%s) + MAILPIT_PROBE_TIMEOUT )) - local api="http://127.0.0.1:${MAILPIT_UI_PORT}/api/v1/messages" - while (( $(date +%s) < until )); do - if curl -fsS "$api" 2>/dev/null | grep -Fq "$subject"; then - return 0 - fi - sleep 1 - done - return 1 -} - -probe_mailpit_from_container(){ - local container="$1" label="$2" - exists "$container" || return 0 - - local subject="trustpoint wizard ${label} mailpit probe $(date +%s)" - log "Sending Mailpit probe email from ${label} container..." - - if ! docker exec -e "PROBE_SUBJECT=${subject}" "$container" bash -lc \ - 'cd /var/www/html/trustpoint && uv run trustpoint/manage.py shell -c '\''import os; from django.conf import settings; from django.core.mail import send_mail; subject=os.environ["PROBE_SUBJECT"]; send_mail(subject, "Trustpoint Mailpit probe.", getattr(settings, "DEFAULT_FROM_EMAIL", None), ["demo@trustpoint.local"], fail_silently=False)'\''' \ - >/dev/null 2>&1; then - warn "Mailpit probe failed from ${label} container." - return 1 - fi - - if ! have curl; then - warn "curl not found on host; Mailpit probe from ${label} was sent but API verification was skipped." - return 0 - fi - - if mailpit_has_subject "$subject"; then - ok "Mailpit received the ${label} probe email." - return 0 - fi - - warn "Mailpit SMTP accepted the ${label} probe email, but it did not appear in the Mailpit UI within ${MAILPIT_PROBE_TIMEOUT}s." - return 1 -} - -verify_mailpit_delivery(){ - $EN_MAILPIT || return 0 - $EN_APP && probe_mailpit_from_container trustpoint "web" || true - $EN_WF2_WORKER && probe_mailpit_from_container "$WF2_WORKER_NAME" "workflows2-worker" || true -} - -show_runtime_status(){ - local net_state="absent" vol_state="absent" - docker network inspect "$NET" >/dev/null 2>&1 && net_state="present" - docker volume inspect "$VOL_DB" >/dev/null 2>&1 && vol_state="present" - - echo - echo "=========================== Runtime Status (Live) ========================" - printf "%-22s %s\n" "Network:" "${NET} (${net_state})" - printf "%-22s %s\n" "DB volume:" "${VOL_DB} (${vol_state})" - printf "%-22s %s\n" "workflow2 folder:" "$([ -d "$WF2_FOLDER" ] && echo "${WF2_FOLDER} (present)" || echo "${WF2_FOLDER} (absent)")" - echo - printf "%-20s %-10s %-10s %s\n" "Container" "State" "Health" "Image" - print_container_status_row trustpoint - print_container_status_row postgres - print_container_status_row mailpit - print_container_status_row sftpgo - print_container_status_row "$WF2_WORKER_NAME" - echo - - if exists trustpoint; then - local http_port https_port db_host db_port db_name db_user db_pass - http_port="$(container_host_port trustpoint 80/tcp)" - https_port="$(container_host_port trustpoint 443/tcp)" - db_host="$(container_env trustpoint DATABASE_HOST)" - db_port="$(container_env trustpoint DATABASE_PORT)" - db_name="$(container_env trustpoint POSTGRES_DB)" - db_user="$(container_env trustpoint DATABASE_USER)" - db_pass="$(container_env trustpoint DATABASE_PASSWORD)" - - [[ -n "$http_port" ]] && printf "%-22s %s\n" "trustpoint HTTP:" "http://localhost:${http_port}" - [[ -n "$https_port" ]] && printf "%-22s %s\n" "trustpoint HTTPS:" "https://localhost:${https_port}" - printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings" - if [[ -n "$db_host" || -n "$db_port" || -n "$db_name" || -n "$db_user" ]]; then - printf "%-22s %s\n" "DB connect:" "host=${db_host:-?} port=${db_port:-?} db=${db_name:-?} user=${db_user:-?} pass=$(mask "${db_pass:-}")" - fi - fi - - if exists postgres; then - local pg_port - pg_port="$(container_host_port postgres 5432/tcp)" - [[ -n "$pg_port" ]] && printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${pg_port}" - fi - - if exists mailpit; then - local mailpit_ui mailpit_smtp - mailpit_ui="$(container_host_port mailpit 8025/tcp)" - mailpit_smtp="$(container_host_port mailpit 1025/tcp)" - [[ -n "$mailpit_ui" ]] && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${mailpit_ui}" - [[ -n "$mailpit_smtp" ]] && printf "%-22s %s\n" "Mailpit SMTP:" "localhost:${mailpit_smtp}" - fi - - if exists "$WF2_WORKER_NAME"; then - local worker_db worker_lease worker_batch worker_sleep - worker_db="$(container_env "$WF2_WORKER_NAME" DATABASE_HOST)" - worker_lease="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_LEASE)" - worker_batch="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_BATCH)" - worker_sleep="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_SLEEP)" - printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" - [[ -n "$worker_db" ]] && printf "%-22s %s\n" "worker DB host:" "${worker_db}" - [[ -n "$worker_lease" || -n "$worker_batch" || -n "$worker_sleep" ]] && \ - printf "%-22s %s\n" "worker tuning:" "lease=${worker_lease:-?} batch=${worker_batch:-?} sleep=${worker_sleep:-?}" - fi - - if exists sftpgo; then - local sftpgo_web sftpgo_sftp sftpgo_admin - sftpgo_web="$(container_host_port sftpgo 8080/tcp)" - sftpgo_sftp="$(container_host_port sftpgo 2022/tcp)" - sftpgo_admin="$(container_env sftpgo SFTPGO_DEFAULT_ADMIN_USERNAME)" - [[ -n "$sftpgo_web" ]] && printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${sftpgo_web}/web/admin" - [[ -n "$sftpgo_sftp" ]] && printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${sftpgo_sftp}" - [[ -n "$sftpgo_admin" ]] && printf "%-22s %s\n" "SFTPGo admin:" "${sftpgo_admin}" - printf "%-22s %s\n" "SFTPGo data dir:" "${SFTPGO_ROOT}" - fi - - echo "=========================================================================" -} - -# -------------------------- Summary ------------------------------------------ -final_summary(){ - echo - echo "========================= Runtime Summary (Actual) =======================" - printf "%-22s %s\n" "Network:" "$NET" - printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker)$' || true)" - echo - if $EN_APP; then - printf "%-22s %s\n" "trustpoint:" "http://localhost:80 | https://localhost:443" - printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings (default: auto)" - fi - if $DB_INTERNAL; then - printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${DB_PORT} (container port 5432)" - fi - if $EN_APP; then - printf "%-22s %s\n" "DB connect:" "host=${APP_DB_HOST} port=${APP_DB_PORT} db=${APP_DB_NAME} user=${APP_DB_USER} pass=$(mask "$APP_DB_PASS")" - fi - $EN_MAILPIT && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${MAILPIT_UI_PORT} (SMTP :${MAILPIT_SMTP_PORT})" - if $EN_WF2_WORKER; then - printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" - printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" - fi - if $EN_SFTPGO; then - local PORT; PORT="$(sftpgo_web_port)" - printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${PORT}/web/admin" - printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${SFTPGO_SFTP_PORT}" - printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" - printf "%-22s %s\n" "Backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" - printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" - printf "%-22s %s\n" "Backup URL:" "sftp://${SFTPGO_BACKUP_USER}:***@127.0.0.1:${SFTPGO_SFTP_PORT}/" - printf "%-22s %s\n" "Data dir:" "${SFTPGO_ROOT}" - fi - if $EN_APP; then - if [[ -n "$TLS_FP_FOUND" ]]; then - printf "%-22s %s\n" "TLS fingerprint:" "$TLS_FP_FOUND" - else - if $NOWAIT; then - printf "%-22s %s\n" "TLS fingerprint:" "skipped (NOWAIT)" - else - printf "%-22s %s\n" "TLS fingerprint:" "not found yet (polled ${TLS_FP_ELAPSED}s; timeout ${TLS_FP_TIMEOUT}s)" - fi - fi - fi - echo "=========================================================================" -} - -# -------------------------- High-level orchestration ------------------------- -wizard(){ - echo "$(bold)trustpoint Setup Wizard$(rst)" - ensure_network - step_enable_trustpoint - step_trustpoint_source - step_enable_postgres - step_postgres_config - step_app_db_binding - step_helpers - step_workflows2_worker - show_plan - ask_yes_no "Proceed with these settings?" "y" || { warn "Aborted by user."; exit 1; } - resolve_app_image - $DB_INTERNAL && ensure_volumes - start_postgres - start_mailpit - start_sftpgo - $EN_WF2_WORKER || stop_one "$WF2_WORKER_NAME" - start_app - start_workflows2_worker - $NOWAIT || await_readiness - $NOWAIT || provision_sftpgo_backup_user - $NOWAIT || verify_mailpit_delivery - $NOWAIT || wait_tls_fingerprint || true - final_summary -} - -# -------------------------- Service selection & CLI -------------------------- -usage(){ - cat <<'EOF2' -Commands: - (no command) Run interactive wizard - up [demo|trustpoint|db|mail|sftp|worker] [--nowait] - down [demo|trustpoint|db|mail|sftp|worker] - logs [trustpoint|db|mail|sftp|worker] - status - nuke - help - -Also supported (legacy): --only trustpoint|db|mail|sftp|worker|demo -EOF2 -} - -map_only_to_flags(){ - case "$1" in - demo) ONLY_APP=true; ONLY_DB=true; ONLY_MAIL=true; ONLY_SFTP=true ;; - trustpoint|app) ONLY_APP=true ;; - db) ONLY_DB=true ;; - mail) ONLY_MAIL=true ;; - sftp) ONLY_SFTP=true ;; - worker) ONLY_WF2_WORKER=true ;; - *) die "Unknown target: $1 (use trustpoint|db|mail|sftp|worker|demo)";; - esac -} - -set_targets_from_args(){ - local any=false - while [[ $# -gt 0 ]]; do - case "$1" in - demo|trustpoint|app|db|mail|sftp|worker) map_only_to_flags "$1"; any=true; shift ;; - --only) map_only_to_flags "${2:-}"; any=true; shift 2 ;; - --nowait) NOWAIT=true; shift ;; - *) die "Unknown option/target: $1" ;; - esac - done - if ! $any; then ONLY_APP=true; ONLY_DB=true; fi -} - -start_selected(){ - configure_selected - ensure_network - resolve_app_image - $ONLY_DB && { EN_PG=true; ensure_volumes; start_postgres; } - $ONLY_MAIL && { EN_MAILPIT=true; start_mailpit; } - $ONLY_SFTP && { EN_SFTPGO=true; start_sftpgo; } - $EN_WF2_WORKER || { $ONLY_APP && stop_one "$WF2_WORKER_NAME"; } - $ONLY_APP && start_app - $EN_WF2_WORKER && start_workflows2_worker - - $NOWAIT || await_readiness - $NOWAIT || provision_sftpgo_backup_user - $NOWAIT || verify_mailpit_delivery - $NOWAIT || wait_tls_fingerprint || true - final_summary -} - -down_selected(){ - local done=false - $ONLY_APP && { stop_one trustpoint; stop_one "$WF2_WORKER_NAME"; done=true; } - $ONLY_DB && stop_one postgres && done=true - $ONLY_MAIL && stop_one mailpit && done=true - $ONLY_SFTP && stop_one sftpgo && done=true - $ONLY_WF2_WORKER && stop_one "$WF2_WORKER_NAME" && done=true - $done || { stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME"; } - ok "Stopped." -} - -logs_selected(){ - local target="trustpoint" - $ONLY_DB && target="postgres" - $ONLY_MAIL && target="mailpit" - $ONLY_SFTP && target="sftpgo" - $ONLY_WF2_WORKER && target="$WF2_WORKER_NAME" - exists "$target" || die "Container not found: $target" - docker logs -f "$target" -} - -nuke_cmd(){ - read -r -p "Remove ALL project containers, network, DB volume, ./sftpgo-data, and ./workflow2Folder? [y/N] " a; [[ "${a}" == "y" ]] || exit 0 - read -r -p "Are you sure? This is destructive. [y/N] " b; [[ "${b}" == "y" ]] || exit 0 - mapfile -t project_volumes < <(collect_project_volumes) - stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME" - docker network rm "$NET" >/dev/null 2>&1 || true - for v in "${project_volumes[@]}"; do - [[ -n "$v" ]] || continue - docker volume rm "$v" >/dev/null 2>&1 || true - done - if [[ -d "$SFTPGO_ROOT" ]]; then rm -rf "$SFTPGO_ROOT"; fi - if [[ -d "$WF2_FOLDER" ]]; then rm -rf "$WF2_FOLDER"; fi - ok "Project resources removed." -} - -# -------------------------- Arg parsing & dispatch ---------------------------- -cmd="${1:-}" -preflight -case "$cmd" in - "" ) wizard ;; - help) usage ;; - up) - shift || true - set_targets_from_args "$@" - start_selected - ;; - down) - shift || true - set_targets_from_args "$@" - down_selected - ;; - logs) - shift || true - set_targets_from_args "$@" - logs_selected - ;; - status) - shift || true - [[ $# -eq 0 ]] || die "status does not take targets. Use it without arguments." - show_runtime_status - ;; - nuke) nuke_cmd ;; - *) usage; die "Unknown command: $cmd" ;; -esac +tp_main "$@" From fd8e69d49cf42d04ce03f0ab12e100429839eed2 Mon Sep 17 00:00:00 2001 From: BytesWelder Date: Mon, 22 Jun 2026 12:26:05 +0200 Subject: [PATCH 15/18] Add monitoring --- scripts/tp_wizard/README.md | 46 ++++---- scripts/tp_wizard/bootstrap.sh | 1 + scripts/tp_wizard/cli.sh | 85 ++++++++++++--- scripts/tp_wizard/commands/down.sh | 5 +- scripts/tp_wizard/commands/logs.sh | 3 +- scripts/tp_wizard/commands/nuke.sh | 19 +++- scripts/tp_wizard/commands/up.sh | 15 ++- scripts/tp_wizard/defaults.sh | 14 +++ scripts/tp_wizard/runtime.sh | 7 +- scripts/tp_wizard/services/monitoring.sh | 131 +++++++++++++++++++++++ scripts/tp_wizard/state.sh | 22 +++- scripts/tp_wizard/summary.sh | 56 ++++++++-- scripts/tp_wizard/wizard.sh | 1 + 13 files changed, 345 insertions(+), 60 deletions(-) create mode 100644 scripts/tp_wizard/services/monitoring.sh diff --git a/scripts/tp_wizard/README.md b/scripts/tp_wizard/README.md index 9569778c8..308df8646 100644 --- a/scripts/tp_wizard/README.md +++ b/scripts/tp_wizard/README.md @@ -2,35 +2,43 @@ `tp_wizard.sh` is the developer-facing setup helper for the local trustpoint Docker stack. -It can run the interactive setup wizard or manage selected services: trustpoint, PostgreSQL, Mailpit, SFTPGo, and the optional workflows2 worker. +It can run the interactive setup wizard or manage selected runtime services: trustpoint, PostgreSQL, Mailpit, SFTPGo, workflows2 worker, Prometheus, and Grafana. -## Usage +## Commands Run from the repository root: ```bash ./tp_wizard.sh -./tp_wizard.sh up [demo|trustpoint|db|mail|sftp|worker] [--nowait] -./tp_wizard.sh down [demo|trustpoint|db|mail|sftp|worker] -./tp_wizard.sh logs [trustpoint|db|mail|sftp|worker] +./tp_wizard.sh up [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] [--nowait] +./tp_wizard.sh down [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] +./tp_wizard.sh logs [trustpoint|db|mail|sftp|worker|prometheus|grafana] ./tp_wizard.sh status ./tp_wizard.sh nuke ``` +Demo presets: + +```bash +./tp_wizard.sh up demo light # trustpoint + PostgreSQL +./tp_wizard.sh up demo # trustpoint + PostgreSQL + Mailpit + SFTPGo + workflows2 worker +./tp_wizard.sh up demo full # demo + Prometheus + Grafana +``` + ## Design -The root `tp_wizard.sh` is only the public entrypoint. The implementation lives in `scripts/tp_wizard/`. +The root `tp_wizard.sh` stays small and only bootstraps the implementation in `scripts/tp_wizard/`. ```text -defaults.sh constants and default values -state.sh mutable wizard/runtime state -cli.sh argument parsing and dispatch -wizard.sh interactive wizard flow -runtime.sh shared start/wait/provision/summary orchestration -summary.sh plan, status, and final summary output -lib/ generic helpers -services/ service-specific prompt/start/wait/provision logic -commands/ command handlers +defaults.sh constants and default values +state.sh mutable wizard/runtime state +cli.sh argument parsing and dispatch +wizard.sh interactive wizard flow +runtime.sh shared start/wait/provision/summary orchestration +summary.sh planned/live/final output +lib/ generic helpers +services/ service-specific prompt/start/wait/provision logic +commands/ command handlers ``` Dependency direction: @@ -40,10 +48,4 @@ cli -> commands -> runtime -> services -> lib wizard -> runtime -> services -> lib ``` -Rules: - -- `lib/` must not call service or command functions. -- `services/` may use `lib/`, but should not parse CLI arguments. -- `commands/` should stay thin and delegate shared work to `runtime.sh`. -- `runtime.sh` owns orchestration used by both wizard and CLI mode. -- The root `tp_wizard.sh` should remain small and stable. +Keep command handlers thin. Shared startup logic belongs in `runtime.sh`; service details belong in `services/`. diff --git a/scripts/tp_wizard/bootstrap.sh b/scripts/tp_wizard/bootstrap.sh index f40b392fd..270d417c9 100644 --- a/scripts/tp_wizard/bootstrap.sh +++ b/scripts/tp_wizard/bootstrap.sh @@ -17,6 +17,7 @@ source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/trustpoint.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/mailpit.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/sftpgo.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/workflows2_worker.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/monitoring.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/runtime.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/wizard.sh" diff --git a/scripts/tp_wizard/cli.sh b/scripts/tp_wizard/cli.sh index 89d208d9d..a831bb447 100644 --- a/scripts/tp_wizard/cli.sh +++ b/scripts/tp_wizard/cli.sh @@ -2,45 +2,106 @@ usage(){ cat <<'EOF2' Commands: (no command) Run interactive wizard - up [demo|trustpoint|db|mail|sftp|worker] [--nowait] - down [demo|trustpoint|db|mail|sftp|worker] - logs [trustpoint|db|mail|sftp|worker] + + up [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] [--nowait] + down [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] + logs [trustpoint|db|mail|sftp|worker|prometheus|grafana] status nuke help +Demo presets: + up demo light trustpoint + PostgreSQL + up demo trustpoint + PostgreSQL + Mailpit + SFTPGo + workflows2 worker + up demo full demo + Prometheus + Grafana + Also supported (legacy): --only trustpoint|db|mail|sftp|worker|demo EOF2 } +map_demo_preset_to_flags(){ + local preset="${1:-default}" + case "$preset" in + light) + DEMO_PRESET="light" + ONLY_APP=true + ONLY_DB=true + ;; + default|demo) + DEMO_PRESET="demo" + ONLY_APP=true + ONLY_DB=true + ONLY_MAIL=true + ONLY_SFTP=true + ONLY_WF2_WORKER=true + ;; + full) + DEMO_PRESET="full" + ONLY_APP=true + ONLY_DB=true + ONLY_MAIL=true + ONLY_SFTP=true + ONLY_WF2_WORKER=true + ONLY_PROMETHEUS=true + ONLY_GRAFANA=true + ;; + *) + die "Unknown demo preset: $preset (use light|full, or omit it for default demo)" + ;; + esac +} map_only_to_flags(){ case "$1" in - demo) ONLY_APP=true; ONLY_DB=true; ONLY_MAIL=true; ONLY_SFTP=true ;; - trustpoint|app) ONLY_APP=true ;; - db) ONLY_DB=true ;; + demo) map_demo_preset_to_flags default ;; + demo-light|light) map_demo_preset_to_flags light ;; + demo-full|full) map_demo_preset_to_flags full ;; + trustpoint|app) ONLY_APP=true ;; + db) ONLY_DB=true ;; mail) ONLY_MAIL=true ;; sftp) ONLY_SFTP=true ;; worker) ONLY_WF2_WORKER=true ;; - *) die "Unknown target: $1 (use trustpoint|db|mail|sftp|worker|demo)";; + prometheus|prom) ONLY_PROMETHEUS=true ;; + grafana) ONLY_GRAFANA=true ;; + monitoring|metrics) ONLY_PROMETHEUS=true; ONLY_GRAFANA=true ;; + *) die "Unknown target: $1 (use demo|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring)" ;; esac } - set_targets_from_args(){ local any=false while [[ $# -gt 0 ]]; do case "$1" in - demo|trustpoint|app|db|mail|sftp|worker) map_only_to_flags "$1"; any=true; shift ;; - --only) map_only_to_flags "${2:-}"; any=true; shift 2 ;; - --nowait) NOWAIT=true; shift ;; + demo) + if [[ "${2:-}" == "light" || "${2:-}" == "full" ]]; then + map_demo_preset_to_flags "$2" + shift 2 + else + map_demo_preset_to_flags default + shift + fi + any=true + ;; + demo-light|demo-full|light|full|trustpoint|app|db|mail|sftp|worker|prometheus|prom|grafana|monitoring|metrics) + map_only_to_flags "$1" + any=true + shift + ;; + --only) + map_only_to_flags "${2:-}" + any=true + shift 2 + ;; + --nowait) + NOWAIT=true + shift + ;; *) die "Unknown option/target: $1" ;; esac done if ! $any; then ONLY_APP=true; ONLY_DB=true; fi } - tp_main(){ local cmd="${1:-}" diff --git a/scripts/tp_wizard/commands/down.sh b/scripts/tp_wizard/commands/down.sh index 8c9813444..f8bfd5dbc 100644 --- a/scripts/tp_wizard/commands/down.sh +++ b/scripts/tp_wizard/commands/down.sh @@ -5,11 +5,12 @@ down_selected(){ $ONLY_MAIL && stop_one mailpit && done=true $ONLY_SFTP && stop_one sftpgo && done=true $ONLY_WF2_WORKER && stop_one "$WF2_WORKER_NAME" && done=true - $done || { stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME"; } + $ONLY_PROMETHEUS && stop_one prometheus && done=true + $ONLY_GRAFANA && stop_one grafana && done=true + $done || { stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME"; stop_one prometheus; stop_one grafana; } ok "Stopped." } - cmd_down(){ set_targets_from_args "$@" down_selected diff --git a/scripts/tp_wizard/commands/logs.sh b/scripts/tp_wizard/commands/logs.sh index 56544ab00..27954e5eb 100644 --- a/scripts/tp_wizard/commands/logs.sh +++ b/scripts/tp_wizard/commands/logs.sh @@ -4,11 +4,12 @@ logs_selected(){ $ONLY_MAIL && target="mailpit" $ONLY_SFTP && target="sftpgo" $ONLY_WF2_WORKER && target="$WF2_WORKER_NAME" + $ONLY_PROMETHEUS && target="prometheus" + $ONLY_GRAFANA && target="grafana" exists "$target" || die "Container not found: $target" docker logs -f "$target" } - cmd_logs(){ set_targets_from_args "$@" logs_selected diff --git a/scripts/tp_wizard/commands/nuke.sh b/scripts/tp_wizard/commands/nuke.sh index 0ef235556..d32f8fa14 100644 --- a/scripts/tp_wizard/commands/nuke.sh +++ b/scripts/tp_wizard/commands/nuke.sh @@ -1,19 +1,30 @@ nuke_cmd(){ - read -r -p "Remove ALL project containers, network, DB volume, ./sftpgo-data, and ./workflow2Folder? [y/N] " a; [[ "${a}" == "y" ]] || exit 0 - read -r -p "Are you sure? This is destructive. [y/N] " b; [[ "${b}" == "y" ]] || exit 0 + read -r -p "Remove ALL project containers, network, DB/Grafana volumes, ./sftpgo-data, ./workflow2Folder, and ./grafana-provisioning? [y/N] " a + [[ "${a}" == "y" ]] || exit 0 + read -r -p "Are you sure? This is destructive. [y/N] " b + [[ "${b}" == "y" ]] || exit 0 + mapfile -t project_volumes < <(collect_project_volumes) - stop_one trustpoint; stop_one postgres; stop_one mailpit; stop_one sftpgo; stop_one "$WF2_WORKER_NAME" + stop_one trustpoint + stop_one postgres + stop_one mailpit + stop_one sftpgo + stop_one "$WF2_WORKER_NAME" + stop_one prometheus + stop_one grafana docker network rm "$NET" >/dev/null 2>&1 || true + for v in "${project_volumes[@]}"; do [[ -n "$v" ]] || continue docker volume rm "$v" >/dev/null 2>&1 || true done + if [[ -d "$SFTPGO_ROOT" ]]; then rm -rf "$SFTPGO_ROOT"; fi if [[ -d "$WF2_FOLDER" ]]; then rm -rf "$WF2_FOLDER"; fi + if [[ -d "$GRAFANA_PROVISIONING_ROOT" ]]; then rm -rf "$GRAFANA_PROVISIONING_ROOT"; fi ok "Project resources removed." } - cmd_nuke(){ nuke_cmd } diff --git a/scripts/tp_wizard/commands/up.sh b/scripts/tp_wizard/commands/up.sh index 7728aa816..9b62f0b81 100644 --- a/scripts/tp_wizard/commands/up.sh +++ b/scripts/tp_wizard/commands/up.sh @@ -5,18 +5,26 @@ configure_selected(){ fi $ONLY_MAIL && EN_MAILPIT=true $ONLY_SFTP && EN_SFTPGO=true + $ONLY_PROMETHEUS && EN_PROMETHEUS=true + $ONLY_GRAFANA && EN_GRAFANA=true if $ONLY_APP; then EN_APP=true configure_app_image_prompt - EN_WF2_WORKER=$( - ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false - ) + if [[ -z "$DEMO_PRESET" ]]; then + EN_WF2_WORKER=$( + ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false + ) + fi elif $ONLY_WF2_WORKER; then EN_WF2_WORKER=true configure_app_image_prompt fi + if $ONLY_WF2_WORKER; then + EN_WF2_WORKER=true + fi + if $ONLY_APP || $ONLY_WF2_WORKER; then if $DB_INTERNAL; then APP_DB_HOST="$DEF_DB_HOST_INTERNAL" @@ -31,7 +39,6 @@ configure_selected(){ fi } - cmd_up(){ set_targets_from_args "$@" configure_selected diff --git a/scripts/tp_wizard/defaults.sh b/scripts/tp_wizard/defaults.sh index f58424a99..1b427d94b 100644 --- a/scripts/tp_wizard/defaults.sh +++ b/scripts/tp_wizard/defaults.sh @@ -2,6 +2,7 @@ PROJECT="trustpoint" NET="${PROJECT}-net" VOL_DB="${PROJECT}_postgres_data" +VOL_GRAFANA="${PROJECT}_grafana_data" # trustpoint image handling TP_DOCKERFILE="docker/trustpoint/Dockerfile" @@ -13,6 +14,8 @@ BUILD_LOCAL=false PG_IMAGE="postgres:15.14" MAILPIT_IMAGE="axllent/mailpit:v1.27" SFTPGO_IMAGE="drakkan/sftpgo:2.6.x-slim" +PROMETHEUS_IMAGE="prom/prometheus:latest" +GRAFANA_IMAGE="grafana/grafana:latest" WF2_WORKER_NAME="trustpoint-worker" # Fixed trustpoint ports @@ -36,6 +39,8 @@ DEF_SFTPGO_WEB_PORT=8080 DEF_SFTPGO_ADMIN_USER="admin" DEF_SFTPGO_ADMIN_PASS="testing321" SFTPGO_ROOT="${PWD}/sftpgo-data" + +# workflows2 worker defaults WF2_FOLDER="${PWD}/workflow2Folder" WF2_WORKER_ENV_FILE="${WF2_FOLDER}/worker.env" WF2_WORKER_README="${WF2_FOLDER}/README.txt" @@ -44,6 +49,15 @@ DEF_WF2_WORKER_BATCH=10 DEF_WF2_WORKER_SLEEP=1 MAILPIT_PROBE_TIMEOUT=20 +# Monitoring defaults +DEF_PROMETHEUS_PORT=9090 +DEF_GRAFANA_PORT=3000 +DEF_GRAFANA_ADMIN_USER="admin" +DEF_GRAFANA_ADMIN_PASS="testing321" +PROMETHEUS_CONFIG="${PWD}/prometheus/prometheus.yml" +GRAFANA_PROVISIONING_ROOT="${PWD}/grafana-provisioning" +GRAFANA_DATASOURCES_DIR="${GRAFANA_PROVISIONING_ROOT}/datasources" + # Timeouts READINESS_TIMEOUT=90 TLS_FP_TIMEOUT=150 diff --git a/scripts/tp_wizard/runtime.sh b/scripts/tp_wizard/runtime.sh index 199cf5026..8303ad815 100644 --- a/scripts/tp_wizard/runtime.sh +++ b/scripts/tp_wizard/runtime.sh @@ -19,10 +19,9 @@ await_readiness(){ echo fi await_sftpgo_ready + await_monitoring_ready } -# ---- SFTPGo provisioning via REST ------------------------------------------- - runtime_after_start(){ $NOWAIT || await_readiness $NOWAIT || provision_sftpgo_backup_user @@ -40,6 +39,8 @@ runtime_start_enabled(){ $EN_WF2_WORKER || stop_one "$WF2_WORKER_NAME" start_app start_workflows2_worker + start_prometheus + start_grafana runtime_after_start } @@ -51,6 +52,8 @@ runtime_start_selected(){ $EN_WF2_WORKER || { $ONLY_APP && stop_one "$WF2_WORKER_NAME"; } $ONLY_APP && start_app $EN_WF2_WORKER && start_workflows2_worker + $ONLY_PROMETHEUS && { EN_PROMETHEUS=true; start_prometheus; } + $ONLY_GRAFANA && { EN_GRAFANA=true; start_grafana; } runtime_after_start } diff --git a/scripts/tp_wizard/services/monitoring.sh b/scripts/tp_wizard/services/monitoring.sh new file mode 100644 index 000000000..2b2a5da94 --- /dev/null +++ b/scripts/tp_wizard/services/monitoring.sh @@ -0,0 +1,131 @@ +monitoring_prompt_config(){ + EN_PROMETHEUS=$(ask_yes_no "Enable Prometheus metrics stack?" "n" && echo true || echo false) + if $EN_PROMETHEUS; then + PROMETHEUS_PORT="$(ask_free_port 'Prometheus host port' "$PROMETHEUS_PORT")" + ask "Prometheus config file" "$PROMETHEUS_CONFIG" + PROMETHEUS_CONFIG="$REPLY" + fi + + EN_GRAFANA=$(ask_yes_no "Enable Grafana dashboard UI?" "n" && echo true || echo false) + if $EN_GRAFANA; then + GRAFANA_PORT="$(ask_free_port 'Grafana host port' "$GRAFANA_PORT")" + GRAFANA_ADMIN_USER="$(ask_user 'Grafana admin user' "$GRAFANA_ADMIN_USER")" + GRAFANA_ADMIN_PASS="$(ask_password 'Grafana admin password' "$GRAFANA_ADMIN_PASS")" + fi +} + +ensure_prometheus_config(){ + mkdir -p "$(dirname "$PROMETHEUS_CONFIG")" + + if [[ -f "$PROMETHEUS_CONFIG" ]]; then + return 0 + fi + + cat > "$PROMETHEUS_CONFIG" <<'EOF2' +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] + + - job_name: trustpoint + metrics_path: /metrics + scheme: http + static_configs: + - targets: ["trustpoint:80"] +EOF2 + + warn "Created default Prometheus config at ${PROMETHEUS_CONFIG}. Adjust the trustpoint metrics path/port there if needed." +} + +prepare_grafana_provisioning(){ + mkdir -p "$GRAFANA_DATASOURCES_DIR" + + cat > "${GRAFANA_DATASOURCES_DIR}/prometheus.yml" <<'EOF2' +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true +EOF2 +} + +start_prometheus(){ + $EN_PROMETHEUS || return 0 + + local name="prometheus" + stop_one "$name" + + if port_in_use "$PROMETHEUS_PORT"; then + die "Host port ${PROMETHEUS_PORT} is in use (Prometheus)." + fi + + ensure_prometheus_config + + log "Starting Prometheus..." + docker run -d --name "$name" --network "$NET" \ + -p "${PROMETHEUS_PORT}:9090" \ + -v "${PROMETHEUS_CONFIG}:/etc/prometheus/prometheus.yml:ro" \ + "$PROMETHEUS_IMAGE" \ + --config.file=/etc/prometheus/prometheus.yml >/dev/null +} + +start_grafana(){ + $EN_GRAFANA || return 0 + + local name="grafana" + stop_one "$name" + + if port_in_use "$GRAFANA_PORT"; then + die "Host port ${GRAFANA_PORT} is in use (Grafana)." + fi + + ensure_volume "$VOL_GRAFANA" + prepare_grafana_provisioning + + log "Starting Grafana..." + docker run -d --name "$name" --network "$NET" \ + -p "${GRAFANA_PORT}:3000" \ + -v "${VOL_GRAFANA}:/var/lib/grafana" \ + -v "${GRAFANA_PROVISIONING_ROOT}:/etc/grafana/provisioning:ro" \ + -e "GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER}" \ + -e "GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASS}" \ + "$GRAFANA_IMAGE" >/dev/null +} + +await_monitoring_ready(){ + if $EN_PROMETHEUS; then + echo "Waiting (<= ${READINESS_TIMEOUT}s) for Prometheus on localhost:${PROMETHEUS_PORT} ..." + local prometheus_deadline=$(( $(date +%s) + READINESS_TIMEOUT )) + while (( $(date +%s) < prometheus_deadline )); do + if tcp_check 127.0.0.1 "$PROMETHEUS_PORT" 1; then + ok "Prometheus reachable on :${PROMETHEUS_PORT}" + break + fi + printf "." + sleep 1 + done + echo + fi + + if $EN_GRAFANA; then + echo "Waiting (<= ${READINESS_TIMEOUT}s) for Grafana on localhost:${GRAFANA_PORT} ..." + local grafana_deadline=$(( $(date +%s) + READINESS_TIMEOUT )) + while (( $(date +%s) < grafana_deadline )); do + if tcp_check 127.0.0.1 "$GRAFANA_PORT" 1; then + ok "Grafana reachable on :${GRAFANA_PORT}" + break + fi + printf "." + sleep 1 + done + echo + fi +} diff --git a/scripts/tp_wizard/state.sh b/scripts/tp_wizard/state.sh index c67cf5277..15b48bbac 100644 --- a/scripts/tp_wizard/state.sh +++ b/scripts/tp_wizard/state.sh @@ -1,5 +1,11 @@ # -------------------------- Wizard state ------------------------------------- -EN_APP=false; EN_PG=false; EN_MAILPIT=false; EN_SFTPGO=false; EN_WF2_WORKER=false +EN_APP=false +EN_PG=false +EN_MAILPIT=false +EN_SFTPGO=false +EN_WF2_WORKER=false +EN_PROMETHEUS=false +EN_GRAFANA=false DB_INTERNAL=true DB_HOST="$DEF_DB_HOST_INTERNAL" # default host when internal @@ -22,6 +28,11 @@ SFTPGO_WEB_PORT="$DEF_SFTPGO_WEB_PORT" SFTPGO_ADMIN_USER="$DEF_SFTPGO_ADMIN_USER" SFTPGO_ADMIN_PASS="$DEF_SFTPGO_ADMIN_PASS" +PROMETHEUS_PORT="$DEF_PROMETHEUS_PORT" +GRAFANA_PORT="$DEF_GRAFANA_PORT" +GRAFANA_ADMIN_USER="$DEF_GRAFANA_ADMIN_USER" +GRAFANA_ADMIN_PASS="$DEF_GRAFANA_ADMIN_PASS" + TLS_FP_FOUND="" TLS_FP_ELAPSED=0 WF2_WORKER_LEASE="$DEF_WF2_WORKER_LEASE" @@ -29,5 +40,12 @@ WF2_WORKER_BATCH="$DEF_WF2_WORKER_BATCH" WF2_WORKER_SLEEP="$DEF_WF2_WORKER_SLEEP" # CLI target flags -ONLY_APP=false; ONLY_DB=false; ONLY_MAIL=false; ONLY_SFTP=false; ONLY_WF2_WORKER=false +ONLY_APP=false +ONLY_DB=false +ONLY_MAIL=false +ONLY_SFTP=false +ONLY_WF2_WORKER=false +ONLY_PROMETHEUS=false +ONLY_GRAFANA=false +DEMO_PRESET="" NOWAIT=false diff --git a/scripts/tp_wizard/summary.sh b/scripts/tp_wizard/summary.sh index 379c6de0d..b7324c6ef 100644 --- a/scripts/tp_wizard/summary.sh +++ b/scripts/tp_wizard/summary.sh @@ -3,15 +3,16 @@ show_plan(){ echo "==================== Configuration Summary (Planned) ====================" printf "%-22s %s\n" "Network:" "$NET" printf "%-22s %s\n" "DB Volume:" "$VOL_DB" + printf "%-22s %s\n" "Grafana Volume:" "$VOL_GRAFANA" echo printf "%-22s %s\n" "trustpoint enabled:" "$EN_APP" if $EN_APP; then if $BUILD_LOCAL; then - printf "%-22s %s\n" "App image:" "Build local → trustpoint:local" + printf "%-22s %s\n" "App image:" "Build local -> trustpoint:local" else - printf "%-22s %s\n" "App image:" "Pull → ${APP_IMAGE}" + printf "%-22s %s\n" "App image:" "Pull -> ${APP_IMAGE}" fi - printf "%-22s %s\n" "Host ports:" "80→80 (HTTP), 443→443 (HTTPS)" + printf "%-22s %s\n" "Host ports:" "80->80 (HTTP), 443->443 (HTTPS)" fi echo printf "%-22s %s\n" "Internal Postgres:" "$DB_INTERNAL" @@ -45,21 +46,33 @@ show_plan(){ printf "%-22s %s\n" "SFTP backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" } + echo + printf "%-22s %s\n" "Prometheus enabled:" "$EN_PROMETHEUS" + $EN_PROMETHEUS && { + printf "%-22s %s\n" "Prometheus UI:" "http://localhost:${PROMETHEUS_PORT}" + printf "%-22s %s\n" "Prometheus config:" "$PROMETHEUS_CONFIG" + } + printf "%-22s %s\n" "Grafana enabled:" "$EN_GRAFANA" + $EN_GRAFANA && { + printf "%-22s %s\n" "Grafana UI:" "http://localhost:${GRAFANA_PORT}" + printf "%-22s %s\n" "Grafana admin:" "${GRAFANA_ADMIN_USER} / $(mask "$GRAFANA_ADMIN_PASS")" + } echo "=========================================================================" } -# -------------------------- Build/Pull & Start ------------------------------- - show_runtime_status(){ - local net_state="absent" vol_state="absent" + local net_state="absent" vol_state="absent" grafana_vol_state="absent" docker network inspect "$NET" >/dev/null 2>&1 && net_state="present" docker volume inspect "$VOL_DB" >/dev/null 2>&1 && vol_state="present" + docker volume inspect "$VOL_GRAFANA" >/dev/null 2>&1 && grafana_vol_state="present" echo echo "=========================== Runtime Status (Live) ========================" printf "%-22s %s\n" "Network:" "${NET} (${net_state})" printf "%-22s %s\n" "DB volume:" "${VOL_DB} (${vol_state})" + printf "%-22s %s\n" "Grafana volume:" "${VOL_GRAFANA} (${grafana_vol_state})" printf "%-22s %s\n" "workflow2 folder:" "$([ -d "$WF2_FOLDER" ] && echo "${WF2_FOLDER} (present)" || echo "${WF2_FOLDER} (absent)")" + printf "%-22s %s\n" "Grafana provisioning:" "$([ -d "$GRAFANA_PROVISIONING_ROOT" ] && echo "${GRAFANA_PROVISIONING_ROOT} (present)" || echo "${GRAFANA_PROVISIONING_ROOT} (absent)")" echo printf "%-20s %-10s %-10s %s\n" "Container" "State" "Health" "Image" print_container_status_row trustpoint @@ -67,6 +80,8 @@ show_runtime_status(){ print_container_status_row mailpit print_container_status_row sftpgo print_container_status_row "$WF2_WORKER_NAME" + print_container_status_row prometheus + print_container_status_row grafana echo if exists trustpoint; then @@ -124,16 +139,29 @@ show_runtime_status(){ printf "%-22s %s\n" "SFTPGo data dir:" "${SFTPGO_ROOT}" fi + if exists prometheus; then + local prometheus_port + prometheus_port="$(container_host_port prometheus 9090/tcp)" + [[ -n "$prometheus_port" ]] && printf "%-22s %s\n" "Prometheus:" "http://localhost:${prometheus_port}" + printf "%-22s %s\n" "Prometheus config:" "$PROMETHEUS_CONFIG" + fi + + if exists grafana; then + local grafana_port grafana_user + grafana_port="$(container_host_port grafana 3000/tcp)" + grafana_user="$(container_env grafana GF_SECURITY_ADMIN_USER)" + [[ -n "$grafana_port" ]] && printf "%-22s %s\n" "Grafana:" "http://localhost:${grafana_port}" + [[ -n "$grafana_user" ]] && printf "%-22s %s\n" "Grafana admin:" "${grafana_user}" + fi + echo "=========================================================================" } -# -------------------------- Summary ------------------------------------------ - final_summary(){ echo echo "========================= Runtime Summary (Actual) =======================" printf "%-22s %s\n" "Network:" "$NET" - printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker)$' || true)" + printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker|prometheus|grafana)$' || true)" echo if $EN_APP; then printf "%-22s %s\n" "trustpoint:" "http://localhost:80 | https://localhost:443" @@ -160,6 +188,14 @@ final_summary(){ printf "%-22s %s\n" "Backup URL:" "sftp://${SFTPGO_BACKUP_USER}:***@127.0.0.1:${SFTPGO_SFTP_PORT}/" printf "%-22s %s\n" "Data dir:" "${SFTPGO_ROOT}" fi + if $EN_PROMETHEUS; then + printf "%-22s %s\n" "Prometheus:" "http://localhost:${PROMETHEUS_PORT}" + printf "%-22s %s\n" "Prometheus config:" "$PROMETHEUS_CONFIG" + fi + if $EN_GRAFANA; then + printf "%-22s %s\n" "Grafana:" "http://localhost:${GRAFANA_PORT}" + printf "%-22s %s\n" "Grafana admin:" "${GRAFANA_ADMIN_USER} / $(mask "$GRAFANA_ADMIN_PASS")" + fi if $EN_APP; then if [[ -n "$TLS_FP_FOUND" ]]; then printf "%-22s %s\n" "TLS fingerprint:" "$TLS_FP_FOUND" @@ -173,5 +209,3 @@ final_summary(){ fi echo "=========================================================================" } - -# -------------------------- High-level orchestration ------------------------- diff --git a/scripts/tp_wizard/wizard.sh b/scripts/tp_wizard/wizard.sh index 8667e02b4..4069aaa90 100644 --- a/scripts/tp_wizard/wizard.sh +++ b/scripts/tp_wizard/wizard.sh @@ -9,6 +9,7 @@ wizard(){ mailpit_prompt_config sftpgo_prompt_config step_workflows2_worker + monitoring_prompt_config show_plan ask_yes_no "Proceed with these settings?" "y" || { warn "Aborted by user."; exit 1; } runtime_start_enabled From 0940b9eecb2cc88a2ad543aff79f7d5392a7cfd1 Mon Sep 17 00:00:00 2001 From: BytesWelder Date: Mon, 22 Jun 2026 13:55:37 +0200 Subject: [PATCH 16/18] scripts/ --- scripts/tp_wizard/README.md | 16 ++++++++++++++++ scripts/tp_wizard/defaults.sh | 7 ++++++- scripts/tp_wizard/services/trustpoint.sh | 8 ++++++-- scripts/tp_wizard/summary.sh | 10 ++++++---- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/scripts/tp_wizard/README.md b/scripts/tp_wizard/README.md index 6e4f4433e..9b6d06345 100644 --- a/scripts/tp_wizard/README.md +++ b/scripts/tp_wizard/README.md @@ -65,3 +65,19 @@ wizard -> runtime -> services -> lib ``` Rules: `lib/` does not call services or commands; services do not parse CLI args; commands stay thin; `runtime.sh` owns shared orchestration. +## Environment files + +The wizard reads the repository `.env` as input, but does not modify it by default. +Generated runtime values are written to `.env.tp_wizard`. Containers started by +the wizard receive `.env` first and `.env.tp_wizard` second, followed by explicit +`docker run -e` values for the active wizard selection. + +To intentionally let the wizard update `.env` directly, run with: + +```bash +TP_WIZARD_WRITE_PROJECT_ENV=true ./tp_wizard.sh up demo light +``` + +When the wizard writes to an existing env file, it creates a timestamped backup +next to that file before changing it. + diff --git a/scripts/tp_wizard/defaults.sh b/scripts/tp_wizard/defaults.sh index 9cacd4142..9fff847c2 100644 --- a/scripts/tp_wizard/defaults.sh +++ b/scripts/tp_wizard/defaults.sh @@ -4,8 +4,13 @@ NET="${PROJECT}-net" VOL_DB="${PROJECT}_postgres_data" VOL_GRAFANA="${PROJECT}_grafana_data" ENV_FILE="${ENV_FILE:-${PWD}/.env}" +TP_WIZARD_ENV_FILE="${TP_WIZARD_ENV_FILE:-${PWD}/.env.tp_wizard}" +TP_WIZARD_WRITE_PROJECT_ENV="${TP_WIZARD_WRITE_PROJECT_ENV:-false}" -# Load .env early so defaults below can inherit repository-local configuration. +# Load the repository .env as read-only input so defaults below can inherit +# developer-local configuration. The wizard writes generated values to +# TP_WIZARD_ENV_FILE by default and does not modify .env unless +# TP_WIZARD_WRITE_PROJECT_ENV=true is set explicitly. if [[ -f "$ENV_FILE" ]]; then set -a # shellcheck source=/dev/null diff --git a/scripts/tp_wizard/services/trustpoint.sh b/scripts/tp_wizard/services/trustpoint.sh index ff80aa40c..668eac39a 100644 --- a/scripts/tp_wizard/services/trustpoint.sh +++ b/scripts/tp_wizard/services/trustpoint.sh @@ -101,8 +101,12 @@ start_app(){ if port_in_use "$APP_HTTPS_HOST"; then die "Host port ${APP_HTTPS_HOST} is in use (trustpoint HTTPS)."; fi log "Starting trustpoint..." - local env_file_arg=() - [[ -f "$ENV_FILE" ]] && env_file_arg=( --env-file "$ENV_FILE" ) + local env_file_arg=() wizard_env_target + wizard_env_target="$(tp_wizard_env_target)" + [[ -f "$ENV_FILE" ]] && env_file_arg+=( --env-file "$ENV_FILE" ) + if [[ "$wizard_env_target" != "$ENV_FILE" && -f "$wizard_env_target" ]]; then + env_file_arg+=( --env-file "$wizard_env_target" ) + fi local smtp_env=() if $EN_MAILPIT; then diff --git a/scripts/tp_wizard/summary.sh b/scripts/tp_wizard/summary.sh index 86d452159..384597194 100644 --- a/scripts/tp_wizard/summary.sh +++ b/scripts/tp_wizard/summary.sh @@ -2,9 +2,9 @@ show_plan(){ echo echo "==================== Configuration Summary (Planned) ====================" printf "%-22s %s\n" "Network:" "$NET" - printf "%-22s %s\n" ".env file:" "$ENV_FILE" + printf "%-22s %s\n" "Repo .env input:" "$ENV_FILE" + printf "%-22s %s\n" "Wizard env output:" "$(tp_wizard_env_target)" printf "%-22s %s\n" "DB Volume:" "$VOL_DB" - printf "%-22s %s\n" ".env file:" "$ENV_FILE" printf "%-22s %s\n" "Grafana Volume:" "$VOL_GRAFANA" echo printf "%-22s %s\n" "trustpoint enabled:" "$EN_APP" @@ -75,7 +75,8 @@ show_runtime_status(){ echo echo "=========================== Runtime Status (Live) ========================" printf "%-22s %s\n" "Network:" "${NET} (${net_state})" - printf "%-22s %s\n" ".env file:" "$ENV_FILE" + printf "%-22s %s\n" "Repo .env input:" "$ENV_FILE" + printf "%-22s %s\n" "Wizard env output:" "$(tp_wizard_env_target)" printf "%-22s %s\n" "DB volume:" "${VOL_DB} (${vol_state})" printf "%-22s %s\n" "Grafana volume:" "${VOL_GRAFANA} (${grafana_vol_state})" printf "%-22s %s\n" "workflow2 folder:" "$([ -d "$WF2_FOLDER" ] && echo "${WF2_FOLDER} (present)" || echo "${WF2_FOLDER} (absent)")" @@ -176,7 +177,8 @@ final_summary(){ echo echo "========================= Runtime Summary (Actual) =======================" printf "%-22s %s\n" "Network:" "$NET" - printf "%-22s %s\n" ".env file:" "$ENV_FILE" + printf "%-22s %s\n" "Repo .env input:" "$ENV_FILE" + printf "%-22s %s\n" "Wizard env output:" "$(tp_wizard_env_target)" printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker|prometheus|grafana)$' || true)" echo if $EN_APP; then From 5990afc11e62f56f225dfaa787eb4eeb39d9c862 Mon Sep 17 00:00:00 2001 From: BytesWelder Date: Mon, 22 Jun 2026 16:06:28 +0200 Subject: [PATCH 17/18] Skip setup-wizard --- scripts/tp_wizard/README.md | 124 ++++- scripts/tp_wizard/bootstrap.sh | 1 + scripts/tp_wizard/cli.sh | 79 ++- scripts/tp_wizard/commands/demo.sh | 58 ++ scripts/tp_wizard/commands/up.sh | 43 -- scripts/tp_wizard/defaults.sh | 27 +- scripts/tp_wizard/runtime.sh | 66 ++- scripts/tp_wizard/services/monitoring.sh | 494 +++++++++++++++++- scripts/tp_wizard/services/trustpoint.sh | 35 +- .../tp_wizard/services/workflows2_worker.sh | 15 +- scripts/tp_wizard/state.sh | 7 + scripts/tp_wizard/summary.sh | 382 +++++++------- 12 files changed, 1013 insertions(+), 318 deletions(-) create mode 100644 scripts/tp_wizard/commands/demo.sh diff --git a/scripts/tp_wizard/README.md b/scripts/tp_wizard/README.md index 9b6d06345..e307caa4e 100644 --- a/scripts/tp_wizard/README.md +++ b/scripts/tp_wizard/README.md @@ -10,8 +10,9 @@ Run from the repository root: ```bash ./tp_wizard.sh -./tp_wizard.sh up [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] [--nowait] -./tp_wizard.sh down [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] +./tp_wizard.sh demo [light|full] [--skip-setup|--no-skip-setup] [--nowait] +./tp_wizard.sh up [trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] [--skip-setup|--no-skip-setup] [--nowait] +./tp_wizard.sh down [trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] ./tp_wizard.sh logs [trustpoint|db|mail|sftp|worker|prometheus|grafana] ./tp_wizard.sh status ./tp_wizard.sh nuke @@ -20,27 +21,85 @@ Run from the repository root: Demo presets: ```text -demo light = trustpoint + PostgreSQL -demo = trustpoint + PostgreSQL + Mailpit + SFTPGo + workflows2 worker -demo full = demo + Prometheus + Grafana +./tp_wizard.sh demo light = trustpoint + PostgreSQL +./tp_wizard.sh demo = trustpoint + PostgreSQL + Mailpit + SFTPGo + workflows2 worker +./tp_wizard.sh demo full = demo + Prometheus + Grafana ``` -## Environment handling -The wizard reads and updates `.env` before starting trustpoint containers. The file is used for database settings, TLS host settings, host ports, and the trustpoint setup-skip flag. +`up` is intentionally only for explicit service targets. Demo presets are intentionally only available through `demo`. -Default setup-skip variable: +## Environment files + +The wizard reads `.env` as project input, but does not modify it by default. +Generated runtime values are written to `.env.tp_wizard`. + +Containers started by the wizard receive env files in this order: ```text -TP_SKIP_SETUP=true +.env -> .env.tp_wizard -> explicit docker run -e values ``` -If the application uses a different variable name, run the wizard with: +To intentionally let the wizard write into `.env` directly: ```bash -TRUSTPOINT_SKIP_SETUP_ENV_KEY=YOUR_ENV_NAME ./tp_wizard.sh up demo light +TP_WIZARD_WRITE_PROJECT_ENV=true ./tp_wizard.sh demo light --skip-setup ``` +The setup-skip variable defaults to `false`. Enable it per run with: + +```bash +./tp_wizard.sh demo full --skip-setup +./tp_wizard.sh up trustpoint db --skip-setup +``` + +If the application uses a different variable name: + +```bash +TRUSTPOINT_SKIP_SETUP_ENV_KEY=YOUR_ENV_NAME ./tp_wizard.sh demo light --skip-setup +``` + +## Output style + +`status`, the setup plan, and the final summary are intentionally compact: + +- one service table +- important access URLs only +- env-file overlay information +- database connection target without passwords +- only essential credentials, masked + + +## Monitoring auto-provisioning + +`demo full` starts Prometheus and Grafana with generated config under `.tp_wizard/`. +This avoids overwriting repository files such as `prometheus/prometheus.yml`. + +Generated files: + +```text +.tp_wizard/prometheus/prometheus.yml +.tp_wizard/grafana/provisioning/datasources/prometheus.yml +.tp_wizard/grafana/provisioning/dashboards/trustpoint.yml +.tp_wizard/grafana/dashboards/trustpoint-overview.json +``` + +Prometheus scrapes trustpoint with these defaults: + +```text +TRUSTPOINT_METRICS_SCHEME=http +TRUSTPOINT_METRICS_TARGET=trustpoint:80 +TRUSTPOINT_METRICS_PATH=/prometheus/metrics +``` + +Override them when needed: + +```bash +TRUSTPOINT_METRICS_PATH=/your/metrics/path ./tp_wizard.sh demo full +``` + +Grafana is provisioned with a Prometheus datasource and a `Trustpoint Overview` dashboard. + ## Design The root `tp_wizard.sh` is only the stable entrypoint. Implementation lives in `scripts/tp_wizard/`: @@ -51,7 +110,7 @@ state.sh mutable wizard/runtime state cli.sh argument parsing and dispatch wizard.sh interactive wizard flow runtime.sh shared start/wait/provision/summary orchestration -summary.sh plan, status, and final output +summary.sh compact plan, status, and final output lib/ generic helpers services/ service-specific logic commands/ command handlers @@ -65,19 +124,42 @@ wizard -> runtime -> services -> lib ``` Rules: `lib/` does not call services or commands; services do not parse CLI args; commands stay thin; `runtime.sh` owns shared orchestration. -## Environment files -The wizard reads the repository `.env` as input, but does not modify it by default. -Generated runtime values are written to `.env.tp_wizard`. Containers started by -the wizard receive `.env` first and `.env.tp_wizard` second, followed by explicit -`docker run -e` values for the active wizard selection. -To intentionally let the wizard update `.env` directly, run with: +## Demo/up split and setup skip + +`demo` is the only command for presets: + +```bash +./tp_wizard.sh demo light --skip-setup +./tp_wizard.sh demo --skip-setup +./tp_wizard.sh demo full --skip-setup +``` + +`up` is only for explicit services: + +```bash +./tp_wizard.sh up trustpoint db --skip-setup +./tp_wizard.sh up prometheus grafana +``` + +`up demo full` is intentionally invalid. + +The `--skip-setup` flag sets the configured trustpoint setup-skip environment variable to `true`. +By default the wizard writes: + +```text +TP_SKIP_SETUP=true +``` + +If the application-side variable name changes, override it without editing the script: ```bash -TP_WIZARD_WRITE_PROJECT_ENV=true ./tp_wizard.sh up demo light +TRUSTPOINT_SKIP_SETUP_ENV_KEY=REAL_ENV_NAME ./tp_wizard.sh demo full --skip-setup ``` -When the wizard writes to an existing env file, it creates a timestamped backup -next to that file before changing it. +To write more than one compatible variable name: +```bash +TRUSTPOINT_SKIP_SETUP_ENV_KEYS="TP_SKIP_SETUP REAL_ENV_NAME" ./tp_wizard.sh demo full --skip-setup +``` diff --git a/scripts/tp_wizard/bootstrap.sh b/scripts/tp_wizard/bootstrap.sh index dca5131de..a1ea10859 100644 --- a/scripts/tp_wizard/bootstrap.sh +++ b/scripts/tp_wizard/bootstrap.sh @@ -23,6 +23,7 @@ source "${TP_WIZARD_ROOT}/scripts/tp_wizard/services/monitoring.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/runtime.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/wizard.sh" +source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/demo.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/up.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/down.sh" source "${TP_WIZARD_ROOT}/scripts/tp_wizard/commands/logs.sh" diff --git a/scripts/tp_wizard/cli.sh b/scripts/tp_wizard/cli.sh index a831bb447..5dec66f5e 100644 --- a/scripts/tp_wizard/cli.sh +++ b/scripts/tp_wizard/cli.sh @@ -3,19 +3,24 @@ usage(){ Commands: (no command) Run interactive wizard - up [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] [--nowait] - down [demo [light|full]|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] + demo [light|full] [--skip-setup|--no-skip-setup] [--nowait] + up [trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] [--skip-setup|--no-skip-setup] [--nowait] + down [trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring] logs [trustpoint|db|mail|sftp|worker|prometheus|grafana] status nuke help Demo presets: - up demo light trustpoint + PostgreSQL - up demo trustpoint + PostgreSQL + Mailpit + SFTPGo + workflows2 worker - up demo full demo + Prometheus + Grafana + demo light trustpoint + PostgreSQL + demo trustpoint + PostgreSQL + Mailpit + SFTPGo + workflows2 worker + demo full demo + Prometheus + Grafana -Also supported (legacy): --only trustpoint|db|mail|sftp|worker|demo +Notes: + - Use `demo ...` for non-interactive demo presets. + - Use `up ...` for individual containers/services only. + - `up demo ...` is intentionally not supported. + - Use `--skip-setup` to make trustpoint skip its in-app setup wizard. EOF2 } @@ -53,9 +58,6 @@ map_demo_preset_to_flags(){ map_only_to_flags(){ case "$1" in - demo) map_demo_preset_to_flags default ;; - demo-light|light) map_demo_preset_to_flags light ;; - demo-full|full) map_demo_preset_to_flags full ;; trustpoint|app) ONLY_APP=true ;; db) ONLY_DB=true ;; mail) ONLY_MAIL=true ;; @@ -64,7 +66,27 @@ map_only_to_flags(){ prometheus|prom) ONLY_PROMETHEUS=true ;; grafana) ONLY_GRAFANA=true ;; monitoring|metrics) ONLY_PROMETHEUS=true; ONLY_GRAFANA=true ;; - *) die "Unknown target: $1 (use demo|trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring)" ;; + demo|light|full|demo-light|demo-full) + die "Demo presets are not valid for 'up'. Use './tp_wizard.sh demo [light|full]' instead." + ;; + *) die "Unknown target: $1 (use trustpoint|db|mail|sftp|worker|prometheus|grafana|monitoring)" ;; + esac +} + +set_common_runtime_flag(){ + case "$1" in + --skip-setup|--skip-trustpoint-setup) + TP_SKIP_SETUP_VALUE="true" + ;; + --no-skip-setup|--no-skip-trustpoint-setup) + TP_SKIP_SETUP_VALUE="false" + ;; + --nowait) + NOWAIT=true + ;; + *) + return 1 + ;; esac } @@ -72,34 +94,33 @@ set_targets_from_args(){ local any=false while [[ $# -gt 0 ]]; do case "$1" in - demo) - if [[ "${2:-}" == "light" || "${2:-}" == "full" ]]; then - map_demo_preset_to_flags "$2" - shift 2 - else - map_demo_preset_to_flags default - shift - fi - any=true - ;; - demo-light|demo-full|light|full|trustpoint|app|db|mail|sftp|worker|prometheus|prom|grafana|monitoring|metrics) + trustpoint|app|db|mail|sftp|worker|prometheus|prom|grafana|monitoring|metrics) map_only_to_flags "$1" any=true shift ;; --only) - map_only_to_flags "${2:-}" + [[ $# -ge 2 ]] || die "--only requires a target" + map_only_to_flags "$2" any=true shift 2 ;; - --nowait) - NOWAIT=true + --skip-setup|--skip-trustpoint-setup|--no-skip-setup|--no-skip-trustpoint-setup|--nowait) + set_common_runtime_flag "$1" shift ;; + demo|light|full|demo-light|demo-full) + map_only_to_flags "$1" + ;; *) die "Unknown option/target: $1" ;; esac done - if ! $any; then ONLY_APP=true; ONLY_DB=true; fi + + # Historical default for `up`: start trustpoint + DB when no target is given. + if ! $any; then + ONLY_APP=true + ONLY_DB=true + fi } tp_main(){ @@ -113,7 +134,15 @@ tp_main(){ help|-h|--help) usage ;; + demo) + preflight + shift || true + cmd_demo "$@" + ;; up) + if [[ "${2:-}" == "demo" || "${2:-}" == "light" || "${2:-}" == "full" || "${2:-}" == "demo-light" || "${2:-}" == "demo-full" ]]; then + die "Demo presets are not valid for 'up'. Use './tp_wizard.sh demo [light|full]' instead." + fi preflight shift || true cmd_up "$@" diff --git a/scripts/tp_wizard/commands/demo.sh b/scripts/tp_wizard/commands/demo.sh new file mode 100644 index 000000000..fb1a16d56 --- /dev/null +++ b/scripts/tp_wizard/commands/demo.sh @@ -0,0 +1,58 @@ +cmd_demo(){ + local preset="default" + local preset_seen=false + + while [[ $# -gt 0 ]]; do + case "$1" in + light|full) + if $preset_seen; then + die "Only one demo preset is allowed." + fi + preset="$1" + preset_seen=true + shift + ;; + --skip-setup|--skip-trustpoint-setup|--no-skip-setup|--no-skip-trustpoint-setup|--nowait) + set_common_runtime_flag "$1" + shift + ;; + demo|default) + if $preset_seen; then + die "Only one demo preset is allowed." + fi + preset="default" + preset_seen=true + shift + ;; + *) + die "Unknown demo option/preset: $1 (use light|full, --skip-setup, --no-skip-setup, --nowait)" + ;; + esac + done + + map_demo_preset_to_flags "$preset" + demo_apply_defaults + runtime_start_selected +} + +demo_apply_defaults(){ + # Demo mode must be non-interactive. Use deterministic defaults. + DB_INTERNAL=true + DB_HOST="$DEF_DB_HOST_INTERNAL" + + APP_DB_NAME="$DB_NAME" + APP_DB_USER="$DB_USER" + APP_DB_PASS="$DB_PASS" + APP_DB_HOST="$DEF_DB_HOST_INTERNAL" + APP_DB_PORT=5432 + + # Demo mode should run the current checkout by default. + # Set TP_WIZARD_DEMO_PULL=true to pull an image instead. + if bool_env_true "${TP_WIZARD_DEMO_PULL:-false}"; then + BUILD_LOCAL=false + APP_IMAGE="${TP_REPO}:${TP_WIZARD_DEMO_IMAGE_TAG:-latest}" + else + BUILD_LOCAL=true + APP_IMAGE="trustpoint:local" + fi +} diff --git a/scripts/tp_wizard/commands/up.sh b/scripts/tp_wizard/commands/up.sh index 9b62f0b81..f0a4b0e30 100644 --- a/scripts/tp_wizard/commands/up.sh +++ b/scripts/tp_wizard/commands/up.sh @@ -1,47 +1,4 @@ -configure_selected(){ - if $ONLY_DB; then - EN_PG=true - DB_INTERNAL=true - fi - $ONLY_MAIL && EN_MAILPIT=true - $ONLY_SFTP && EN_SFTPGO=true - $ONLY_PROMETHEUS && EN_PROMETHEUS=true - $ONLY_GRAFANA && EN_GRAFANA=true - - if $ONLY_APP; then - EN_APP=true - configure_app_image_prompt - if [[ -z "$DEMO_PRESET" ]]; then - EN_WF2_WORKER=$( - ask_yes_no "Delegate workflows2 tasks to a dedicated worker container?" "n" && echo true || echo false - ) - fi - elif $ONLY_WF2_WORKER; then - EN_WF2_WORKER=true - configure_app_image_prompt - fi - - if $ONLY_WF2_WORKER; then - EN_WF2_WORKER=true - fi - - if $ONLY_APP || $ONLY_WF2_WORKER; then - if $DB_INTERNAL; then - APP_DB_HOST="$DEF_DB_HOST_INTERNAL" - APP_DB_PORT=5432 - else - APP_DB_HOST="$DB_HOST" - APP_DB_PORT="$DB_PORT" - fi - APP_DB_NAME="$DB_NAME" - APP_DB_USER="$DB_USER" - APP_DB_PASS="$DB_PASS" - fi -} - cmd_up(){ set_targets_from_args "$@" - configure_selected - ensure_network runtime_start_selected } diff --git a/scripts/tp_wizard/defaults.sh b/scripts/tp_wizard/defaults.sh index 9fff847c2..7f271caca 100644 --- a/scripts/tp_wizard/defaults.sh +++ b/scripts/tp_wizard/defaults.sh @@ -49,15 +49,21 @@ DEF_TP_TLS_DNS_NAMES="${TP_TLS_DNS_NAMES:-trustpoint.local}" DEF_TP_TLS_IPV4_ADDRESSES="${TP_TLS_IPV4_ADDRESSES:-}" DEF_TP_TLS_IPV6_ADDRESSES="${TP_TLS_IPV6_ADDRESSES:-}" -# The setup-skip variable is configurable because the exact application-side -# name can change. The default used by the wizard is TP_SKIP_SETUP=true. -TRUSTPOINT_SKIP_SETUP_ENV_KEY="${TRUSTPOINT_SKIP_SETUP_ENV_KEY:-TP_SKIP_SETUP}" -TRUSTPOINT_SKIP_SETUP_ENV_VALUE="${TRUSTPOINT_SKIP_SETUP_ENV_VALUE:-true}" +# Trustpoint auto-setup bypasses the in-app setup wizard. The key is +# configurable for local app changes, but the current app uses TP_AUTO_SETUP. +# Use --skip-setup to set it to true for CLI/demo runs. +TRUSTPOINT_SKIP_SETUP_ENV_KEY="${TRUSTPOINT_SKIP_SETUP_ENV_KEY:-TP_AUTO_SETUP}" +TRUSTPOINT_SKIP_SETUP_ENV_KEYS="${TRUSTPOINT_SKIP_SETUP_ENV_KEYS:-$TRUSTPOINT_SKIP_SETUP_ENV_KEY}" +TRUSTPOINT_SKIP_SETUP_ENV_VALUE="${TRUSTPOINT_SKIP_SETUP_ENV_VALUE:-false}" if [[ -v "$TRUSTPOINT_SKIP_SETUP_ENV_KEY" ]]; then DEF_TRUSTPOINT_SKIP_SETUP_VALUE="${!TRUSTPOINT_SKIP_SETUP_ENV_KEY}" else DEF_TRUSTPOINT_SKIP_SETUP_VALUE="$TRUSTPOINT_SKIP_SETUP_ENV_VALUE" fi +DEF_TP_ADMIN_USERNAME="${TP_ADMIN_USERNAME:-admin}" +DEF_TP_ADMIN_PASSWORD="${TP_ADMIN_PASSWORD:-testing321}" +DEF_TP_ADMIN_EMAIL="${TP_ADMIN_EMAIL:-admin@trustpoint.local}" +DEF_TP_INJECT_DEMO_DATA="${TP_INJECT_DEMO_DATA:-true}" # Mailpit defaults DEF_MAILPIT_SMTP_PORT=1025 @@ -84,9 +90,18 @@ DEF_PROMETHEUS_PORT=9090 DEF_GRAFANA_PORT=3000 DEF_GRAFANA_ADMIN_USER="admin" DEF_GRAFANA_ADMIN_PASS="testing321" -PROMETHEUS_CONFIG="${PWD}/prometheus/prometheus.yml" -GRAFANA_PROVISIONING_ROOT="${PWD}/grafana-provisioning" + +# Wizard-generated observability config. Keep this separate from the repository's +# prometheus/prometheus.yml so existing project config is not overwritten. +TP_WIZARD_GENERATED_ROOT="${TP_WIZARD_GENERATED_ROOT:-${PWD}/.tp_wizard}" +PROMETHEUS_CONFIG="${PROMETHEUS_CONFIG:-${TP_WIZARD_GENERATED_ROOT}/prometheus/prometheus.yml}" +TRUSTPOINT_METRICS_SCHEME="${TRUSTPOINT_METRICS_SCHEME:-http}" +TRUSTPOINT_METRICS_TARGET="${TRUSTPOINT_METRICS_TARGET:-trustpoint:80}" +TRUSTPOINT_METRICS_PATH="${TRUSTPOINT_METRICS_PATH:-/prometheus/metrics}" +GRAFANA_PROVISIONING_ROOT="${TP_WIZARD_GENERATED_ROOT}/grafana/provisioning" GRAFANA_DATASOURCES_DIR="${GRAFANA_PROVISIONING_ROOT}/datasources" +GRAFANA_DASHBOARD_PROVIDERS_DIR="${GRAFANA_PROVISIONING_ROOT}/dashboards" +GRAFANA_DASHBOARDS_DIR="${TP_WIZARD_GENERATED_ROOT}/grafana/dashboards" # Timeouts READINESS_TIMEOUT=90 diff --git a/scripts/tp_wizard/runtime.sh b/scripts/tp_wizard/runtime.sh index 860d17008..0f2abdd50 100644 --- a/scripts/tp_wizard/runtime.sh +++ b/scripts/tp_wizard/runtime.sh @@ -1,23 +1,34 @@ -# Shared orchestration used by wizard mode and CLI up mode. +# Shared orchestration used by wizard mode, demo mode, and CLI up mode. await_readiness(){ local deadline=$(( $(date +%s) + READINESS_TIMEOUT )) + if $DB_INTERNAL; then echo "Waiting (<= ${READINESS_TIMEOUT}s) for PostgreSQL on localhost:${DB_PORT} ..." while (( $(date +%s) < deadline )); do - if tcp_check 127.0.0.1 "$DB_PORT" 1; then ok "PostgreSQL ready on :$DB_PORT"; break; fi - printf "."; sleep 1 + if tcp_check 127.0.0.1 "$DB_PORT" 1; then + ok "PostgreSQL ready on :$DB_PORT" + break + fi + printf "." + sleep 1 done echo fi + if $EN_APP; then echo "Waiting (<= ${READINESS_TIMEOUT}s) for trustpoint HTTP on localhost:${APP_HTTP_HOST} ..." while (( $(date +%s) < deadline )); do - if tcp_check 127.0.0.1 "$APP_HTTP_HOST" 1; then ok "trustpoint reachable on :$APP_HTTP_HOST"; break; fi - printf "."; sleep 1 + if tcp_check 127.0.0.1 "$APP_HTTP_HOST" 1; then + ok "trustpoint reachable on :$APP_HTTP_HOST" + break + fi + printf "." + sleep 1 done echo fi + await_sftpgo_ready await_monitoring_ready } @@ -31,31 +42,62 @@ runtime_after_start(){ } runtime_start_enabled(){ + ensure_network sync_env_file resolve_app_image + $DB_INTERNAL && ensure_volumes + start_postgres start_mailpit start_sftpgo + $EN_WF2_WORKER || stop_one "$WF2_WORKER_NAME" + start_app start_workflows2_worker start_prometheus start_grafana + runtime_after_start } runtime_start_selected(){ + $ONLY_APP && EN_APP=true + $ONLY_DB && { + EN_PG=true + DB_INTERNAL=true + } + $ONLY_MAIL && EN_MAILPIT=true + $ONLY_SFTP && EN_SFTPGO=true + $ONLY_WF2_WORKER && EN_WF2_WORKER=true + $ONLY_PROMETHEUS && EN_PROMETHEUS=true + $ONLY_GRAFANA && EN_GRAFANA=true + + ensure_network sync_env_file resolve_app_image - $ONLY_DB && { EN_PG=true; ensure_volumes; start_postgres; } - $ONLY_MAIL && { EN_MAILPIT=true; start_mailpit; } - $ONLY_SFTP && { EN_SFTPGO=true; start_sftpgo; } - $EN_WF2_WORKER || { $ONLY_APP && stop_one "$WF2_WORKER_NAME"; } - $ONLY_APP && start_app + + $ONLY_DB && { + ensure_volumes + start_postgres + } + + $ONLY_MAIL && start_mailpit + + $ONLY_SFTP && start_sftpgo + + $EN_WF2_WORKER || { + $ONLY_APP && stop_one "$WF2_WORKER_NAME" + } + + $ONLY_APP && start_app + $EN_WF2_WORKER && start_workflows2_worker - $ONLY_PROMETHEUS && { EN_PROMETHEUS=true; start_prometheus; } - $ONLY_GRAFANA && { EN_GRAFANA=true; start_grafana; } + + $ONLY_PROMETHEUS && start_prometheus + + $ONLY_GRAFANA && start_grafana runtime_after_start } diff --git a/scripts/tp_wizard/services/monitoring.sh b/scripts/tp_wizard/services/monitoring.sh index 2b2a5da94..8d2d3c599 100644 --- a/scripts/tp_wizard/services/monitoring.sh +++ b/scripts/tp_wizard/services/monitoring.sh @@ -2,7 +2,13 @@ monitoring_prompt_config(){ EN_PROMETHEUS=$(ask_yes_no "Enable Prometheus metrics stack?" "n" && echo true || echo false) if $EN_PROMETHEUS; then PROMETHEUS_PORT="$(ask_free_port 'Prometheus host port' "$PROMETHEUS_PORT")" - ask "Prometheus config file" "$PROMETHEUS_CONFIG" + ask "Trustpoint metrics scheme" "$TRUSTPOINT_METRICS_SCHEME_VALUE" + TRUSTPOINT_METRICS_SCHEME_VALUE="$REPLY" + ask "Trustpoint metrics target from Prometheus container" "$TRUSTPOINT_METRICS_TARGET_VALUE" + TRUSTPOINT_METRICS_TARGET_VALUE="$REPLY" + ask "Trustpoint metrics path" "$TRUSTPOINT_METRICS_PATH_VALUE" + TRUSTPOINT_METRICS_PATH_VALUE="$REPLY" + ask "Prometheus generated config file" "$PROMETHEUS_CONFIG" PROMETHEUS_CONFIG="$REPLY" fi @@ -17,11 +23,7 @@ monitoring_prompt_config(){ ensure_prometheus_config(){ mkdir -p "$(dirname "$PROMETHEUS_CONFIG")" - if [[ -f "$PROMETHEUS_CONFIG" ]]; then - return 0 - fi - - cat > "$PROMETHEUS_CONFIG" <<'EOF2' + cat > "$PROMETHEUS_CONFIG" < "${GRAFANA_DATASOURCES_DIR}/prometheus.yml" <<'EOF2' @@ -49,6 +51,7 @@ apiVersion: 1 datasources: - name: Prometheus + uid: prometheus type: prometheus access: proxy url: http://prometheus:9090 @@ -57,6 +60,474 @@ datasources: EOF2 } +prepare_grafana_dashboard_provider(){ + mkdir -p "$GRAFANA_DASHBOARD_PROVIDERS_DIR" "$GRAFANA_DASHBOARDS_DIR" + + cat > "${GRAFANA_DASHBOARD_PROVIDERS_DIR}/trustpoint.yml" <<'EOF2' +apiVersion: 1 + +providers: + - name: trustpoint + orgId: 1 + folder: Trustpoint + type: file + disableDeletion: false + allowUiUpdates: true + updateIntervalSeconds: 10 + options: + path: /var/lib/grafana/dashboards +EOF2 +} + +prepare_grafana_trustpoint_dashboard(){ + mkdir -p "$GRAFANA_DASHBOARDS_DIR" + + cat > "${GRAFANA_DASHBOARDS_DIR}/trustpoint-overview.json" <<'EOF2' +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "up{job=\"trustpoint\"}", + "legendFormat": "trustpoint", + "range": true, + "refId": "A" + } + ], + "title": "Trustpoint scrape status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 6, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "scrape_duration_seconds{job=\"trustpoint\"}", + "legendFormat": "scrape duration", + "range": true, + "refId": "A" + } + ], + "title": "Scrape duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 15, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "scrape_samples_scraped{job=\"trustpoint\"}", + "legendFormat": "samples scraped", + "range": true, + "refId": "A" + } + ], + "title": "Samples scraped", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "scrape_series_added{job=\"trustpoint\"}", + "legendFormat": "series added", + "range": true, + "refId": "A" + } + ], + "title": "New series per scrape", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "count by (__name__)({job=\"trustpoint\"})", + "format": "table", + "instant": true, + "legendFormat": "{{__name__}}", + "range": false, + "refId": "A" + } + ], + "title": "Exported metric names", + "type": "table" + } + ], + "refresh": "10s", + "schemaVersion": 39, + "tags": [ + "trustpoint", + "tp_wizard" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Trustpoint Overview", + "uid": "trustpoint-overview", + "version": 1, + "weekStart": "" +} +EOF2 +} + +prepare_grafana_provisioning(){ + prepare_grafana_datasource + prepare_grafana_dashboard_provider + prepare_grafana_trustpoint_dashboard + ok "Generated Grafana datasource and dashboard provisioning under ${TP_WIZARD_GENERATED_ROOT}/grafana" +} + start_prometheus(){ $EN_PROMETHEUS || return 0 @@ -95,6 +566,7 @@ start_grafana(){ -p "${GRAFANA_PORT}:3000" \ -v "${VOL_GRAFANA}:/var/lib/grafana" \ -v "${GRAFANA_PROVISIONING_ROOT}:/etc/grafana/provisioning:ro" \ + -v "${GRAFANA_DASHBOARDS_DIR}:/var/lib/grafana/dashboards:ro" \ -e "GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER}" \ -e "GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASS}" \ "$GRAFANA_IMAGE" >/dev/null diff --git a/scripts/tp_wizard/services/trustpoint.sh b/scripts/tp_wizard/services/trustpoint.sh index 668eac39a..612bf4e91 100644 --- a/scripts/tp_wizard/services/trustpoint.sh +++ b/scripts/tp_wizard/services/trustpoint.sh @@ -57,7 +57,7 @@ step_trustpoint_runtime_env(){ ask "Trustpoint TLS IPv6 addresses (comma-separated, optional)" "$TP_TLS_IPV6_ADDRESSES_VALUE" TP_TLS_IPV6_ADDRESSES_VALUE="$REPLY" - if ask_yes_no "Skip trustpoint in-app setup wizard using ${TRUSTPOINT_SKIP_SETUP_ENV_KEY}?" "y"; then + if ask_yes_no "Skip trustpoint in-app setup wizard using ${TRUSTPOINT_SKIP_SETUP_ENV_KEY}?" "n"; then TP_SKIP_SETUP_VALUE="true" else TP_SKIP_SETUP_VALUE="false" @@ -112,7 +112,38 @@ start_app(){ if $EN_MAILPIT; then smtp_env+=( -e "EMAIL_HOST=mailpit" -e "EMAIL_PORT=1025" -e "EMAIL_USE_TLS=0" -e "EMAIL_USE_SSL=0" -e "DEFAULT_FROM_EMAIL=no-reply@trustpoint.local" ) fi - docker run -d --name "$name" --network "$NET" -p "${APP_HTTP_HOST}:80" -p "${APP_HTTPS_HOST}:443" "${env_file_arg[@]}" -e "POSTGRES_DB=$APP_DB_NAME" -e "DATABASE_USER=$APP_DB_USER" -e "DATABASE_PASSWORD=$APP_DB_PASS" -e "DATABASE_HOST=$APP_DB_HOST" -e "DATABASE_PORT=$APP_DB_PORT" -e "TP_HTTP_PORT=$APP_HTTP_HOST" -e "TP_HTTPS_PORT=$APP_HTTPS_HOST" -e "TP_TLS_DNS_NAMES=$TP_TLS_DNS_NAMES_VALUE" -e "TP_TLS_IPV4_ADDRESSES=$TP_TLS_IPV4_ADDRESSES_VALUE" -e "TP_TLS_IPV6_ADDRESSES=$TP_TLS_IPV6_ADDRESSES_VALUE" -e "${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${TP_SKIP_SETUP_VALUE}" "${smtp_env[@]}" "$APP_IMAGE" >/dev/null + + local skip_env=() skip_key + for skip_key in $TRUSTPOINT_SKIP_SETUP_ENV_KEYS; do + [[ -n "$skip_key" ]] || continue + skip_env+=( -e "${skip_key}=${TP_SKIP_SETUP_VALUE}" ) + done + if [[ "$TP_SKIP_SETUP_VALUE" == "true" ]]; then + skip_env+=( + -e "TP_ADMIN_USERNAME=${TP_ADMIN_USERNAME_VALUE}" + -e "TP_ADMIN_PASSWORD=${TP_ADMIN_PASSWORD_VALUE}" + -e "TP_ADMIN_EMAIL=${TP_ADMIN_EMAIL_VALUE}" + -e "TP_INJECT_DEMO_DATA=${TP_INJECT_DEMO_DATA_VALUE}" + ) + fi + + docker run -d --name "$name" --network "$NET" \ + -p "${APP_HTTP_HOST}:80" \ + -p "${APP_HTTPS_HOST}:443" \ + "${env_file_arg[@]}" \ + -e "POSTGRES_DB=$APP_DB_NAME" \ + -e "DATABASE_USER=$APP_DB_USER" \ + -e "DATABASE_PASSWORD=$APP_DB_PASS" \ + -e "DATABASE_HOST=$APP_DB_HOST" \ + -e "DATABASE_PORT=$APP_DB_PORT" \ + -e "TP_HTTP_PORT=$APP_HTTP_HOST" \ + -e "TP_HTTPS_PORT=$APP_HTTPS_HOST" \ + -e "TP_TLS_DNS_NAMES=$TP_TLS_DNS_NAMES_VALUE" \ + -e "TP_TLS_IPV4_ADDRESSES=$TP_TLS_IPV4_ADDRESSES_VALUE" \ + -e "TP_TLS_IPV6_ADDRESSES=$TP_TLS_IPV6_ADDRESSES_VALUE" \ + "${skip_env[@]}" \ + "${smtp_env[@]}" \ + "$APP_IMAGE" >/dev/null } diff --git a/scripts/tp_wizard/services/workflows2_worker.sh b/scripts/tp_wizard/services/workflows2_worker.sh index 2423c6549..bb7be89c1 100644 --- a/scripts/tp_wizard/services/workflows2_worker.sh +++ b/scripts/tp_wizard/services/workflows2_worker.sh @@ -32,7 +32,6 @@ TP_HTTPS_PORT=${APP_HTTPS_HOST} TP_TLS_DNS_NAMES=${TP_TLS_DNS_NAMES_VALUE} TP_TLS_IPV4_ADDRESSES=${TP_TLS_IPV4_ADDRESSES_VALUE} TP_TLS_IPV6_ADDRESSES=${TP_TLS_IPV6_ADDRESSES_VALUE} -${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${TP_SKIP_SETUP_VALUE} TRUSTPOINT_SERVICE_ROLE=worker WORKFLOWS2_WORKER_ID=${WF2_WORKER_NAME} WORKFLOWS2_WORKER_LEASE=${WF2_WORKER_LEASE} @@ -41,6 +40,20 @@ WORKFLOWS2_WORKER_SLEEP=${WF2_WORKER_SLEEP} DEFAULT_FROM_EMAIL=no-reply@trustpoint.local EOF2 + local skip_key + for skip_key in $TRUSTPOINT_SKIP_SETUP_ENV_KEYS; do + [[ -n "$skip_key" ]] || continue + printf '%s=%s\n' "$skip_key" "$TP_SKIP_SETUP_VALUE" >> "$WF2_WORKER_ENV_FILE" + done + if [[ "$TP_SKIP_SETUP_VALUE" == "true" ]]; then + cat >> "$WF2_WORKER_ENV_FILE" <> "$WF2_WORKER_ENV_FILE" < trustpoint:local" + printf '%s\n' "$(bold)$1$(rst)" + _tp_line +} + +_tp_kv(){ + local key="$1" value="$2" + [[ -n "$value" ]] || return 0 + printf ' %-16s %s\n' "$key" "$value" +} + +_tp_join_words(){ + local first=true item + for item in "$@"; do + if $first; then + printf '%s' "$item" + first=false else - printf "%-22s %s\n" "App image:" "Pull -> ${APP_IMAGE}" + printf ', %s' "$item" fi - printf "%-22s %s\n" "Host ports:" "80->80 (HTTP), 443->443 (HTTPS)" + done +} + +_tp_env_summary(){ + local target + target="$(tp_wizard_env_target)" + if [[ "$target" == "$ENV_FILE" ]]; then + printf '%s' "${ENV_FILE} (wizard writes here)" + elif [[ -f "$ENV_FILE" ]]; then + printf '%s' "${ENV_FILE} + ${target}" + else + printf '%s' "${target}" fi - echo - printf "%-22s %s\n" "Internal Postgres:" "$DB_INTERNAL" - printf "%-22s %s\n" "DB host:" "$DB_HOST" - printf "%-22s %s\n" "DB host port:" "$DB_PORT" - printf "%-22s %s\n" "DB name:" "$DB_NAME" - printf "%-22s %s\n" "DB user:" "$DB_USER" - printf "%-22s %s\n" "DB pass:" "$(mask "$DB_PASS")" - echo +} + +_tp_enabled_services(){ + local services=() + $EN_APP && services+=(trustpoint) + $DB_INTERNAL && services+=(postgres) + $EN_MAILPIT && services+=(mailpit) + $EN_SFTPGO && services+=(sftpgo) + $EN_WF2_WORKER && services+=(worker) + $EN_PROMETHEUS && services+=(prometheus) + $EN_GRAFANA && services+=(grafana) + + if ((${#services[@]} == 0)); then + printf '%s' 'none' + else + _tp_join_words "${services[@]}" + fi +} + +_tp_app_urls_from_state(){ + local urls=() + $EN_APP || return 0 + urls+=("http://localhost:${APP_HTTP_HOST}") + urls+=("https://localhost:${APP_HTTPS_HOST}") + _tp_join_words "${urls[@]}" +} + +_tp_status_text(){ + local state="$1" health="${2:-}" + case "$state" in + absent) printf '%s' 'absent' ;; + running) + case "$health" in + healthy) printf '%s' 'up/healthy' ;; + starting) printf '%s' 'up/starting' ;; + unhealthy) printf '%s' 'up/unhealthy' ;; + -|'') printf '%s' 'up' ;; + *) printf '%s' "up/${health}" ;; + esac + ;; + exited|dead) printf '%s' 'stopped' ;; + *) printf '%s' "$state" ;; + esac +} + +_tp_container_url(){ + local name="$1" port_spec="$2" scheme="$3" path="${4:-}" + local port + port="$(container_host_port "$name" "$port_spec")" + [[ -n "$port" ]] || return 0 + printf '%s://localhost:%s%s' "$scheme" "$port" "$path" +} + +_tp_live_trustpoint_urls(){ + local http_port https_port urls=() + http_port="$(container_host_port trustpoint 80/tcp)" + https_port="$(container_host_port trustpoint 443/tcp)" + [[ -n "$http_port" ]] && urls+=("http://localhost:${http_port}") + [[ -n "$https_port" ]] && urls+=("https://localhost:${https_port}") + ((${#urls[@]} > 0)) && _tp_join_words "${urls[@]}" +} + +_tp_live_sftpgo_urls(){ + local web_port sftp_port urls=() + web_port="$(container_host_port sftpgo 8080/tcp)" + sftp_port="$(container_host_port sftpgo 2022/tcp)" + [[ -n "$web_port" ]] && urls+=("http://localhost:${web_port}/web/admin") + [[ -n "$sftp_port" ]] && urls+=("sftp://localhost:${sftp_port}") + ((${#urls[@]} > 0)) && _tp_join_words "${urls[@]}" +} + +_tp_live_postgres_url(){ + local pg_port + pg_port="$(container_host_port postgres 5432/tcp)" + [[ -n "$pg_port" ]] && printf 'localhost:%s' "$pg_port" +} + +_tp_runtime_row(){ + local name="$1" label="$2" url="$3" + local state health status + state="$(container_state "$name")" + health="$(container_health "$name")" + status="$(_tp_status_text "$state" "$health")" + printf ' %-18s %-13s %s\n' "$label" "$status" "${url:--}" +} + +_tp_runtime_rows(){ + printf ' %-18s %-13s %s\n' 'Service' 'Status' 'Access' + printf ' %-18s %-13s %s\n' '-------' '------' '------' + _tp_runtime_row trustpoint 'trustpoint' "$(_tp_live_trustpoint_urls)" + _tp_runtime_row postgres 'postgres' "$(_tp_live_postgres_url)" + _tp_runtime_row mailpit 'mailpit' "$(_tp_container_url mailpit 8025/tcp http)" + _tp_runtime_row sftpgo 'sftpgo' "$(_tp_live_sftpgo_urls)" + _tp_runtime_row "$WF2_WORKER_NAME" 'worker' '-' + _tp_runtime_row prometheus 'prometheus' "$(_tp_container_url prometheus 9090/tcp http)" + _tp_runtime_row grafana 'grafana' "$(_tp_container_url grafana 3000/tcp http)" +} + +show_plan(){ + _tp_title 'trustpoint setup plan' + _tp_kv 'services' "$(_tp_enabled_services)" + _tp_kv 'env files' "$(_tp_env_summary)" + if $EN_APP; then - printf "%-22s %s\n" "trustpoint DB host:" "$APP_DB_HOST" - printf "%-22s %s\n" "trustpoint DB port:" "$APP_DB_PORT" - printf "%-22s %s\n" "trustpoint DB name:" "$APP_DB_NAME" - printf "%-22s %s\n" "trustpoint DB user:" "$APP_DB_USER" - printf "%-22s %s\n" "trustpoint DB pass:" "$(mask "$APP_DB_PASS")" - printf "%-22s %s\n" "TLS DNS names:" "$TP_TLS_DNS_NAMES_VALUE" - printf "%-22s %s\n" "TLS IPv4 addresses:" "${TP_TLS_IPV4_ADDRESSES_VALUE:-(none)}" - printf "%-22s %s\n" "TLS IPv6 addresses:" "${TP_TLS_IPV6_ADDRESSES_VALUE:-(none)}" - printf "%-22s %s\n" "Setup skipped:" "${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${TP_SKIP_SETUP_VALUE}" + _tp_kv 'app' "$(_tp_app_urls_from_state)" + _tp_kv 'image' "$($BUILD_LOCAL && printf 'build local -> trustpoint:local' || printf 'pull -> %s' "$APP_IMAGE")" + _tp_kv 'database' "${APP_DB_USER}@${APP_DB_HOST}:${APP_DB_PORT}/${APP_DB_NAME}" + _tp_kv 'setup skip' "${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${TP_SKIP_SETUP_VALUE}" fi - echo - printf "%-22s %s\n" "Mailpit enabled:" "$EN_MAILPIT" - $EN_MAILPIT && printf "%-22s %s\n" "Mailpit ports:" "SMTP ${MAILPIT_SMTP_PORT}, UI ${MAILPIT_UI_PORT}" - echo - printf "%-22s %s\n" "workflows2 worker:" "$EN_WF2_WORKER" - $EN_WF2_WORKER && { - printf "%-22s %s\n" "Worker container:" "${WF2_WORKER_NAME}" - printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" - } - echo - printf "%-22s %s\n" "SFTPGo enabled:" "$EN_SFTPGO" - $EN_SFTPGO && { - printf "%-22s %s\n" "SFTPGo ports:" "SFTP ${SFTPGO_SFTP_PORT}, Web ${SFTPGO_WEB_PORT}" - printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" - printf "%-22s %s\n" "SFTP backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" - printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" - } - echo - printf "%-22s %s\n" "Prometheus enabled:" "$EN_PROMETHEUS" - $EN_PROMETHEUS && { - printf "%-22s %s\n" "Prometheus UI:" "http://localhost:${PROMETHEUS_PORT}" - printf "%-22s %s\n" "Prometheus config:" "$PROMETHEUS_CONFIG" - } - printf "%-22s %s\n" "Grafana enabled:" "$EN_GRAFANA" - $EN_GRAFANA && { - printf "%-22s %s\n" "Grafana UI:" "http://localhost:${GRAFANA_PORT}" - printf "%-22s %s\n" "Grafana admin:" "${GRAFANA_ADMIN_USER} / $(mask "$GRAFANA_ADMIN_PASS")" - } - echo "=========================================================================" + + $EN_MAILPIT && _tp_kv 'mailpit' "http://localhost:${MAILPIT_UI_PORT} (smtp ${MAILPIT_SMTP_PORT})" + $EN_SFTPGO && _tp_kv 'sftpgo' "http://localhost:${SFTPGO_WEB_PORT}/web/admin, sftp ${SFTPGO_SFTP_PORT}" + $EN_PROMETHEUS && _tp_kv 'prometheus' "http://localhost:${PROMETHEUS_PORT} -> ${TRUSTPOINT_METRICS_SCHEME_VALUE}://${TRUSTPOINT_METRICS_TARGET_VALUE}${TRUSTPOINT_METRICS_PATH_VALUE}" + $EN_GRAFANA && _tp_kv 'grafana' "http://localhost:${GRAFANA_PORT} (${GRAFANA_ADMIN_USER} / $(mask "$GRAFANA_ADMIN_PASS"))" + $EN_WF2_WORKER && _tp_kv 'worker' "$WF2_WORKER_NAME" + + _tp_line } show_runtime_status(){ - local net_state="absent" vol_state="absent" grafana_vol_state="absent" - docker network inspect "$NET" >/dev/null 2>&1 && net_state="present" - docker volume inspect "$VOL_DB" >/dev/null 2>&1 && vol_state="present" - docker volume inspect "$VOL_GRAFANA" >/dev/null 2>&1 && grafana_vol_state="present" + local net_state='absent' db_vol_state='absent' grafana_vol_state='absent' + docker network inspect "$NET" >/dev/null 2>&1 && net_state='present' + docker volume inspect "$VOL_DB" >/dev/null 2>&1 && db_vol_state='present' + docker volume inspect "$VOL_GRAFANA" >/dev/null 2>&1 && grafana_vol_state='present' + _tp_title 'trustpoint runtime status' + _tp_runtime_rows echo - echo "=========================== Runtime Status (Live) ========================" - printf "%-22s %s\n" "Network:" "${NET} (${net_state})" - printf "%-22s %s\n" "Repo .env input:" "$ENV_FILE" - printf "%-22s %s\n" "Wizard env output:" "$(tp_wizard_env_target)" - printf "%-22s %s\n" "DB volume:" "${VOL_DB} (${vol_state})" - printf "%-22s %s\n" "Grafana volume:" "${VOL_GRAFANA} (${grafana_vol_state})" - printf "%-22s %s\n" "workflow2 folder:" "$([ -d "$WF2_FOLDER" ] && echo "${WF2_FOLDER} (present)" || echo "${WF2_FOLDER} (absent)")" - printf "%-22s %s\n" "Grafana provisioning:" "$([ -d "$GRAFANA_PROVISIONING_ROOT" ] && echo "${GRAFANA_PROVISIONING_ROOT} (present)" || echo "${GRAFANA_PROVISIONING_ROOT} (absent)")" - echo - printf "%-20s %-10s %-10s %s\n" "Container" "State" "Health" "Image" - print_container_status_row trustpoint - print_container_status_row postgres - print_container_status_row mailpit - print_container_status_row sftpgo - print_container_status_row "$WF2_WORKER_NAME" - print_container_status_row prometheus - print_container_status_row grafana - echo + _tp_kv 'network' "${NET} (${net_state})" + _tp_kv 'env files' "$(_tp_env_summary)" + _tp_kv 'volumes' "db=${db_vol_state}, grafana=${grafana_vol_state}" if exists trustpoint; then - local http_port https_port db_host db_port db_name db_user db_pass tp_tls_dns_names tp_tls_ipv4_addresses tp_tls_ipv6_addresses tp_skip_setup - http_port="$(container_host_port trustpoint 80/tcp)" - https_port="$(container_host_port trustpoint 443/tcp)" + local db_host db_port db_name db_user setup_skip db_host="$(container_env trustpoint DATABASE_HOST)" db_port="$(container_env trustpoint DATABASE_PORT)" db_name="$(container_env trustpoint POSTGRES_DB)" db_user="$(container_env trustpoint DATABASE_USER)" - db_pass="$(container_env trustpoint DATABASE_PASSWORD)" - tp_tls_dns_names="$(container_env trustpoint TP_TLS_DNS_NAMES)" - tp_tls_ipv4_addresses="$(container_env trustpoint TP_TLS_IPV4_ADDRESSES)" - tp_tls_ipv6_addresses="$(container_env trustpoint TP_TLS_IPV6_ADDRESSES)" - tp_skip_setup="$(container_env trustpoint "$TRUSTPOINT_SKIP_SETUP_ENV_KEY")" - - [[ -n "$http_port" ]] && printf "%-22s %s\n" "trustpoint HTTP:" "http://localhost:${http_port}" - [[ -n "$https_port" ]] && printf "%-22s %s\n" "trustpoint HTTPS:" "https://localhost:${https_port}" - [[ -n "$tp_tls_dns_names" ]] && printf "%-22s %s\n" "TLS DNS names:" "${tp_tls_dns_names}" - [[ -n "$tp_tls_ipv4_addresses" ]] && printf "%-22s %s\n" "TLS IPv4 addresses:" "${tp_tls_ipv4_addresses}" - [[ -n "$tp_tls_ipv6_addresses" ]] && printf "%-22s %s\n" "TLS IPv6 addresses:" "${tp_tls_ipv6_addresses}" - [[ -n "$tp_skip_setup" ]] && printf "%-22s %s\n" "Setup skipped:" "${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${tp_skip_setup}" - printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings" - if [[ -n "$db_host" || -n "$db_port" || -n "$db_name" || -n "$db_user" ]]; then - printf "%-22s %s\n" "DB connect:" "host=${db_host:-?} port=${db_port:-?} db=${db_name:-?} user=${db_user:-?} pass=$(mask "${db_pass:-}")" - fi - fi - - if exists postgres; then - local pg_port - pg_port="$(container_host_port postgres 5432/tcp)" - [[ -n "$pg_port" ]] && printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${pg_port}" - fi - - if exists mailpit; then - local mailpit_ui mailpit_smtp - mailpit_ui="$(container_host_port mailpit 8025/tcp)" - mailpit_smtp="$(container_host_port mailpit 1025/tcp)" - [[ -n "$mailpit_ui" ]] && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${mailpit_ui}" - [[ -n "$mailpit_smtp" ]] && printf "%-22s %s\n" "Mailpit SMTP:" "localhost:${mailpit_smtp}" - fi - - if exists "$WF2_WORKER_NAME"; then - local worker_db worker_lease worker_batch worker_sleep - worker_db="$(container_env "$WF2_WORKER_NAME" DATABASE_HOST)" - worker_lease="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_LEASE)" - worker_batch="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_BATCH)" - worker_sleep="$(container_env "$WF2_WORKER_NAME" WORKFLOWS2_WORKER_SLEEP)" - printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" - [[ -n "$worker_db" ]] && printf "%-22s %s\n" "worker DB host:" "${worker_db}" - [[ -n "$worker_lease" || -n "$worker_batch" || -n "$worker_sleep" ]] && \ - printf "%-22s %s\n" "worker tuning:" "lease=${worker_lease:-?} batch=${worker_batch:-?} sleep=${worker_sleep:-?}" + setup_skip="$(container_env trustpoint "$TRUSTPOINT_SKIP_SETUP_ENV_KEY")" + [[ -n "$db_host$db_port$db_name$db_user" ]] && _tp_kv 'database' "${db_user:-?}@${db_host:-?}:${db_port:-?}/${db_name:-?}" + [[ -n "$setup_skip" ]] && _tp_kv 'setup skip' "${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${setup_skip}" fi - if exists sftpgo; then - local sftpgo_web sftpgo_sftp sftpgo_admin - sftpgo_web="$(container_host_port sftpgo 8080/tcp)" - sftpgo_sftp="$(container_host_port sftpgo 2022/tcp)" - sftpgo_admin="$(container_env sftpgo SFTPGO_DEFAULT_ADMIN_USERNAME)" - [[ -n "$sftpgo_web" ]] && printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${sftpgo_web}/web/admin" - [[ -n "$sftpgo_sftp" ]] && printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${sftpgo_sftp}" - [[ -n "$sftpgo_admin" ]] && printf "%-22s %s\n" "SFTPGo admin:" "${sftpgo_admin}" - printf "%-22s %s\n" "SFTPGo data dir:" "${SFTPGO_ROOT}" - fi - - if exists prometheus; then - local prometheus_port - prometheus_port="$(container_host_port prometheus 9090/tcp)" - [[ -n "$prometheus_port" ]] && printf "%-22s %s\n" "Prometheus:" "http://localhost:${prometheus_port}" - printf "%-22s %s\n" "Prometheus config:" "$PROMETHEUS_CONFIG" - fi - - if exists grafana; then - local grafana_port grafana_user - grafana_port="$(container_host_port grafana 3000/tcp)" - grafana_user="$(container_env grafana GF_SECURITY_ADMIN_USER)" - [[ -n "$grafana_port" ]] && printf "%-22s %s\n" "Grafana:" "http://localhost:${grafana_port}" - [[ -n "$grafana_user" ]] && printf "%-22s %s\n" "Grafana admin:" "${grafana_user}" - fi - - echo "=========================================================================" + _tp_line } final_summary(){ + _tp_title 'trustpoint stack ready' + _tp_runtime_rows echo - echo "========================= Runtime Summary (Actual) =======================" - printf "%-22s %s\n" "Network:" "$NET" - printf "%-22s %s\n" "Repo .env input:" "$ENV_FILE" - printf "%-22s %s\n" "Wizard env output:" "$(tp_wizard_env_target)" - printf "%-22s %s\n" "Containers:" "$(docker ps --format '{{.Names}}' | grep -E '^(trustpoint|postgres|mailpit|sftpgo|trustpoint-worker|prometheus|grafana)$' || true)" - echo - if $EN_APP; then - printf "%-22s %s\n" "trustpoint:" "http://localhost:${APP_HTTP_HOST} | https://localhost:${APP_HTTPS_HOST}" - printf "%-22s %s\n" "TLS DNS names:" "$TP_TLS_DNS_NAMES_VALUE" - printf "%-22s %s\n" "TLS IPv4 addresses:" "${TP_TLS_IPV4_ADDRESSES_VALUE:-(none)}" - printf "%-22s %s\n" "TLS IPv6 addresses:" "${TP_TLS_IPV6_ADDRESSES_VALUE:-(none)}" - printf "%-22s %s\n" "Setup skipped:" "${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${TP_SKIP_SETUP_VALUE}" - printf "%-22s %s\n" "workflows2 mode:" "managed in Trustpoint settings (default: auto)" - fi - if $DB_INTERNAL; then - printf "%-22s %s\n" "PostgreSQL:" "tcp://localhost:${DB_PORT} (container port 5432)" - fi + _tp_kv 'env files' "$(_tp_env_summary)" + if $EN_APP; then - printf "%-22s %s\n" "DB connect:" "host=${APP_DB_HOST} port=${APP_DB_PORT} db=${APP_DB_NAME} user=${APP_DB_USER} pass=$(mask "$APP_DB_PASS")" - fi - $EN_MAILPIT && printf "%-22s %s\n" "Mailpit UI:" "http://localhost:${MAILPIT_UI_PORT} (SMTP :${MAILPIT_SMTP_PORT})" - if $EN_WF2_WORKER; then - printf "%-22s %s\n" "workflows2 worker:" "${WF2_WORKER_NAME}" - printf "%-22s %s\n" "workflow2 folder:" "${WF2_FOLDER}" + _tp_kv 'database' "${APP_DB_USER}@${APP_DB_HOST}:${APP_DB_PORT}/${APP_DB_NAME}" + _tp_kv 'setup skip' "${TRUSTPOINT_SKIP_SETUP_ENV_KEY}=${TP_SKIP_SETUP_VALUE}" fi + if $EN_SFTPGO; then - local PORT; PORT="$(sftpgo_web_port)" - printf "%-22s %s\n" "SFTPGo Web:" "http://localhost:${PORT}/web/admin" - printf "%-22s %s\n" "SFTPGo SFTP:" "sftp://localhost:${SFTPGO_SFTP_PORT}" - printf "%-22s %s\n" "SFTPGo admin:" "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" - printf "%-22s %s\n" "Backup user:" "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" - printf "%-22s %s\n" "Backup home:" "${SFTPGO_BACKUP_HOME}" - printf "%-22s %s\n" "Backup URL:" "sftp://${SFTPGO_BACKUP_USER}:***@127.0.0.1:${SFTPGO_SFTP_PORT}/" - printf "%-22s %s\n" "Data dir:" "${SFTPGO_ROOT}" + _tp_kv 'sftp admin' "${SFTPGO_ADMIN_USER} / $(mask "$SFTPGO_ADMIN_PASS")" + _tp_kv 'backup user' "${SFTPGO_BACKUP_USER} / $(mask "$SFTPGO_BACKUP_PASS")" fi + if $EN_PROMETHEUS; then - printf "%-22s %s\n" "Prometheus:" "http://localhost:${PROMETHEUS_PORT}" - printf "%-22s %s\n" "Prometheus config:" "$PROMETHEUS_CONFIG" + _tp_kv 'metrics' "${TRUSTPOINT_METRICS_SCHEME_VALUE}://${TRUSTPOINT_METRICS_TARGET_VALUE}${TRUSTPOINT_METRICS_PATH_VALUE}" fi if $EN_GRAFANA; then - printf "%-22s %s\n" "Grafana:" "http://localhost:${GRAFANA_PORT}" - printf "%-22s %s\n" "Grafana admin:" "${GRAFANA_ADMIN_USER} / $(mask "$GRAFANA_ADMIN_PASS")" + _tp_kv 'grafana admin' "${GRAFANA_ADMIN_USER} / $(mask "$GRAFANA_ADMIN_PASS")" + _tp_kv 'dashboard' "Grafana -> Dashboards -> Trustpoint -> Trustpoint Overview" fi + if $EN_APP; then if [[ -n "$TLS_FP_FOUND" ]]; then - printf "%-22s %s\n" "TLS fingerprint:" "$TLS_FP_FOUND" - else - if $NOWAIT; then - printf "%-22s %s\n" "TLS fingerprint:" "skipped (NOWAIT)" - else - printf "%-22s %s\n" "TLS fingerprint:" "not found yet (polled ${TLS_FP_ELAPSED}s; timeout ${TLS_FP_TIMEOUT}s)" - fi + _tp_kv 'tls fingerprint' "$TLS_FP_FOUND" + elif ! $NOWAIT; then + _tp_kv 'tls fingerprint' "not found after ${TLS_FP_ELAPSED}s" fi fi - echo "=========================================================================" + + _tp_line } From e6d96c7e74b2962f27eab2c0fb0b0d91c28e33f7 Mon Sep 17 00:00:00 2001 From: BytesWelder Date: Tue, 23 Jun 2026 14:33:08 +0200 Subject: [PATCH 18/18] El finito --- docker/trustpoint/wizard/update_tls.sh | 8 +- docker/trustpoint/wizard/update_tls_nginx.sh | 10 +- scripts/tp_wizard/README.md | 4 +- scripts/tp_wizard/defaults.sh | 6 +- scripts/tp_wizard/services/monitoring.sh | 1613 ++++++++++++++++- scripts/tp_wizard/services/trustpoint.sh | 5 + .../tp_wizard/services/workflows2_worker.sh | 1 + scripts/tp_wizard/state.sh | 2 + .../commands/auto_setup_from_env.py | 18 +- 9 files changed, 1584 insertions(+), 83 deletions(-) diff --git a/docker/trustpoint/wizard/update_tls.sh b/docker/trustpoint/wizard/update_tls.sh index 241fdc31c..62ae72459 100644 --- a/docker/trustpoint/wizard/update_tls.sh +++ b/docker/trustpoint/wizard/update_tls.sh @@ -26,7 +26,13 @@ mkdir -p "$NGINX_TLS_DIR" log INFO "Move TLS Server credentials into $NGINX_TLS_DIR" # Copies the TLS-Server credentials into the nginx TLS directory. -if ! mv /var/www/html/trustpoint/docker/trustpoint/nginx/tls/* "$NGINX_TLS_DIR" +shopt -s nullglob +TLS_FILES=(/var/www/html/trustpoint/docker/trustpoint/nginx/tls/*) +shopt -u nullglob + +if [ ${#TLS_FILES[@]} -eq 0 ]; then + log INFO "No staged TLS files found; keeping existing nginx TLS files" +elif ! mv "${TLS_FILES[@]}" "$NGINX_TLS_DIR" then log ERROR "Failed to copy Trustpoint TLS files to $NGINX_TLS_DIR." exit 5 diff --git a/docker/trustpoint/wizard/update_tls_nginx.sh b/docker/trustpoint/wizard/update_tls_nginx.sh index ff08805a8..c420bd533 100644 --- a/docker/trustpoint/wizard/update_tls_nginx.sh +++ b/docker/trustpoint/wizard/update_tls_nginx.sh @@ -39,7 +39,13 @@ mkdir -p "$NGINX_TLS_DIR" log INFO "Move TLS Server credentials into $NGINX_TLS_DIR" # Copies the TLS-Server credentials into the nginx TLS directory. -if ! mv /var/www/html/trustpoint/docker/trustpoint/nginx/tls/* "$NGINX_TLS_DIR" +shopt -s nullglob +TLS_FILES=(/var/www/html/trustpoint/docker/trustpoint/nginx/tls/*) +shopt -u nullglob + +if [ ${#TLS_FILES[@]} -eq 0 ]; then + log INFO "No staged TLS files found; keeping existing nginx TLS files" +elif ! mv "${TLS_FILES[@]}" "$NGINX_TLS_DIR" then log ERROR "Failed to copy Trustpoint TLS files to $NGINX_TLS_DIR." exit 3 @@ -75,4 +81,4 @@ else fi log INFO "TLS certificate update for nginx completed successfully" -exit 0 \ No newline at end of file +exit 0 diff --git a/scripts/tp_wizard/README.md b/scripts/tp_wizard/README.md index e307caa4e..8462f3463 100644 --- a/scripts/tp_wizard/README.md +++ b/scripts/tp_wizard/README.md @@ -87,8 +87,8 @@ Generated files: Prometheus scrapes trustpoint with these defaults: ```text -TRUSTPOINT_METRICS_SCHEME=http -TRUSTPOINT_METRICS_TARGET=trustpoint:80 +TRUSTPOINT_METRICS_SCHEME=https +TRUSTPOINT_METRICS_TARGET=trustpoint.local:443 TRUSTPOINT_METRICS_PATH=/prometheus/metrics ``` diff --git a/scripts/tp_wizard/defaults.sh b/scripts/tp_wizard/defaults.sh index 7f271caca..28aebea17 100644 --- a/scripts/tp_wizard/defaults.sh +++ b/scripts/tp_wizard/defaults.sh @@ -64,6 +64,7 @@ DEF_TP_ADMIN_USERNAME="${TP_ADMIN_USERNAME:-admin}" DEF_TP_ADMIN_PASSWORD="${TP_ADMIN_PASSWORD:-testing321}" DEF_TP_ADMIN_EMAIL="${TP_ADMIN_EMAIL:-admin@trustpoint.local}" DEF_TP_INJECT_DEMO_DATA="${TP_INJECT_DEMO_DATA:-true}" +DEF_TP_ENABLE_PROMETHEUS_METRICS="${TP_ENABLE_PROMETHEUS_METRICS:-true}" # Mailpit defaults DEF_MAILPIT_SMTP_PORT=1025 @@ -95,9 +96,10 @@ DEF_GRAFANA_ADMIN_PASS="testing321" # prometheus/prometheus.yml so existing project config is not overwritten. TP_WIZARD_GENERATED_ROOT="${TP_WIZARD_GENERATED_ROOT:-${PWD}/.tp_wizard}" PROMETHEUS_CONFIG="${PROMETHEUS_CONFIG:-${TP_WIZARD_GENERATED_ROOT}/prometheus/prometheus.yml}" -TRUSTPOINT_METRICS_SCHEME="${TRUSTPOINT_METRICS_SCHEME:-http}" -TRUSTPOINT_METRICS_TARGET="${TRUSTPOINT_METRICS_TARGET:-trustpoint:80}" +TRUSTPOINT_METRICS_SCHEME="${TRUSTPOINT_METRICS_SCHEME:-https}" +TRUSTPOINT_METRICS_TARGET="${TRUSTPOINT_METRICS_TARGET:-trustpoint.local:443}" TRUSTPOINT_METRICS_PATH="${TRUSTPOINT_METRICS_PATH:-/prometheus/metrics}" +TRUSTPOINT_METRICS_TLS_INSECURE_SKIP_VERIFY="${TRUSTPOINT_METRICS_TLS_INSECURE_SKIP_VERIFY:-true}" GRAFANA_PROVISIONING_ROOT="${TP_WIZARD_GENERATED_ROOT}/grafana/provisioning" GRAFANA_DATASOURCES_DIR="${GRAFANA_PROVISIONING_ROOT}/datasources" GRAFANA_DASHBOARD_PROVIDERS_DIR="${GRAFANA_PROVISIONING_ROOT}/dashboards" diff --git a/scripts/tp_wizard/services/monitoring.sh b/scripts/tp_wizard/services/monitoring.sh index 8d2d3c599..16c86c85c 100644 --- a/scripts/tp_wizard/services/monitoring.sh +++ b/scripts/tp_wizard/services/monitoring.sh @@ -36,6 +36,16 @@ scrape_configs: - job_name: trustpoint metrics_path: ${TRUSTPOINT_METRICS_PATH_VALUE} scheme: ${TRUSTPOINT_METRICS_SCHEME_VALUE} +EOF2 + + if [[ "$TRUSTPOINT_METRICS_SCHEME_VALUE" == "https" && "$TRUSTPOINT_METRICS_TLS_INSECURE_SKIP_VERIFY_VALUE" == "true" ]]; then + cat >> "$PROMETHEUS_CONFIG" <<'EOF2' + tls_config: + insecure_skip_verify: true +EOF2 + fi + + cat >> "$PROMETHEUS_CONFIG" </dev/null } diff --git a/scripts/tp_wizard/services/trustpoint.sh b/scripts/tp_wizard/services/trustpoint.sh index 612bf4e91..6a142f3b6 100644 --- a/scripts/tp_wizard/services/trustpoint.sh +++ b/scripts/tp_wizard/services/trustpoint.sh @@ -118,16 +118,21 @@ start_app(){ [[ -n "$skip_key" ]] || continue skip_env+=( -e "${skip_key}=${TP_SKIP_SETUP_VALUE}" ) done + if [[ " $TRUSTPOINT_SKIP_SETUP_ENV_KEYS " != *" TP_SKIP_SETUP "* ]]; then + skip_env+=( -e "TP_SKIP_SETUP=" ) + fi if [[ "$TP_SKIP_SETUP_VALUE" == "true" ]]; then skip_env+=( -e "TP_ADMIN_USERNAME=${TP_ADMIN_USERNAME_VALUE}" -e "TP_ADMIN_PASSWORD=${TP_ADMIN_PASSWORD_VALUE}" -e "TP_ADMIN_EMAIL=${TP_ADMIN_EMAIL_VALUE}" -e "TP_INJECT_DEMO_DATA=${TP_INJECT_DEMO_DATA_VALUE}" + -e "TP_ENABLE_PROMETHEUS_METRICS=${TP_ENABLE_PROMETHEUS_METRICS_VALUE}" ) fi docker run -d --name "$name" --network "$NET" \ + --network-alias trustpoint.local \ -p "${APP_HTTP_HOST}:80" \ -p "${APP_HTTPS_HOST}:443" \ "${env_file_arg[@]}" \ diff --git a/scripts/tp_wizard/services/workflows2_worker.sh b/scripts/tp_wizard/services/workflows2_worker.sh index bb7be89c1..29d906298 100644 --- a/scripts/tp_wizard/services/workflows2_worker.sh +++ b/scripts/tp_wizard/services/workflows2_worker.sh @@ -51,6 +51,7 @@ TP_ADMIN_USERNAME=${TP_ADMIN_USERNAME_VALUE} TP_ADMIN_PASSWORD=${TP_ADMIN_PASSWORD_VALUE} TP_ADMIN_EMAIL=${TP_ADMIN_EMAIL_VALUE} TP_INJECT_DEMO_DATA=${TP_INJECT_DEMO_DATA_VALUE} +TP_ENABLE_PROMETHEUS_METRICS=${TP_ENABLE_PROMETHEUS_METRICS_VALUE} EOF2 fi diff --git a/scripts/tp_wizard/state.sh b/scripts/tp_wizard/state.sh index e1e8edc46..2292e0bda 100644 --- a/scripts/tp_wizard/state.sh +++ b/scripts/tp_wizard/state.sh @@ -28,6 +28,7 @@ TP_ADMIN_USERNAME_VALUE="$DEF_TP_ADMIN_USERNAME" TP_ADMIN_PASSWORD_VALUE="$DEF_TP_ADMIN_PASSWORD" TP_ADMIN_EMAIL_VALUE="$DEF_TP_ADMIN_EMAIL" TP_INJECT_DEMO_DATA_VALUE="$DEF_TP_INJECT_DEMO_DATA" +TP_ENABLE_PROMETHEUS_METRICS_VALUE="$DEF_TP_ENABLE_PROMETHEUS_METRICS" MAILPIT_SMTP_PORT="$DEF_MAILPIT_SMTP_PORT" MAILPIT_UI_PORT="$DEF_MAILPIT_UI_PORT" @@ -44,6 +45,7 @@ GRAFANA_ADMIN_PASS="$DEF_GRAFANA_ADMIN_PASS" TRUSTPOINT_METRICS_SCHEME_VALUE="$TRUSTPOINT_METRICS_SCHEME" TRUSTPOINT_METRICS_TARGET_VALUE="$TRUSTPOINT_METRICS_TARGET" TRUSTPOINT_METRICS_PATH_VALUE="$TRUSTPOINT_METRICS_PATH" +TRUSTPOINT_METRICS_TLS_INSECURE_SKIP_VERIFY_VALUE="$TRUSTPOINT_METRICS_TLS_INSECURE_SKIP_VERIFY" TLS_FP_FOUND="" TLS_FP_ELAPSED=0 diff --git a/trustpoint/management/management/commands/auto_setup_from_env.py b/trustpoint/management/management/commands/auto_setup_from_env.py index 44d46bce5..1e923fb81 100644 --- a/trustpoint/management/management/commands/auto_setup_from_env.py +++ b/trustpoint/management/management/commands/auto_setup_from_env.py @@ -12,7 +12,7 @@ from django.db import DatabaseError, transaction from django.db.models import ProtectedError -from management.models import KeyStorageConfig +from management.models import KeyStorageConfig, PrometheusConfig from management.nginx_paths import NGINX_CERT_CHAIN_PATH, NGINX_CERT_PATH, NGINX_KEY_PATH from pki.models import CredentialModel from pki.models.truststore import ActiveTrustpointTlsServerCredentialModel @@ -75,6 +75,20 @@ def _configure_storage(self) -> None: err_msg = f'Failed to configure storage: {e}' raise CommandError(err_msg) from e + def _configure_prometheus_metrics(self) -> None: + """Enable the Prometheus metrics endpoint when requested.""" + if not self._env_bool('TP_ENABLE_PROMETHEUS_METRICS', default=False): + return + + config = PrometheusConfig.get() + if config.enabled: + self.stdout.write(self.style.WARNING('Prometheus metrics endpoint already enabled')) + return + + config.enabled = True + config.save(update_fields=['enabled']) + self.stdout.write(self.style.SUCCESS('Prometheus metrics endpoint enabled')) + def _parse_csv_list(self, value: str | None) -> list[str]: """Parse comma-separated values into a list.""" if not value: @@ -151,6 +165,7 @@ def handle(self, *args: Any, **options: Any) -> None: self.stdout.write(self.style.WARNING('=== Trustpoint Auto-Setup from Environment Variables ===')) if SetupWizardCompletedModel.setup_wizard_completed(): + self._configure_prometheus_metrics() self.stdout.write(self.style.WARNING('Setup wizard already completed, skipping auto-setup')) return @@ -198,6 +213,7 @@ def handle(self, *args: Any, **options: Any) -> None: credential_model = self._generate_tls_credential(tls_ipv4, tls_ipv6, tls_dns) self._apply_tls_credential(credential_model) + self._configure_prometheus_metrics() SetupWizardCompletedModel.mark_setup_complete_once() self.stdout.write(self.style.SUCCESS('Setup marked as complete'))