diff --git a/.env.dist b/.env.dist index 5d3f23f..1ba7ff4 100644 --- a/.env.dist +++ b/.env.dist @@ -11,8 +11,6 @@ MYSQL_PORT=3306 PHPMYADMIN_PORT=8031 STATSD_PORT=8125 -JAEGER_HTTP_PORT=14268 -JAEGER_UDP_PORT=6832 OTLP_GRPC_PORT=4317 OTLP_HTTP_PORT=4318 JAEGER_UI_PORT=8034 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..6ba5d26 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,23 @@ +name: e2e + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + + - name: Free up disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + docker system prune -af + + - name: Run e2e tests + run: make test diff --git a/Makefile b/Makefile index 70a112c..65b5147 100644 --- a/Makefile +++ b/Makefile @@ -9,3 +9,7 @@ install: @echo "Creating network..." @stack network @echo "🧱 Installed. Happy hacking!" + +.PHONY: test +test: + @bash $(STACK_PATH)/tests/run-all.sh diff --git a/README.md b/README.md index eaefbaa..6eb1bfa 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/opencodeco/stack/main/in | `stack mysql`| MySQL & phpMyAdmin (http://localhost:8031) | | `stack redis` | Redis & RedisInsight (http://localhost:8032) | | `stack mongo` | MongoDB & Mongo Express (http://localhost:8033) | -| `stack postgres` | PostgreSQL & pgAdmin (http://localhost:8034) | +| `stack postgres` | PostgreSQL & pgAdmin (http://localhost:8039) | | `stack kafka` | Kafka and UI for Apache Kafka (http://localhost:8037) | | `stack rabbitmq` | RabbitMQ & Management Plugin (http://localhost:8038) | | `stack aws` | AWS services via LocalStack _(legacy, [see details below](#aws-localstack-vs-ministack))_ (http://localhost:4566) | @@ -29,8 +29,6 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/opencodeco/stack/main/in | Component | Description | Port | | --- | --- | --- | -| OpenTelemetry Collector | Jaeger HTTP | `14268` | -| OpenTelemetry Collector | Jaeger UDP | `6832` | | OpenTelemetry Collector | Statsd UDP | `8125` | | OpenTelemetry Collector | OTLP gRPC | `4317` | | OpenTelemetry Collector | OTLP HTTP | `4318` | @@ -77,7 +75,7 @@ stack mysql up -d ``` Which does a: ```shell -docker-compose -d mysql/docker-compose.yml up -d +docker-compose -f mysql/docker-compose.yml up -d ``` Behind the scenes. @@ -109,3 +107,5 @@ stack mysql logs -f --- ⚠️ **Remember:** this is suited for development environments only. + +> **Note on upgrades:** Major version upgrades for MySQL (8→9), MongoDB (6→8), and RabbitMQ (3→4) may require recreating volumes if you have existing data. Run `stack down -v` to remove volumes before upgrading. diff --git a/aws/docker-compose.yml b/aws/docker-compose.yml index 33ab82e..a3ba79c 100644 --- a/aws/docker-compose.yml +++ b/aws/docker-compose.yml @@ -1,7 +1,7 @@ services: opencodeco-aws: container_name: opencodeco-aws - image: localstack/localstack:2.2.0 + image: localstack/localstack:4.14.0 ports: - ${AWS_PORT}:4566 diff --git a/hyperdx/docker-compose.yml b/hyperdx/docker-compose.yml index 4412277..2bda9ad 100644 --- a/hyperdx/docker-compose.yml +++ b/hyperdx/docker-compose.yml @@ -1,7 +1,7 @@ services: opencodeco-hyperx: container_name: opencodeco-hyperx - image: hyperdx/hyperdx-local + image: hyperdx/hyperdx-local:2.23.2 ports: - ${HYPERDX_API_PORT}:8000 - ${HYPERDX_APP_PORT}:8080 diff --git a/kafka/docker-compose.yml b/kafka/docker-compose.yml index dab1b87..10adb4d 100644 --- a/kafka/docker-compose.yml +++ b/kafka/docker-compose.yml @@ -1,26 +1,36 @@ services: opencodeco-kafka: container_name: opencodeco-kafka - image: bitnami/kafka:3.5 + image: apache/kafka:3.9.0 ports: - ${KAFKA_PORT}:9092 volumes: - - opencodeco-kafka:/bitnami + - opencodeco-kafka:/var/lib/kafka/data environment: - - KAFKA_CFG_NODE_ID=0 - - KAFKA_CFG_PROCESS_ROLES=controller,broker - - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 - - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT - - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@opencodeco-kafka:9093 - - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + - KAFKA_NODE_ID=1 + - KAFKA_PROCESS_ROLES=broker,controller + - KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 + - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://opencodeco-kafka:9092 + - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT + - KAFKA_CONTROLLER_QUORUM_VOTERS=1@opencodeco-kafka:9093 + - KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER + - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 + - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 + - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 + healthcheck: + test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 > /dev/null 2>&1"] + interval: 15s + timeout: 10s + retries: 5 opencodeco-kafka-ui: container_name: opencodeco-kafka-ui - image: provectuslabs/kafka-ui:latest + image: ghcr.io/kafbat/kafka-ui:v1.4.2 ports: - ${KAFKA_UI_PORT}:8080 depends_on: - - opencodeco-kafka + opencodeco-kafka: + condition: service_healthy environment: DYNAMIC_CONFIG_ENABLED: 'true' volumes: diff --git a/mongo/docker-compose.yml b/mongo/docker-compose.yml index d01a7b7..8797741 100644 --- a/mongo/docker-compose.yml +++ b/mongo/docker-compose.yml @@ -1,21 +1,27 @@ services: opencodeco-mongo: container_name: opencodeco-mongo - image: mongo:6.0 + image: mongo:8.0 ports: - ${MONGO_PORT}:27017 volumes: - opencodeco-mongo:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.runCommand('ping').ok", "--quiet"] + interval: 10s + timeout: 5s + retries: 5 opencodeco-mongo-express: container_name: opencodeco-mongo-express - image: mongo-express:1.0.0-alpha + image: mongo-express:1.0.2 ports: - ${MONGO_EXPRESS_PORT}:8081 environment: ME_CONFIG_MONGODB_URL: mongodb://opencodeco-mongo:27017/ depends_on: - - opencodeco-mongo + opencodeco-mongo: + condition: service_healthy networks: default: diff --git a/mysql/docker-compose.yml b/mysql/docker-compose.yml index 1074b05..970053d 100644 --- a/mysql/docker-compose.yml +++ b/mysql/docker-compose.yml @@ -1,14 +1,18 @@ services: opencodeco-mysql: container_name: opencodeco-mysql - image: mysql:8.1 - command: --default-authentication-plugin=mysql_native_password + image: mysql:9.3 ports: - ${MYSQL_PORT}:3306 environment: MYSQL_ROOT_PASSWORD: opencodeco volumes: - opencodeco-mysql:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-popencodeco"] + interval: 10s + timeout: 5s + retries: 5 opencodeco-phpmyadmin: container_name: opencodeco-phpmyadmin @@ -17,6 +21,9 @@ services: - ${PHPMYADMIN_PORT}:80 environment: - PMA_HOST=opencodeco-mysql + depends_on: + opencodeco-mysql: + condition: service_healthy networks: default: diff --git a/o11y/docker-compose.yml b/o11y/docker-compose.yml index d84e651..4bbbbde 100644 --- a/o11y/docker-compose.yml +++ b/o11y/docker-compose.yml @@ -1,25 +1,23 @@ services: opencodeco-otelcol: container_name: opencodeco-otelcol - image: otel/opentelemetry-collector-contrib:0.95.0 + image: otel/opentelemetry-collector-contrib:0.123.0 volumes: - ./otelcol-config.yml:/etc/otelcol-contrib/config.yaml ports: - - ${JAEGER_HTTP_PORT}:14268 - - ${JAEGER_UDP_PORT}:6832/udp - ${STATSD_PORT}:8125/udp - ${OTLP_GRPC_PORT}:4317 - ${OTLP_HTTP_PORT}:4318 opencodeco-jaeger: container_name: opencodeco-jaeger - image: jaegertracing/all-in-one:1.54 + image: jaegertracing/all-in-one:1.76.0 ports: - ${JAEGER_UI_PORT}:16686 opencodeco-prometheus: container_name: opencodeco-prometheus - image: prom/prometheus:v2.46.0 + image: prom/prometheus:v3.3.0 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: @@ -27,7 +25,7 @@ services: opencodeco-grafana: container_name: opencodeco-grafana - image: grafana/grafana:10.0.3 + image: grafana/grafana:11.6.0 ports: - ${GRAFANA_PORT}:3000 volumes: diff --git a/o11y/otelcol-config.yml b/o11y/otelcol-config.yml index c016797..0b65559 100644 --- a/o11y/otelcol-config.yml +++ b/o11y/otelcol-config.yml @@ -1,8 +1,4 @@ receivers: - jaeger: - protocols: - thrift_http: - thrift_binary: statsd: endpoint: 0.0.0.0:8125 is_monotonic_counter: true @@ -27,7 +23,7 @@ service: level: "debug" pipelines: traces: - receivers: [otlp, jaeger] + receivers: [otlp] exporters: [debug, otlp/jaeger] metrics: receivers: [otlp, statsd] diff --git a/postgres/docker-compose.yml b/postgres/docker-compose.yml index bd87ac8..d2b9e9a 100644 --- a/postgres/docker-compose.yml +++ b/postgres/docker-compose.yml @@ -1,8 +1,7 @@ -version: '3' services: opencodeco-postgres: container_name: opencodeco-postgres - image: postgres:latest + image: postgres:17.4 restart: always ports: - ${POSTGRES_PORT}:5432 @@ -10,17 +9,23 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - opencodeco-postgres:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 opencodeco-pgadmin: container_name: opencodeco-pgadmin - image: dpage/pgadmin4:latest + image: dpage/pgadmin4:9.2 ports: - ${PGADMIN_PORT}:80 environment: - PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL} - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD} depends_on: - - opencodeco-postgres + opencodeco-postgres: + condition: service_healthy networks: default: diff --git a/rabbitmq/docker-compose.yml b/rabbitmq/docker-compose.yml index 0f0e28c..10ad0c3 100644 --- a/rabbitmq/docker-compose.yml +++ b/rabbitmq/docker-compose.yml @@ -1,7 +1,7 @@ services: opencodeco-rabbitmq: container_name: opencodeco-rabbitmq - image: rabbitmq:3-management-alpine + image: rabbitmq:4.1-management-alpine ports: - ${RABBITMQ_PORT}:5672 - ${RABBITMQ_UI_PORT}:15672 @@ -10,6 +10,11 @@ services: RABBITMQ_DEFAULT_PASS: opencodeco volumes: - opencodeco-rabbitmq:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 5s + retries: 5 networks: default: diff --git a/redis/docker-compose.yml b/redis/docker-compose.yml index 9757832..aeff67b 100644 --- a/redis/docker-compose.yml +++ b/redis/docker-compose.yml @@ -1,15 +1,20 @@ services: opencodeco-redis: container_name: opencodeco-redis - image: redis:7.0 + image: redis:7.4 ports: - ${REDIS_PORT}:6379 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 opencodeco-redisinsight: container_name: opencodeco-redisinsight - image: redislabs/redisinsight:1.14.0 + image: redis/redisinsight:3.2.0 ports: - - ${REDISINSIGHT_PORT}:8001 + - ${REDISINSIGHT_PORT}:5540 networks: default: diff --git a/stack b/stack index e6fa52c..8c525b5 100755 --- a/stack +++ b/stack @@ -56,14 +56,14 @@ else fi CONTAINER_COMPOSE= -if command -v docker-compose > /dev/null 2>&1; then +if docker compose --help > /dev/null 2>&1; then + CONTAINER_COMPOSE="docker compose" +elif command -v docker-compose > /dev/null 2>&1; then CONTAINER_COMPOSE=docker-compose elif command -v podman-compose > /dev/null 2>&1; then CONTAINER_COMPOSE=podman-compose -elif docker compose --help > /dev/null 2>&1; then - CONTAINER_COMPOSE="docker compose" else - echo "Could not find \"docker-compose\", \"podman-compose\" or \"docker compose\", aborting" + echo "Could not find \"docker compose\", \"docker-compose\" or \"podman-compose\", aborting" exit 1 fi diff --git a/tests/lib.sh b/tests/lib.sh new file mode 100755 index 0000000..db7a43f --- /dev/null +++ b/tests/lib.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Shared utilities for e2e tests + +PASS=0 +FAIL=0 +COMPONENT="" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +stack() { + (cd "$REPO_ROOT" && "$REPO_ROOT/stack" "$@") +} +export -f stack + +ok() { + echo " ✓ $1" + PASS=$((PASS + 1)) +} + +fail() { + echo " ✗ $1" + FAIL=$((FAIL + 1)) +} + +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + ok "$desc" + else + fail "$desc (expected: '$expected', got: '$actual')" + fi +} + +assert_contains() { + local desc="$1" needle="$2" haystack="$3" + if echo "$haystack" | grep -q "$needle"; then + ok "$desc" + else + fail "$desc (expected to contain '$needle', got: '$haystack')" + fi +} + +# Check if a host port is already bound by something other than Docker +port_in_use() { + local port="$1" + lsof -i ":$port" -sTCP:LISTEN 2>/dev/null | grep -qv "com.docker\|Docker" +} + +# Start a component; on failure print the error and return 1 +start_component() { + local component="$1" + local output + output=$(stack "$component" 2>&1) + local exit_code=$? + if [ $exit_code -ne 0 ]; then + echo " ✗ failed to start '$component':" + echo "$output" | sed 's/^/ /' + return 1 + fi +} + +# Wait for a container with a Docker healthcheck to become healthy +wait_healthy() { + local container="$1" + local timeout="${2:-120}" + echo " Waiting for $container to be healthy..." + local elapsed=0 + while [ "$elapsed" -lt "$timeout" ]; do + local status + status=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null) + if [ "$status" = "healthy" ]; then + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + done + echo " Timeout: $container did not become healthy within ${timeout}s" + return 1 +} + +# Wait for an HTTP endpoint to return a 2xx status +wait_http() { + local url="$1" + local timeout="${2:-180}" + echo " Waiting for $url..." + local elapsed=0 + while [ "$elapsed" -lt "$timeout" ]; do + local code + code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 2 "$url" 2>/dev/null) + if [[ "$code" =~ ^2 ]]; then + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + done + echo " Timeout: $url did not respond within ${timeout}s" + return 1 +} + +# Wait for a TCP port to be open +wait_port() { + local host="$1" port="$2" + local timeout="${3:-180}" + echo " Waiting for $host:$port..." + local elapsed=0 + while [ "$elapsed" -lt "$timeout" ]; do + if bash -c "echo > /dev/tcp/$host/$port" 2>/dev/null; then + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + done + echo " Timeout: $host:$port did not open within ${timeout}s" + return 1 +} + +report() { + echo "" + if [ $FAIL -eq 0 ]; then + echo " ✅ $COMPONENT: $PASS passed, 0 failed" + else + echo " ❌ $COMPONENT: $PASS passed, $FAIL failed" + fi + [ $FAIL -eq 0 ] +} diff --git a/tests/run-all.sh b/tests/run-all.sh new file mode 100755 index 0000000..59ff907 --- /dev/null +++ b/tests/run-all.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +echo "══════════════════════════════════════════════" +echo " stack e2e test suite" +echo "══════════════════════════════════════════════" +echo "" + +# Ensure the shared Docker network exists before running any test +echo "▶ Ensuring 'opencodeco' network exists..." +stack network > /dev/null 2>&1 +echo " ✓ network ready" +echo "" + +TESTS=( + "test-postgres.sh" + "test-mysql.sh" + "test-redis.sh" + "test-mongo.sh" + "test-kafka.sh" + "test-rabbitmq.sh" + "test-aws.sh" + "test-aws-ministack.sh" + "test-hyperdx.sh" + "test-o11y.sh" +) + +SUITE_PASS=0 +SUITE_FAIL=0 +FAILED_TESTS=() + +for test in "${TESTS[@]}"; do + test_path="$SCRIPT_DIR/$test" + if bash "$test_path"; then + SUITE_PASS=$((SUITE_PASS + 1)) + else + SUITE_FAIL=$((SUITE_FAIL + 1)) + FAILED_TESTS+=("$test") + fi + echo "" +done + +echo "══════════════════════════════════════════════" +echo " Suite results: $SUITE_PASS passed, $SUITE_FAIL failed" +if [ ${#FAILED_TESTS[@]} -gt 0 ]; then + echo "" + echo " Failed:" + for t in "${FAILED_TESTS[@]}"; do + echo " - $t" + done +fi +echo "══════════════════════════════════════════════" + +[ $SUITE_FAIL -eq 0 ] diff --git a/tests/test-aws-ministack.sh b/tests/test-aws-ministack.sh new file mode 100755 index 0000000..99e6ef3 --- /dev/null +++ b/tests/test-aws-ministack.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="aws-ministack" +trap 'stack aws-ministack down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +AWS_MINISTACK_PORT="${AWS_MINISTACK_PORT:-4567}" + +if ! start_component aws-ministack; then report; exit 1; fi + +# MiniStack has no Docker healthcheck — wait for TCP port to open +if ! wait_port localhost "$AWS_MINISTACK_PORT"; then + fail "MiniStack port $AWS_MINISTACK_PORT did not open" + report; exit 1 +fi +# Give the service a moment to fully initialize after port opens +sleep 3 + +_aws() { + local tmpconf + tmpconf=$(mktemp) + printf '[default]\nregion = us-east-1\n' > "$tmpconf" + env -u AWS_DEFAULT_PROFILE -u AWS_PROFILE -u AWS_CA_BUNDLE \ + AWS_CONFIG_FILE="$tmpconf" AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \ + aws --endpoint-url="http://localhost:${AWS_MINISTACK_PORT}" "$@" + local ret=$? + rm -f "$tmpconf" + return $ret +} + +if command -v aws > /dev/null 2>&1; then + _aws s3 mb s3://stack-test-bucket > /dev/null 2>&1 + ok "create S3 bucket" + + buckets=$(_aws s3 ls 2>&1) + assert_contains "list S3 buckets" "stack-test-bucket" "$buckets" + + _aws s3 rb s3://stack-test-bucket > /dev/null 2>&1 + ok "delete S3 bucket" +else + ok "MiniStack TCP port $AWS_MINISTACK_PORT is open" +fi + +report diff --git a/tests/test-aws.sh b/tests/test-aws.sh new file mode 100755 index 0000000..117e6f5 --- /dev/null +++ b/tests/test-aws.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="aws" +trap 'stack aws down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +AWS_PORT="${AWS_PORT:-4566}" + +if ! start_component aws; then report; exit 1; fi +if ! wait_http "http://localhost:${AWS_PORT}/_localstack/health"; then + fail "LocalStack health endpoint timed out" + report; exit 1 +fi + +health=$(curl -s "http://localhost:${AWS_PORT}/_localstack/health") +assert_contains "LocalStack health endpoint responds" "available" "$health" + +_aws() { + local tmpconf + tmpconf=$(mktemp) + printf '[default]\nregion = us-east-1\n' > "$tmpconf" + env -u AWS_DEFAULT_PROFILE -u AWS_PROFILE -u AWS_CA_BUNDLE \ + AWS_CONFIG_FILE="$tmpconf" AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \ + aws --endpoint-url="http://localhost:${AWS_PORT}" "$@" + local ret=$? + rm -f "$tmpconf" + return $ret +} + +if command -v aws > /dev/null 2>&1; then + _aws s3 mb s3://stack-test-bucket > /dev/null 2>&1 + ok "create S3 bucket" + + buckets=$(_aws s3 ls 2>&1) + assert_contains "list S3 buckets" "stack-test-bucket" "$buckets" + + _aws s3 rb s3://stack-test-bucket > /dev/null 2>&1 + ok "delete S3 bucket" +else + echo " (skipping S3 operations: 'aws' CLI not found on host)" +fi + +report diff --git a/tests/test-hyperdx.sh b/tests/test-hyperdx.sh new file mode 100755 index 0000000..eb46b2c --- /dev/null +++ b/tests/test-hyperdx.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="hyperdx" +trap 'stack hyperdx down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +HYPERDX_APP_PORT="${HYPERDX_APP_PORT:-8080}" + +if ! start_component hyperdx; then report; exit 1; fi + +# HyperDX has no Docker healthcheck — wait for the app UI port +if ! wait_http "http://localhost:${HYPERDX_APP_PORT}"; then + fail "HyperDX UI did not respond" + report; exit 1 +fi + +code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${HYPERDX_APP_PORT}" 2>/dev/null) +assert_contains "HyperDX UI responds with 2xx" "2" "$code" + +# Container name is opencodeco-hyperx (not opencodeco-hyperdx) +running=$(docker inspect --format='{{.State.Status}}' opencodeco-hyperx 2>/dev/null) +assert_eq "container is running" "running" "$running" + +report diff --git a/tests/test-kafka.sh b/tests/test-kafka.sh new file mode 100755 index 0000000..2899890 --- /dev/null +++ b/tests/test-kafka.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="kafka" +trap 'stack kafka down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +KAFKA_PORT="${KAFKA_PORT:-9092}" +if port_in_use "$KAFKA_PORT"; then + echo " ⚠ skipped: port $KAFKA_PORT already in use" + exit 0 +fi + +if ! start_component kafka; then report; exit 1; fi +if ! wait_healthy opencodeco-kafka; then fail "kafka did not become healthy"; report; exit 1; fi + +TOPIC="stack-test-topic-$$" + +create=$(docker exec opencodeco-kafka /opt/kafka/bin/kafka-topics.sh \ + --bootstrap-server localhost:9092 \ + --create --topic "$TOPIC" --partitions 1 --replication-factor 1 2>&1) +assert_contains "create topic" "Created topic" "$create" + +echo "hello-from-stack" | docker exec -i opencodeco-kafka /opt/kafka/bin/kafka-console-producer.sh \ + --bootstrap-server localhost:9092 \ + --topic "$TOPIC" > /dev/null 2>&1 +ok "produce message" + +consumed=$(docker exec opencodeco-kafka /opt/kafka/bin/kafka-console-consumer.sh \ + --bootstrap-server localhost:9092 \ + --topic "$TOPIC" \ + --from-beginning \ + --max-messages 1 \ + --timeout-ms 10000 2>/dev/null) +assert_contains "consume message" "hello-from-stack" "$consumed" + +docker exec opencodeco-kafka /opt/kafka/bin/kafka-topics.sh \ + --bootstrap-server localhost:9092 \ + --delete --topic "$TOPIC" > /dev/null 2>&1 +ok "delete topic" + +report diff --git a/tests/test-mongo.sh b/tests/test-mongo.sh new file mode 100755 index 0000000..67c5ffe --- /dev/null +++ b/tests/test-mongo.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="mongo" +trap 'stack mongo down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +MONGO_PORT="${MONGO_PORT:-27017}" +if port_in_use "$MONGO_PORT"; then + echo " ⚠ skipped: port $MONGO_PORT already in use" + exit 0 +fi + +if ! start_component mongo; then report; exit 1; fi +if ! wait_healthy opencodeco-mongo; then fail "mongo did not become healthy"; report; exit 1; fi + +insert=$(docker exec opencodeco-mongo mongosh --quiet --eval " + db = db.getSiblingDB('stack_test'); + db.items.insertOne({ val: 'hello' }); + JSON.stringify(db.items.findOne({ val: 'hello' })); +" 2>&1) +assert_contains "insertOne and findOne" "hello" "$insert" + +cleanup=$(docker exec opencodeco-mongo mongosh --quiet --eval " + db.getSiblingDB('stack_test').dropDatabase(); +" 2>&1) +assert_contains "drop test database" "ok" "$cleanup" + +report diff --git a/tests/test-mysql.sh b/tests/test-mysql.sh new file mode 100755 index 0000000..087f001 --- /dev/null +++ b/tests/test-mysql.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="mysql" +trap 'stack mysql down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +MYSQL_PORT="${MYSQL_PORT:-3306}" +if port_in_use "$MYSQL_PORT"; then + echo " ⚠ skipped: port $MYSQL_PORT already in use" + exit 0 +fi + +if ! start_component mysql; then report; exit 1; fi +if ! wait_healthy opencodeco-mysql; then fail "mysql did not become healthy"; report; exit 1; fi + +result=$(docker exec opencodeco-mysql mysql -u root -popencodeco -tNe " + CREATE DATABASE IF NOT EXISTS stack_test; + USE stack_test; + CREATE TABLE IF NOT EXISTS items (id INT AUTO_INCREMENT PRIMARY KEY, val VARCHAR(255)); + INSERT INTO items (val) VALUES ('hello'); + SELECT val FROM items WHERE val = 'hello'; + DROP DATABASE stack_test; +" 2>/dev/null) +assert_contains "create db, table, insert, and select" "hello" "$result" + +version=$(docker exec opencodeco-mysql mysql -u root -popencodeco -tNe "SELECT VERSION();" 2>/dev/null) +assert_contains "MySQL 9.x is running" "9." "$version" + +report diff --git a/tests/test-o11y.sh b/tests/test-o11y.sh new file mode 100755 index 0000000..87a8c08 --- /dev/null +++ b/tests/test-o11y.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="o11y" +trap 'stack o11y down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +JAEGER_UI_PORT="${JAEGER_UI_PORT:-8034}" +PROMETHEUS_PORT="${PROMETHEUS_PORT:-8035}" +GRAFANA_PORT="${GRAFANA_PORT:-8036}" +OTLP_HTTP_PORT="${OTLP_HTTP_PORT:-4318}" + +if ! start_component o11y; then report; exit 1; fi + +# None of the o11y services have Docker healthchecks — wait via HTTP +if ! wait_http "http://localhost:${JAEGER_UI_PORT}"; then + fail "Jaeger UI did not respond"; report; exit 1 +fi +if ! wait_http "http://localhost:${PROMETHEUS_PORT}/-/healthy"; then + fail "Prometheus did not respond"; report; exit 1 +fi +if ! wait_http "http://localhost:${GRAFANA_PORT}/api/health"; then + fail "Grafana did not respond"; report; exit 1 +fi + +jaeger_code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${JAEGER_UI_PORT}" 2>/dev/null) +assert_contains "Jaeger UI responds with 2xx" "2" "$jaeger_code" + +prom_health=$(curl -s "http://localhost:${PROMETHEUS_PORT}/-/healthy" 2>/dev/null) +assert_contains "Prometheus is healthy" "Healthy" "$prom_health" + +prom_config=$(curl -s "http://localhost:${PROMETHEUS_PORT}/api/v1/status/config" 2>/dev/null) +assert_contains "Prometheus scrape job is configured" "opencodeco" "$prom_config" + +grafana_health=$(curl -s "http://localhost:${GRAFANA_PORT}/api/health" 2>/dev/null) +assert_contains "Grafana is healthy" "ok" "$grafana_health" + +otlp_code=$(curl -s -o /dev/null -w "%{http_code}" \ + --connect-timeout 3 \ + "http://localhost:${OTLP_HTTP_PORT}/" 2>/dev/null) +ok "OTel Collector OTLP HTTP is reachable (got HTTP $otlp_code)" + +report diff --git a/tests/test-postgres.sh b/tests/test-postgres.sh new file mode 100755 index 0000000..ca919f0 --- /dev/null +++ b/tests/test-postgres.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="postgres" +trap 'stack postgres down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +if port_in_use "$POSTGRES_PORT"; then + echo " ⚠ skipped: port $POSTGRES_PORT already in use" + exit 0 +fi + +if ! start_component postgres; then report; exit 1; fi +if ! wait_healthy opencodeco-postgres; then fail "postgres did not become healthy"; report; exit 1; fi + +result=$(docker exec opencodeco-postgres psql -U postgres -tAc " + CREATE TABLE IF NOT EXISTS stack_test (id SERIAL PRIMARY KEY, val TEXT); + INSERT INTO stack_test (val) VALUES ('hello'); + SELECT val FROM stack_test WHERE val = 'hello'; + DROP TABLE stack_test; +" 2>&1) +assert_contains "create table, insert, and select" "hello" "$result" + +db_list=$(docker exec opencodeco-postgres psql -U postgres -tAc "SELECT datname FROM pg_database;" 2>&1) +assert_contains "default postgres database exists" "postgres" "$db_list" + +report diff --git a/tests/test-rabbitmq.sh b/tests/test-rabbitmq.sh new file mode 100755 index 0000000..45a830c --- /dev/null +++ b/tests/test-rabbitmq.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="rabbitmq" +trap 'stack rabbitmq down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +RABBITMQ_PORT="${RABBITMQ_PORT:-5672}" +if port_in_use "$RABBITMQ_PORT"; then + echo " ⚠ skipped: port $RABBITMQ_PORT already in use" + exit 0 +fi + +if ! start_component rabbitmq; then report; exit 1; fi +if ! wait_healthy opencodeco-rabbitmq; then fail "rabbitmq did not become healthy"; report; exit 1; fi + +ping=$(docker exec opencodeco-rabbitmq rabbitmq-diagnostics -q ping 2>&1) +assert_contains "broker ping" "Ping succeeded" "$ping" + +docker exec opencodeco-rabbitmq rabbitmqadmin -u opencodeco -p opencodeco \ + declare queue --name stack-test-queue --durable false > /dev/null 2>&1 +ok "declare queue" + +docker exec opencodeco-rabbitmq rabbitmqadmin -u opencodeco -p opencodeco \ + publish message --routing-key stack-test-queue --payload "hello" > /dev/null 2>&1 +ok "publish message" + +message=$(docker exec opencodeco-rabbitmq rabbitmqadmin -u opencodeco -p opencodeco \ + get messages --queue stack-test-queue 2>&1) +assert_contains "get message from queue" "hello" "$message" + +docker exec opencodeco-rabbitmq rabbitmqadmin -u opencodeco -p opencodeco \ + delete queue --name stack-test-queue > /dev/null 2>&1 +ok "delete queue" + +report diff --git a/tests/test-redis.sh b/tests/test-redis.sh new file mode 100755 index 0000000..73f956a --- /dev/null +++ b/tests/test-redis.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=tests/lib.sh +source "$SCRIPT_DIR/lib.sh" + +COMPONENT="redis" +trap 'stack redis down -v > /dev/null 2>&1' EXIT + +echo "▶ $COMPONENT" + +source "$REPO_ROOT/.env.dist" +REDIS_PORT="${REDIS_PORT:-6379}" +if port_in_use "$REDIS_PORT"; then + echo " ⚠ skipped: port $REDIS_PORT already in use" + exit 0 +fi + +if ! start_component redis; then report; exit 1; fi +if ! wait_healthy opencodeco-redis; then fail "redis did not become healthy"; report; exit 1; fi + +set_result=$(docker exec opencodeco-redis redis-cli SET stack:test "hello" 2>&1) +assert_eq "SET key" "OK" "$set_result" + +get_result=$(docker exec opencodeco-redis redis-cli GET stack:test 2>&1) +assert_eq "GET key returns value" "hello" "$get_result" + +del_result=$(docker exec opencodeco-redis redis-cli DEL stack:test 2>&1) +assert_eq "DEL key" "1" "$del_result" + +ping=$(docker exec opencodeco-redis redis-cli PING 2>&1) +assert_eq "PING returns PONG" "PONG" "$ping" + +report