Step 6 of 8

Part 5: Infrastructure

Create a compose.yml in the root of your swim-ffice-consumer project. This file defines the complete local stack:

Service Image Host Ports Purpose
ffice-consumer-validator Local build 8086, 8447 Mock SM API + event generator
ffice-consumer-validator-artemis Local build (Artemis) 5673, 5674, 8166 AMQP broker
ffice-consumer-validator-mariadb mariadb:12-ubi 3310 Validator persistence
mongodb mongodb-community-server:8.2 27019 Consumer event store
mongo-express mongo-express:1.0 9084 MongoDB web UI
kafka apache/kafka:4.1.2 9093 Event streaming
akhq tchiotludo/akhq 9085 Kafka web UI
kafka-init apache/kafka:4.1.2 -- Creates Kafka topics on startup

compose.yml

services:

  ffice-consumer-validator:
    build:
      context: ../swim-ffice-consumer-validator
      dockerfile: src/main/docker/Containerfile.jvm
    container_name: ffice-consumer-validator
    ports:
      - "8086:8080"
      - "8447:8443"
    environment:
      AMQP_BROKER_HOST: ffice-consumer-validator-artemis
      AMQP_BROKER_PORT: 5672
      AMQP_BROKER_USERNAME: admin
      AMQP_BROKER_PASSWORD: admin
      MARIADB_HOST: ffice-consumer-validator-mariadb
      MARIADB_PORT: 3306
      MARIADB_DATABASE: swim_ffice_consumer_validator
      MARIADB_USERNAME: swim
      MARIADB_PASSWORD: swim
      EVENT_GENERATOR_ENABLED: "true"
      EVENT_GENERATOR_SCHEDULE: "0 */1 * * * ?"
      HEARTBEAT_PUBLISHER_ENABLED: "true"
      HEARTBEAT_PUBLISHER_INTERVAL: 15s
      QUARKUS_HTTP_INSECURE_REQUESTS: enabled
    volumes:
      - ./certs/validator.crt:/certs/server/tls.crt:ro,Z
      - ./certs/validator.key:/certs/server/tls.key:ro,Z
      - ./certs/ca.crt:/certs/ca/ca.crt:ro,Z
    networks:
      - swim-network
    depends_on:
      ffice-consumer-validator-artemis:
        condition: service_healthy
      ffice-consumer-validator-mariadb:
        condition: service_healthy

  ffice-consumer-validator-artemis:
    build:
      context: ./src/local-dev/artemis
      dockerfile: Containerfile
    container_name: ffice-consumer-validator-artemis
    ports:
      - "5673:5672"
      - "5674:5671"
      - "8166:8161"
    environment:
      ARTEMIS_USER: admin
      ARTEMIS_PASSWORD: admin
    volumes:
      - ./certs/broker.p12:/certs/broker.p12:ro,Z
      - ./certs/ca-truststore.p12:/certs/ca-truststore.p12:ro,Z
    networks:
      - swim-network
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:8161/console/ || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 60s

  ffice-consumer-validator-mariadb:
    image: docker.io/library/mariadb:12-ubi
    container_name: ffice-consumer-validator-mariadb
    ports:
      - "3310:3306"
    environment:
      MARIADB_DATABASE: swim_ffice_consumer_validator
      MARIADB_USER: swim
      MARIADB_PASSWORD: swim
      MARIADB_ROOT_PASSWORD: root
    volumes:
      - ffice-consumer-validator-mariadb-data:/var/lib/mysql
    networks:
      - swim-network
    healthcheck:
      test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 30s

  mongodb:
    image: quay.io/mongodb/mongodb-community-server:8.2-ubi9-slim
    container_name: ffice-consumer-mongodb
    command: ["mongod", "--quiet", "--logpath", "/dev/null"]
    environment:
      MONGO_INITDB_DATABASE: swim_ffice
      GLIBC_TUNABLES: glibc.cpu.hwcaps=-SHSTK
    ports:
      - "27019:27017"
    volumes:
      - ffice-consumer-mongodb-data:/data/db
    networks:
      - swim-network
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 3

  mongo-express:
    image: docker.io/library/mongo-express:1.0
    container_name: ffice-consumer-mongo-express
    environment:
      ME_CONFIG_MONGODB_URL: mongodb://mongodb:27017/
      ME_CONFIG_BASICAUTH: false
    ports:
      - "9084:8081"
    networks:
      - swim-network
    depends_on:
      mongodb:
        condition: service_healthy

  kafka:
    image: docker.io/apache/kafka:4.1.2
    hostname: ffice-kafka
    container_name: ffice-kafka
    ports:
      - "9093:9092"
    networks:
      - swim-network
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://ffice-kafka:29092,PLAINTEXT_HOST://localhost:9093
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_NODE_ID: 1
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@ffice-kafka:29093
      KAFKA_LISTENERS: PLAINTEXT://ffice-kafka:29092,CONTROLLER://ffice-kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
      CLUSTER_ID: FFICE_CONSUMER_LOCAL
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: false

  akhq:
    image: docker.io/tchiotludo/akhq:latest
    container_name: ffice-consumer-akhq
    environment:
      AKHQ_CONFIGURATION: |
        akhq:
          connections:
            local:
              properties:
                bootstrap.servers: "ffice-kafka:29092"
    ports:
      - "9085:8080"
    networks:
      - swim-network
    depends_on:
      - kafka

  kafka-init:
    image: docker.io/apache/kafka:4.1.2
    container_name: ffice-kafka-init
    depends_on:
      - kafka
    networks:
      - swim-network
    restart: "no"
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        echo 'Waiting for Kafka...'
        until /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --list > /dev/null 2>&1; do sleep 2; done
        echo 'Creating FF-ICE topics...'
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-inbox-topic          --partitions 10 --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-flight-plan-topic    --partitions 3  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-flight-update-topic  --partitions 3  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-operations-topic     --partitions 3  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-trial-topic          --partitions 3  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-submission-topic     --partitions 3  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-data-topic           --partitions 3  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-dlq-topic            --partitions 1  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-events-topic         --partitions 3  --replication-factor 1
        /opt/kafka/bin/kafka-topics.sh --bootstrap-server ffice-kafka:29092 --create --if-not-exists --topic ffice-events-dlq-topic     --partitions 1  --replication-factor 1
        echo 'Done.'

