diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 0178d61..3b5e748 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -378,13 +378,13 @@ jobs: shell: bash run: | set -euo pipefail - bash scripts/package-docker-dist.sh docker/dist dist/attune-docker-dist.tar.gz + bash scripts/package-docker-dist.sh docker/distributable artifacts/attune-docker-dist.tar.gz - name: Upload docker dist archive uses: actions/upload-artifact@v4 with: name: attune-docker-dist-${{ needs.metadata.outputs.image_tag }} - path: dist/attune-docker-dist.tar.gz + path: artifacts/attune-docker-dist.tar.gz if-no-files-found: error - name: Attach docker dist archive to release @@ -404,7 +404,7 @@ jobs: api_base="${{ github.server_url }}/api/v1" owner_repo="${{ github.repository }}" tag_name="${{ github.ref_name }}" - archive_path="dist/attune-docker-dist.tar.gz" + archive_path="artifacts/attune-docker-dist.tar.gz" asset_name="attune-docker-dist-${tag_name}.tar.gz" release_response_file="$(mktemp)" diff --git a/docker/distributable/README.md b/docker/distributable/README.md new file mode 100644 index 0000000..8627b26 --- /dev/null +++ b/docker/distributable/README.md @@ -0,0 +1,64 @@ +# Attune Docker Dist Bundle + +This directory is a distributable Docker bundle built from the main workspace compose setup. + +It is designed to run Attune without building the Rust services locally: + +- `api`, `executor`, `notifier`, `agent`, and `web` pull published images +- database bootstrap, user bootstrap, and pack loading run from local scripts shipped in this bundle +- workers and sensor still use stock runtime images plus the published injected agent binaries + +## Registry Defaults + +The compose file defaults to: + +- registry: `git.rdrx.app/attune-system` +- tag: `latest` + +Override them with env vars: + +```bash +export ATTUNE_IMAGE_REGISTRY=git.rdrx.app/attune-system +export ATTUNE_IMAGE_TAG=latest +``` + +If the registry requires auth: + +```bash +docker login git.rdrx.app +``` + +## Run + +From this directory: + +```bash +docker compose up -d +``` + +Or with an explicit tag: + +```bash +ATTUNE_IMAGE_TAG=sha-xxxxxxxxxxxx docker compose up -d +``` + +## Rebuild Bundle + +Refresh this bundle and create a tarball from the workspace root: + +```bash +bash scripts/package-docker-dist.sh +``` + +## Included Assets + +- `docker-compose.yaml` - published-image compose stack +- `config.docker.yaml` - container config mounted into services +- `docker/` - init scripts and SQL helpers +- `migrations/` - schema migrations for the bootstrap job +- `packs/core/` - builtin core pack content +- `scripts/load_core_pack.py` - pack loader used by `init-packs` + +## Current Limitation + +The publish workflow does not currently publish dedicated worker or sensor runtime images. This bundle therefore keeps using stock runtime images with the published `attune/agent` image for injection. diff --git a/docker/distributable/docker-compose.yaml b/docker/distributable/docker-compose.yaml new file mode 100644 index 0000000..da6b89a --- /dev/null +++ b/docker/distributable/docker-compose.yaml @@ -0,0 +1,581 @@ +name: attune + +services: + postgres: + image: timescale/timescaledb:2.17.2-pg16 + container_name: attune-postgres + environment: + POSTGRES_USER: attune + POSTGRES_PASSWORD: attune + POSTGRES_DB: attune + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U attune"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - attune-network + restart: unless-stopped + + migrations: + image: postgres:16-alpine + container_name: attune-migrations + volumes: + - ./migrations:/migrations:ro + - ./docker/run-migrations.sh:/run-migrations.sh:ro + - ./docker/init-roles.sql:/docker/init-roles.sql:ro + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: attune + DB_PASSWORD: attune + DB_NAME: attune + MIGRATIONS_DIR: /migrations + command: ["/bin/sh", "/run-migrations.sh"] + depends_on: + postgres: + condition: service_healthy + networks: + - attune-network + restart: on-failure + + init-user: + image: postgres:16-alpine + container_name: attune-init-user + volumes: + - ./docker/init-user.sh:/init-user.sh:ro + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: attune + DB_PASSWORD: attune + DB_NAME: attune + DB_SCHEMA: public + TEST_LOGIN: ${ATTUNE_TEST_LOGIN:-test@attune.local} + TEST_PASSWORD: ${ATTUNE_TEST_PASSWORD:-TestPass123!} + TEST_DISPLAY_NAME: ${ATTUNE_TEST_DISPLAY_NAME:-Test User} + command: ["/bin/sh", "/init-user.sh"] + depends_on: + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + networks: + - attune-network + restart: on-failure + + init-packs: + image: python:3.11-slim + container_name: attune-init-packs + volumes: + - ./packs:/source/packs:ro + - ./scripts/load_core_pack.py:/scripts/load_core_pack.py:ro + - ./docker/init-packs.sh:/init-packs.sh:ro + - packs_data:/opt/attune/packs + - runtime_envs:/opt/attune/runtime_envs + - artifacts_data:/opt/attune/artifacts + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: attune + DB_PASSWORD: attune + DB_NAME: attune + DB_SCHEMA: public + SOURCE_PACKS_DIR: /source/packs + TARGET_PACKS_DIR: /opt/attune/packs + LOADER_SCRIPT: /scripts/load_core_pack.py + DEFAULT_ADMIN_LOGIN: ${ATTUNE_TEST_LOGIN:-test@attune.local} + DEFAULT_ADMIN_PERMISSION_SET_REF: core.admin + command: ["/bin/sh", "/init-packs.sh"] + depends_on: + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + networks: + - attune-network + restart: on-failure + entrypoint: "" + + init-agent: + image: ${ATTUNE_IMAGE_REGISTRY:-git.rdrx.app/attune-system}/attune/agent:${ATTUNE_IMAGE_TAG:-latest} + container_name: attune-init-agent + volumes: + - agent_bin:/opt/attune/agent + entrypoint: + [ + "/bin/sh", + "-c", + "cp /usr/local/bin/attune-agent /opt/attune/agent/attune-agent && cp /usr/local/bin/attune-sensor-agent /opt/attune/agent/attune-sensor-agent && chmod +x /opt/attune/agent/attune-agent /opt/attune/agent/attune-sensor-agent && /usr/local/bin/attune-agent --version > /opt/attune/agent/attune-agent.version && /usr/local/bin/attune-sensor-agent --version > /opt/attune/agent/attune-sensor-agent.version && echo 'Agent binaries copied successfully'", + ] + restart: "no" + networks: + - attune-network + + rabbitmq: + image: rabbitmq:3.13-management-alpine + container_name: attune-rabbitmq + environment: + RABBITMQ_DEFAULT_USER: attune + RABBITMQ_DEFAULT_PASS: attune + RABBITMQ_DEFAULT_VHOST: / + ports: + - "5672:5672" + - "15672:15672" + volumes: + - rabbitmq_data:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - attune-network + restart: unless-stopped + + redis: + image: redis:7-alpine + container_name: attune-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - attune-network + restart: unless-stopped + command: redis-server --appendonly yes + + api: + image: ${ATTUNE_IMAGE_REGISTRY:-git.rdrx.app/attune-system}/attune/api:${ATTUNE_IMAGE_TAG:-latest} + container_name: attune-api + environment: + RUST_LOG: info + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE__CACHE__URL: redis://redis:6379 + ATTUNE__WORKER__WORKER_TYPE: container + ports: + - "8080:8080" + volumes: + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - packs_data:/opt/attune/packs:rw + - runtime_envs:/opt/attune/runtime_envs + - artifacts_data:/opt/attune/artifacts + - api_logs:/opt/attune/logs + - agent_bin:/opt/attune/agent:ro + depends_on: + init-agent: + condition: service_completed_successfully + init-packs: + condition: service_completed_successfully + init-user: + condition: service_completed_successfully + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + executor: + image: ${ATTUNE_IMAGE_REGISTRY:-git.rdrx.app/attune-system}/attune/executor:${ATTUNE_IMAGE_TAG:-latest} + container_name: attune-executor + environment: + RUST_LOG: info + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE__CACHE__URL: redis://redis:6379 + ATTUNE__WORKER__WORKER_TYPE: container + volumes: + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - packs_data:/opt/attune/packs:ro + - artifacts_data:/opt/attune/artifacts:ro + - executor_logs:/opt/attune/logs + depends_on: + init-packs: + condition: service_completed_successfully + init-user: + condition: service_completed_successfully + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "kill -0 1 || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + worker-shell: + image: debian:bookworm-slim + container_name: attune-worker-shell + entrypoint: ["/opt/attune/agent/attune-agent"] + stop_grace_period: 45s + environment: + RUST_LOG: info + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE_WORKER_TYPE: container + ATTUNE_WORKER_NAME: worker-shell-01 + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE_API_URL: http://attune-api:8080 + volumes: + - agent_bin:/opt/attune/agent:ro + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - packs_data:/opt/attune/packs:ro + - runtime_envs:/opt/attune/runtime_envs + - artifacts_data:/opt/attune/artifacts + - worker_shell_logs:/opt/attune/logs + depends_on: + init-agent: + condition: service_completed_successfully + init-packs: + condition: service_completed_successfully + init-user: + condition: service_completed_successfully + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep -f attune-agent || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + worker-python: + image: python:3.12-slim + container_name: attune-worker-python + entrypoint: ["/opt/attune/agent/attune-agent"] + stop_grace_period: 45s + environment: + RUST_LOG: info + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE_WORKER_TYPE: container + ATTUNE_WORKER_NAME: worker-python-01 + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE_API_URL: http://attune-api:8080 + volumes: + - agent_bin:/opt/attune/agent:ro + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - packs_data:/opt/attune/packs:ro + - runtime_envs:/opt/attune/runtime_envs + - artifacts_data:/opt/attune/artifacts + - worker_python_logs:/opt/attune/logs + depends_on: + init-agent: + condition: service_completed_successfully + init-packs: + condition: service_completed_successfully + init-user: + condition: service_completed_successfully + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep -f attune-agent || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + worker-node: + image: node:22-slim + container_name: attune-worker-node + entrypoint: ["/opt/attune/agent/attune-agent"] + stop_grace_period: 45s + environment: + RUST_LOG: info + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE_WORKER_TYPE: container + ATTUNE_WORKER_NAME: worker-node-01 + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE_API_URL: http://attune-api:8080 + volumes: + - agent_bin:/opt/attune/agent:ro + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - packs_data:/opt/attune/packs:ro + - runtime_envs:/opt/attune/runtime_envs + - artifacts_data:/opt/attune/artifacts + - worker_node_logs:/opt/attune/logs + depends_on: + init-agent: + condition: service_completed_successfully + init-packs: + condition: service_completed_successfully + init-user: + condition: service_completed_successfully + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep -f attune-agent || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + worker-full: + image: nikolaik/python-nodejs:python3.12-nodejs22-slim + container_name: attune-worker-full + entrypoint: ["/opt/attune/agent/attune-agent"] + stop_grace_period: 45s + environment: + RUST_LOG: info + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE_WORKER_RUNTIMES: shell,python,node,native + ATTUNE_WORKER_TYPE: container + ATTUNE_WORKER_NAME: worker-full-01 + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE_API_URL: http://attune-api:8080 + volumes: + - agent_bin:/opt/attune/agent:ro + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - packs_data:/opt/attune/packs:ro + - runtime_envs:/opt/attune/runtime_envs + - artifacts_data:/opt/attune/artifacts + - worker_full_logs:/opt/attune/logs + depends_on: + init-agent: + condition: service_completed_successfully + init-packs: + condition: service_completed_successfully + init-user: + condition: service_completed_successfully + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pgrep -f attune-agent || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + sensor: + image: nikolaik/python-nodejs:python3.12-nodejs22-slim + container_name: attune-sensor + entrypoint: ["/opt/attune/agent/attune-sensor-agent"] + stop_grace_period: 45s + environment: + RUST_LOG: debug + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE_SENSOR_RUNTIMES: shell,python,node,native + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE__WORKER__WORKER_TYPE: container + ATTUNE_API_URL: http://attune-api:8080 + ATTUNE_MQ_URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE_PACKS_BASE_DIR: /opt/attune/packs + volumes: + - agent_bin:/opt/attune/agent:ro + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - packs_data:/opt/attune/packs:rw + - runtime_envs:/opt/attune/runtime_envs + - sensor_logs:/opt/attune/logs + depends_on: + init-agent: + condition: service_completed_successfully + init-packs: + condition: service_completed_successfully + init-user: + condition: service_completed_successfully + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "kill -0 1 || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + notifier: + image: ${ATTUNE_IMAGE_REGISTRY:-git.rdrx.app/attune-system}/attune/notifier:${ATTUNE_IMAGE_TAG:-latest} + container_name: attune-notifier + environment: + RUST_LOG: info + ATTUNE_CONFIG: /opt/attune/config/config.yaml + ATTUNE__SECURITY__JWT_SECRET: ${JWT_SECRET:-docker-dev-secret-change-in-production} + ATTUNE__SECURITY__ENCRYPTION_KEY: ${ENCRYPTION_KEY:-docker-dev-encryption-key-please-change-in-production-32plus} + ATTUNE__DATABASE__URL: postgresql://attune:attune@postgres:5432/attune + ATTUNE__DATABASE__SCHEMA: public + ATTUNE__MESSAGE_QUEUE__URL: amqp://attune:attune@rabbitmq:5672 + ATTUNE__WORKER__WORKER_TYPE: container + ports: + - "8081:8081" + volumes: + - ${ATTUNE_DOCKER_CONFIG_PATH:-./config.docker.yaml}:/opt/attune/config/config.yaml:ro + - notifier_logs:/opt/attune/logs + depends_on: + migrations: + condition: service_completed_successfully + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - attune-network + restart: unless-stopped + + web: + image: ${ATTUNE_IMAGE_REGISTRY:-git.rdrx.app/attune-system}/attune/web:${ATTUNE_IMAGE_TAG:-latest} + container_name: attune-web + environment: + API_URL: ${API_URL:-http://localhost:8080} + WS_URL: ${WS_URL:-ws://localhost:8081} + ENVIRONMENT: docker + ports: + - "3000:80" + depends_on: + api: + condition: service_healthy + notifier: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost/health", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - attune-network + restart: unless-stopped + +volumes: + postgres_data: + driver: local + rabbitmq_data: + driver: local + redis_data: + driver: local + api_logs: + driver: local + executor_logs: + driver: local + worker_shell_logs: + driver: local + worker_python_logs: + driver: local + worker_node_logs: + driver: local + worker_full_logs: + driver: local + sensor_logs: + driver: local + notifier_logs: + driver: local + packs_data: + driver: local + runtime_envs: + driver: local + artifacts_data: + driver: local + agent_bin: + driver: local + +networks: + attune-network: + driver: bridge + ipam: + config: + - subnet: 172.28.0.0/16 diff --git a/docker/distributable/docker/init-packs.sh b/docker/distributable/docker/init-packs.sh new file mode 100755 index 0000000..1fceb28 --- /dev/null +++ b/docker/distributable/docker/init-packs.sh @@ -0,0 +1,296 @@ +#!/bin/sh +# Initialize builtin packs for Attune +# This script copies pack files to the shared volume and registers them in the database +# Designed to run on python:3.11-slim (Debian-based) image + +set -e + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration from environment +DB_HOST="${DB_HOST:-postgres}" +DB_PORT="${DB_PORT:-5432}" +DB_USER="${DB_USER:-attune}" +DB_PASSWORD="${DB_PASSWORD:-attune}" +DB_NAME="${DB_NAME:-attune}" +DB_SCHEMA="${DB_SCHEMA:-public}" + +# Pack directories +SOURCE_PACKS_DIR="${SOURCE_PACKS_DIR:-/source/packs}" +TARGET_PACKS_DIR="${TARGET_PACKS_DIR:-/opt/attune/packs}" + +# Python loader script +LOADER_SCRIPT="${LOADER_SCRIPT:-/scripts/load_core_pack.py}" +DEFAULT_ADMIN_LOGIN="${DEFAULT_ADMIN_LOGIN:-}" +DEFAULT_ADMIN_PERMISSION_SET_REF="${DEFAULT_ADMIN_PERMISSION_SET_REF:-core.admin}" + +echo "" +echo -e "${BLUE}╔════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Attune Builtin Packs Initialization ║${NC}" +echo -e "${BLUE}╚════════════════════════════════════════════════╝${NC}" +echo "" + +# Install Python dependencies +echo -e "${YELLOW}→${NC} Installing Python dependencies..." +if pip install --quiet --no-cache-dir psycopg2-binary pyyaml; then + echo -e "${GREEN}✓${NC} Python dependencies installed" +else + echo -e "${RED}✗${NC} Failed to install Python dependencies" + exit 1 +fi +echo "" + +# Wait for database to be ready (using Python instead of psql to avoid needing postgresql-client) +echo -e "${YELLOW}→${NC} Waiting for database to be ready..." +until python3 -c " +import psycopg2, sys +try: + conn = psycopg2.connect(host='$DB_HOST', port=$DB_PORT, user='$DB_USER', password='$DB_PASSWORD', dbname='$DB_NAME', connect_timeout=3) + conn.close() + sys.exit(0) +except Exception: + sys.exit(1) +" 2>/dev/null; do + echo -e "${YELLOW} ...${NC} Database is unavailable - sleeping" + sleep 2 +done +echo -e "${GREEN}✓${NC} Database is ready" + +# Create target packs directory if it doesn't exist +echo -e "${YELLOW}→${NC} Ensuring packs directory exists..." +mkdir -p "$TARGET_PACKS_DIR" +# Ensure the attune user (uid 1000) can write to the packs directory +# so the API service can install packs at runtime +chown -R 1000:1000 "$TARGET_PACKS_DIR" +echo -e "${GREEN}✓${NC} Packs directory ready at: $TARGET_PACKS_DIR" + +# Initialise runtime environments volume with correct ownership. +# Workers (running as attune uid 1000) need write access to create +# virtualenvs, node_modules, etc. at runtime. +RUNTIME_ENVS_DIR="${RUNTIME_ENVS_DIR:-/opt/attune/runtime_envs}" +if [ -d "$RUNTIME_ENVS_DIR" ] || mkdir -p "$RUNTIME_ENVS_DIR" 2>/dev/null; then + chown -R 1000:1000 "$RUNTIME_ENVS_DIR" + echo -e "${GREEN}✓${NC} Runtime environments directory ready at: $RUNTIME_ENVS_DIR" +else + echo -e "${YELLOW}⚠${NC} Runtime environments directory not mounted, skipping" +fi + +# Initialise artifacts volume with correct ownership. +# The API service (creates directories for file-backed artifact versions) and +# workers (write artifact files during execution) both run as attune uid 1000. +ARTIFACTS_DIR="${ARTIFACTS_DIR:-/opt/attune/artifacts}" +if [ -d "$ARTIFACTS_DIR" ] || mkdir -p "$ARTIFACTS_DIR" 2>/dev/null; then + chown -R 1000:1000 "$ARTIFACTS_DIR" + echo -e "${GREEN}✓${NC} Artifacts directory ready at: $ARTIFACTS_DIR" +else + echo -e "${YELLOW}⚠${NC} Artifacts directory not mounted, skipping" +fi + +# Check if source packs directory exists +if [ ! -d "$SOURCE_PACKS_DIR" ]; then + echo -e "${RED}✗${NC} Source packs directory not found: $SOURCE_PACKS_DIR" + exit 1 +fi + +# Find all pack directories (directories with pack.yaml) +echo "" +echo -e "${BLUE}Discovering builtin packs...${NC}" +echo "----------------------------------------" + +PACK_COUNT=0 +COPIED_COUNT=0 +LOADED_COUNT=0 + +for pack_dir in "$SOURCE_PACKS_DIR"/*; do + if [ -d "$pack_dir" ]; then + pack_name=$(basename "$pack_dir") + pack_yaml="$pack_dir/pack.yaml" + + if [ -f "$pack_yaml" ]; then + PACK_COUNT=$((PACK_COUNT + 1)) + echo -e "${BLUE}→${NC} Found pack: ${GREEN}$pack_name${NC}" + + # Check if pack already exists in target + target_pack_dir="$TARGET_PACKS_DIR/$pack_name" + + if [ -d "$target_pack_dir" ]; then + # Pack exists, update files to ensure we have latest (especially binaries) + echo -e "${YELLOW} ⟳${NC} Pack exists at: $target_pack_dir, updating files..." + if cp -rf "$pack_dir"/* "$target_pack_dir"/; then + echo -e "${GREEN} ✓${NC} Updated pack files at: $target_pack_dir" + else + echo -e "${RED} ✗${NC} Failed to update pack" + exit 1 + fi + else + # Copy pack to target directory + echo -e "${YELLOW} →${NC} Copying pack files..." + if cp -r "$pack_dir" "$target_pack_dir"; then + COPIED_COUNT=$((COPIED_COUNT + 1)) + echo -e "${GREEN} ✓${NC} Copied to: $target_pack_dir" + else + echo -e "${RED} ✗${NC} Failed to copy pack" + exit 1 + fi + fi + fi + fi +done + +echo "----------------------------------------" +echo "" + +if [ $PACK_COUNT -eq 0 ]; then + echo -e "${YELLOW}⚠${NC} No builtin packs found in $SOURCE_PACKS_DIR" + echo -e "${BLUE}ℹ${NC} This is OK if you're running with no packs" + exit 0 +fi + +echo -e "${BLUE}Pack Discovery Summary:${NC}" +echo " Total packs found: $PACK_COUNT" +echo " Newly copied: $COPIED_COUNT" +echo " Already present: $((PACK_COUNT - COPIED_COUNT))" +echo "" + +# Load packs into database using Python loader +if [ -f "$LOADER_SCRIPT" ]; then + echo -e "${BLUE}Loading packs into database...${NC}" + echo "----------------------------------------" + + # Build database URL with schema support + DATABASE_URL="postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" + + # Set search_path for the Python script if not using default schema + if [ "$DB_SCHEMA" != "public" ]; then + export PGOPTIONS="-c search_path=$DB_SCHEMA,public" + fi + + # Run the Python loader for each pack + for pack_dir in "$TARGET_PACKS_DIR"/*; do + if [ -d "$pack_dir" ]; then + pack_name=$(basename "$pack_dir") + pack_yaml="$pack_dir/pack.yaml" + + if [ -f "$pack_yaml" ]; then + echo -e "${YELLOW}→${NC} Loading pack: ${GREEN}$pack_name${NC}" + + # Run Python loader + if python3 "$LOADER_SCRIPT" \ + --database-url "$DATABASE_URL" \ + --pack-dir "$TARGET_PACKS_DIR" \ + --pack-name "$pack_name" \ + --schema "$DB_SCHEMA"; then + LOADED_COUNT=$((LOADED_COUNT + 1)) + echo -e "${GREEN}✓${NC} Loaded pack: $pack_name" + else + echo -e "${RED}✗${NC} Failed to load pack: $pack_name" + echo -e "${YELLOW}⚠${NC} Continuing with other packs..." + fi + fi + fi + done + + echo "----------------------------------------" + echo "" + echo -e "${BLUE}Database Loading Summary:${NC}" + echo " Successfully loaded: $LOADED_COUNT" + echo " Failed: $((PACK_COUNT - LOADED_COUNT))" + echo "" +else + echo -e "${YELLOW}⚠${NC} Pack loader script not found: $LOADER_SCRIPT" + echo -e "${BLUE}ℹ${NC} Packs copied but not registered in database" + echo -e "${BLUE}ℹ${NC} You can manually load them later" +fi + +if [ -n "$DEFAULT_ADMIN_LOGIN" ] && [ "$LOADED_COUNT" -gt 0 ]; then + echo "" + echo -e "${BLUE}Bootstrapping local admin assignment...${NC}" + if python3 - </dev/null; do + echo -e "${YELLOW} ...${NC} Database is unavailable - sleeping" + sleep 2 +done +echo -e "${GREEN}✓${NC} Database is ready" + +# Check if user already exists +echo -e "${YELLOW}→${NC} Checking if user exists..." +USER_EXISTS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -tAc \ + "SELECT COUNT(*) FROM ${DB_SCHEMA}.identity WHERE login = '$TEST_LOGIN';") + +if [ "$USER_EXISTS" -gt 0 ]; then + echo -e "${GREEN}✓${NC} User '$TEST_LOGIN' already exists" + echo -e "${BLUE}ℹ${NC} Skipping user creation" +else + echo -e "${YELLOW}→${NC} Creating default test user..." + + # Use the pre-computed hash for default password + if [ "$TEST_PASSWORD" = "TestPass123!" ]; then + PASSWORD_HASH="$DEFAULT_PASSWORD_HASH" + echo -e "${BLUE}ℹ${NC} Using default password hash" + else + echo -e "${YELLOW}⚠${NC} Custom password detected - using basic hash" + echo -e "${YELLOW}⚠${NC} For production, generate proper Argon2id hash" + # Note: For custom passwords in Docker, you should pre-generate the hash + # This is a fallback that will work but is less secure + PASSWORD_HASH="$DEFAULT_PASSWORD_HASH" + fi + + # Insert the user + PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" << EOF +INSERT INTO ${DB_SCHEMA}.identity (login, display_name, password_hash, attributes) +VALUES ( + '$TEST_LOGIN', + '$TEST_DISPLAY_NAME', + '$PASSWORD_HASH', + jsonb_build_object( + 'email', '$TEST_LOGIN', + 'created_via', 'docker-init', + 'is_test_user', true + ) +); +EOF + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓${NC} User created successfully" + else + echo -e "${RED}✗${NC} Failed to create user" + exit 1 + fi +fi + +echo "" +echo -e "${GREEN}╔════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Default User Initialization Complete! ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${BLUE}Default User Credentials:${NC}" +echo -e " Login: ${GREEN}$TEST_LOGIN${NC}" +echo -e " Password: ${GREEN}$TEST_PASSWORD${NC}" +echo "" +echo -e "${BLUE}Test Login:${NC}" +echo -e " ${YELLOW}curl -X POST http://localhost:8080/auth/login \\${NC}" +echo -e " ${YELLOW}-H 'Content-Type: application/json' \\${NC}" +echo -e " ${YELLOW}-d '{\"login\":\"$TEST_LOGIN\",\"password\":\"$TEST_PASSWORD\"}'${NC}" +echo "" +echo -e "${BLUE}ℹ${NC} For custom users, see: docs/testing/test-user-setup.md" +echo "" + +exit 0 diff --git a/docker/distributable/docker/inject-env.sh b/docker/distributable/docker/inject-env.sh new file mode 100755 index 0000000..6884c63 --- /dev/null +++ b/docker/distributable/docker/inject-env.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# inject-env.sh - Injects runtime environment variables into the Web UI +# This script runs at container startup to make environment variables available to the browser + +set -e + +# Default values +API_URL="${API_URL:-http://localhost:8080}" +WS_URL="${WS_URL:-ws://localhost:8081}" + +# Create runtime configuration file +cat > /usr/share/nginx/html/config/runtime-config.js <