volumes:
  ffice-consumer-mongodb-data:
  ffice-consumer-validator-mariadb-data:

networks:
  swim-network:
    name: swim-ffice-consumer-network
    driver: bridge

5.1 Artemis broker files

The Artemis service requires custom files in src/local-dev/artemis/. Create this directory structure:

src/local-dev/artemis/
  Containerfile
  broker.xml
  tls-entrypoint.sh

Containerfile

FROM apache/activemq-artemis:2.41.0

USER root
COPY tls-entrypoint.sh /opt/tls-entrypoint.sh
RUN chmod +x /opt/tls-entrypoint.sh
USER artemis

COPY broker.xml /var/lib/artemis-instance/etc/broker.xml

ENTRYPOINT ["/opt/tls-entrypoint.sh"]

broker.xml

Create a standard Artemis broker.xml with AMQP and AMQPS acceptors. The key configuration is:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<configuration xmlns="urn:activemq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="urn:activemq /schema/artemis-configuration.xsd">
  <core xmlns="urn:activemq:core">
    <name>ffice-consumer-validator-artemis</name>
    <persistence-enabled>true</persistence-enabled>
    <journal-type>NIO</journal-type>

    <acceptors>
      <acceptor name="amqp">tcp://0.0.0.0:5672?protocols=AMQP;tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpCredits=1000;amqpLowCredits=300;amqpMinLargeMessageSize=102400;amqpDuplicateDetection=true</acceptor>
      <acceptor name="amqps">tcp://0.0.0.0:5671?protocols=AMQP;sslEnabled=true;keyStorePath=/certs/broker.p12;keyStorePassword=changeit;trustStorePath=/certs/ca-truststore.p12;trustStorePassword=changeit;needClientAuth=false</acceptor>
    </acceptors>

    <security-settings>
      <security-setting match="#">
        <permission type="createNonDurableQueue" roles="amq"/>
        <permission type="deleteNonDurableQueue" roles="amq"/>
        <permission type="createDurableQueue" roles="amq"/>
        <permission type="deleteDurableQueue" roles="amq"/>
        <permission type="createAddress" roles="amq"/>
        <permission type="deleteAddress" roles="amq"/>
        <permission type="consume" roles="amq"/>
        <permission type="browse" roles="amq"/>
        <permission type="send" roles="amq"/>
        <permission type="manage" roles="amq"/>
      </security-setting>
    </security-settings>

    <address-settings>
      <address-setting match="#">
        <dead-letter-address>DLQ</dead-letter-address>
        <expiry-address>ExpiryQueue</expiry-address>
        <redelivery-delay>1000</redelivery-delay>
        <max-size-bytes>-1</max-size-bytes>
        <message-counter-history-day-limit>10</message-counter-history-day-limit>
        <address-full-policy>PAGE</address-full-policy>
        <auto-create-queues>true</auto-create-queues>
        <auto-create-addresses>true</auto-create-addresses>
      </address-setting>
    </address-settings>
  </core>
</configuration>

tls-entrypoint.sh

#!/bin/bash
set -e

if [ -f /certs/broker.p12 ]; then
  echo "TLS certificates found, starting with AMQPS support"
else
  echo "No TLS certificates found, starting with AMQP only"
fi

exec /opt/activemq-artemis/bin/artemis run

5.2 Generate TLS certificates

Create a certs/generate.sh script that generates a local CA and service certificates.

#!/bin/bash
set -euo pipefail

CERTS_DIR="$(cd "$(dirname "$0")" && pwd)"
CA_PASS="changeit"

echo "=== Generating local CA ==="
mkcert -install
cp "$(mkcert -CAROOT)/rootCA.pem" "$CERTS_DIR/ca.crt"
cp "$(mkcert -CAROOT)/rootCA-key.pem" "$CERTS_DIR/ca.key"

echo "=== Generating broker certificate ==="
mkcert -cert-file "$CERTS_DIR/broker.crt" -key-file "$CERTS_DIR/broker.key" \
  ffice-consumer-validator-artemis localhost 127.0.0.1

echo "=== Generating validator certificate ==="
mkcert -cert-file "$CERTS_DIR/validator.crt" -key-file "$CERTS_DIR/validator.key" \
  ffice-consumer-validator localhost 127.0.0.1

echo "=== Creating PKCS12 keystores ==="
openssl pkcs12 -export -in "$CERTS_DIR/broker.crt" -inkey "$CERTS_DIR/broker.key" \
  -out "$CERTS_DIR/broker.p12" -name broker -password "pass:$CA_PASS"

keytool -importcert -noprompt -alias ca -file "$CERTS_DIR/ca.crt" \
  -keystore "$CERTS_DIR/ca-truststore.p12" -storetype PKCS12 -storepass "$CA_PASS"

echo "=== Creating JKS keystores for consumer ==="
keytool -importcert -noprompt -alias ca -file "$CERTS_DIR/ca.crt" \
  -keystore "$CERTS_DIR/truststore.jks" -storepass "$CA_PASS"

mkcert -cert-file "$CERTS_DIR/consumer.crt" -key-file "$CERTS_DIR/consumer.key" \
  ffice-consumer localhost 127.0.0.1

openssl pkcs12 -export -in "$CERTS_DIR/consumer.crt" -inkey "$CERTS_DIR/consumer.key" \
  -out "$CERTS_DIR/consumer.p12" -name consumer -password "pass:$CA_PASS"

keytool -importkeystore -noprompt \
  -srckeystore "$CERTS_DIR/consumer.p12" -srcstoretype PKCS12 -srcstorepass "$CA_PASS" \
  -destkeystore "$CERTS_DIR/keystore.jks" -deststorepass "$CA_PASS"

echo "=== Done. Generated files: ==="
ls -la "$CERTS_DIR"

Run the script:

chmod +x certs/generate.sh              # Linux / macOS only
./certs/generate.sh

On Windows, run the equivalent commands manually or use Git Bash / WSL to execute the shell script.

After running, the certs/ directory should contain:

  • ca.crt, ca.key - CA certificate and key
  • broker.p12 - broker keystore (PKCS12)
  • ca-truststore.p12 - CA truststore (PKCS12)
  • validator.crt, validator.key - validator server certificate
  • truststore.jks, keystore.jks - Java keystores for the consumer

Part 6: Run

6.1 Start infrastructure

From the swim-ffice-consumer directory:

podman compose up --build -d

Wait for all services to be healthy. Verify with podman compose ps.

6.2 Create the development profile

Create src/main/resources/application-dev.properties. This profile is used when running with quarkus:dev and connects to the local infrastructure from compose.yml:

# =============================================================================
# SWIM FF-ICE Consumer - Development Profile
# =============================================================================
# Uses swim-ffice-consumer-validator (port 8086) as Subscription Manager
# and ffice-consumer-validator-artemis (port 5672) as AMQP broker
# DevServices are DISABLED to prevent Testcontainers from starting
# =============================================================================

# =============================================================================
# MongoDB (ffice-consumer-mongodb on port 27019)
# =============================================================================
quarkus.mongodb.connection-string=${MONGODB_URI:mongodb://localhost:27019}

# =============================================================================
# FF-ICE Subscriptions (Hard-coded for local testing)
# =============================================================================
ffice.subscriptions=[{"provider":"validator","topic":"ffice.v1","messageTypes":["FILED_FLIGHT_PLAN","FLIGHT_PLAN_UPDATE","FLIGHT_DEPARTURE","FLIGHT_ARRIVAL","FLIGHT_CANCELLATION","PLANNING_STATUS","FILING_STATUS"],"aerodromes":["LPPT","LEMD","LFPG"],"description":"FF-ICE messages for Lisbon, Madrid, Paris"}]

# =============================================================================
# SWIM Providers (Dev - ffice-consumer-validator-artemis AMQPS + mTLS)
# SM: http://localhost:8086 (swim-ffice-consumer-validator)
# AMQP: localhost:5673 -> container 5672 (plain AMQP for dev simplicity)
# =============================================================================
swim.providers=[{"providerId":"validator","subscriptionManager":{"url":"http://localhost:8086","tls":null,"resilience":{"connectTimeoutMs":5000,"readTimeoutMs":5000,"retryMaxAttempts":3,"retryDelayMs":1000}},"amqpBroker":{"host":"localhost","port":5673,"sslEnabled":false,"username":"admin","password":"admin","tls":null}}]

# =============================================================================
# Subscription Renewal (Faster intervals for dev)
# =============================================================================
swim.subscription.renewal.check-interval=2m
swim.subscription.renewal.threshold=30m

# =============================================================================
# DevServices - DISABLED (use compose.yml instead)
# =============================================================================
quarkus.devservices.enabled=false

# =============================================================================
# Kafka
# =============================================================================
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9093}

# =============================================================================
# Per-Subscription Heartbeat Monitor (faster check interval for dev)
# =============================================================================
swim.heartbeat.monitor.check-interval=10s

# =============================================================================
# OpenTelemetry - DISABLED in dev (no collector running)
# =============================================================================
quarkus.otel.enabled=false
quarkus.otel.sdk.disabled=true

# =============================================================================
# TLS
# =============================================================================
swim.tls.reload-period=off

# =============================================================================
# Logging
# =============================================================================
quarkus.log.level=INFO
quarkus.log.file.enabled=true
quarkus.log.file.path=target/app.log