UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

483 lines (460 loc) 18.4 kB
# Mesh full-local platform (`mesh start`). # # Shipped inside @mesh-tech/mesh-cli so any repo can run the platform locally # with only Docker installed — no AWS credentials, no VPN, no Pulumi state. # Evolved from tools/local/docker-compose.yml in mesh-platform; every image is # pullable (no monorepo build contexts). The codec server and Temporal UI # gateway require monorepo image builds and join the stack once their images # are published. # # Discovery fabric: ministack (MIT-licensed AWS emulator) provides SSM # Parameter Store + Secrets Manager + S3 so the registry # (/mesh-platform/{tenant}/{env}/…) works locally with unchanged code paths — # services point at it via the SDK-native AWS_ENDPOINT_URL_* env overrides. # `mesh start` seeds tenant `local` / env `dev`. # # SECURITY: every credential in this stack (postgres/postgres, test/test AWS # keys, the Zitadel masterkey, SpiceDB's local-dev-key, permissive CORS) is a # LOCAL-ONLY default. Never reuse any of them outside this compose project — # deployed platforms mint their own secrets. Data/secret ports are bound to # 127.0.0.1 so none of this is reachable from the LAN. name: mesh-local # AWS wiring for in-network services (Hub API/worker join the stack once # their images are published): merge this anchor into a service's environment # and its AWS SDK clients hit the local fabric with zero code changes — # AWS_ENDPOINT_URL is the SDK-native endpoint override. x-local-aws-env: &local-aws-env AWS_ENDPOINT_URL: http://ministack:4566 AWS_REGION: us-east-2 AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test services: database: image: postgres:17@sha256:a426e44bac0b759c95894d68e1a0ac03ecc20b619f498a91aae373bf06d8508d ports: - '127.0.0.1:5433:5432' environment: PGUSER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: temporal volumes: - postgres_data:/var/lib/postgresql/data - ./init:/docker-entrypoint-initdb.d networks: - mesh_network healthcheck: # Single-string CMD-SHELL (an array's extra elements are ignored shell # positionals) probing over TCP: the entrypoint's temporary initdb-phase # server is socket-only, so -h 127.0.0.1 can't go green until the final # server is up — after /docker-entrypoint-initdb.d has created the # spicedb/app/hub databases spicedb-migrate needs. test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -d temporal -U postgres'] interval: '10s' timeout: '30s' retries: 5 start_period: '20s' memcached: image: memcached:1.6-alpine@sha256:c29847751abb41f4c268c84fb3087fee05d4edcbda44409ccb5086e26148e8a7 ports: - '11211:11211' networks: - mesh_network command: -m 128 healthcheck: test: ['CMD', 'nc', '-z', 'localhost', '11211'] interval: 30s timeout: 5s retries: 3 elasticsearch: image: opensearchproject/opensearch:2.11.1 ports: - '9200:9200' environment: - cluster.name=temporal-elasticsearch - discovery.type=single-node - bootstrap.memory_lock=true - "OPENSEARCH_JAVA_OPTS=-Xms256m -Xmx256m" - DISABLE_INSTALL_DEMO_CONFIG=true - DISABLE_SECURITY_PLUGIN=true - compatibility.override_main_response_version=true ulimits: memlock: soft: -1 hard: -1 volumes: - elasticsearch_data:/usr/share/elasticsearch/data networks: - mesh_network healthcheck: # URL must be quoted: an unquoted & backgrounds curl and the check # always passes, letting Temporal start before OpenSearch is up. test: ['CMD-SHELL', 'curl -f "http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=10s" || exit 1'] interval: 30s timeout: 10s retries: 5 temporal: image: temporalio/auto-setup:1.28.1 depends_on: database: condition: service_healthy elasticsearch: condition: service_healthy environment: - DB=postgres12 - DB_PORT=5432 - POSTGRES_USER=postgres - POSTGRES_PWD=postgres - POSTGRES_SEEDS=database - DYNAMIC_CONFIG_FILE_PATH=/etc/temporal/config/dynamicconfig/local.yaml - TEMPORAL_ADDRESS=temporal:7233 - TEMPORAL_CLI_ADDRESS=temporal:7233 - ENABLE_ES=true - ES_SEEDS=elasticsearch - ES_PORT=9200 - ES_VERSION=v7 - ES_VIS_INDEX=temporal_visibility_v1_dev ports: - '7233:7233' volumes: # Mount ONLY the dynamic config — mounting over /etc/temporal/config # would clobber the auto-setup image's config_template.yaml. - ./temporal/local.yaml:/etc/temporal/config/dynamicconfig/local.yaml networks: - mesh_network temporal-admin-tools: image: temporalio/admin-tools:1.13.4 depends_on: - temporal environment: - TEMPORAL_ADDRESS=temporal:7233 - TEMPORAL_CLI_ADDRESS=temporal:7233 networks: - mesh_network stdin_open: true tty: true temporal-ui: image: temporalio/ui:2.16.2 depends_on: - temporal environment: - TEMPORAL_ADDRESS=temporal:7233 - TEMPORAL_CORS_ORIGINS=http://localhost:3000,http://localhost:8233,http://localhost:8080 # Land on the seeded local tenant namespace ({tenant}-{env}). - TEMPORAL_DEFAULT_NAMESPACE=local-dev # Auth comes back with the seeded local Zitadel (mesh start seeds an # org + CLI app + test users in a follow-up; until then the UI is open). - TEMPORAL_AUTH_ENABLED=false ports: # 8233 = Temporal's conventional dev-UI port. Deliberately NOT 8081: # the legacy tools/local stack serves its UI there, and a shared origin # means shared browser localStorage — the UI would auto-redirect to # namespaces remembered from the old stack ("namespace not found"). - '8233:8080' networks: - mesh_network # Zitadel Database zitadel-db: image: postgres:17@sha256:a426e44bac0b759c95894d68e1a0ac03ecc20b619f498a91aae373bf06d8508d ports: - '127.0.0.1:5435:5432' environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: zitadel volumes: - zitadel_postgres_data:/var/lib/postgresql/data networks: - mesh_network healthcheck: # -h 127.0.0.1 (TCP) so the socket-only initdb-phase server can't # satisfy the probe before first-boot init completes. test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -d zitadel -U postgres'] interval: '10s' timeout: '30s' retries: 5 start_period: '20s' # One-shot: the machinekey volume must be writable by the zitadel user # (uid 1000 — the image runs non-root and a fresh named volume is # root-owned; the first-instance PAT write fails otherwise). zitadel-machinekey-init: image: busybox:1.36 command: chown -R 1000:1000 /machinekey restart: 'no' volumes: - zitadel_machinekey:/machinekey networks: - mesh_network # Zitadel Identity Server — v4 to match the cloud (approved tenant-auth # pattern spec item 7: v4's no-role-claims machine tokens make version # parity matter for auth testing). # Local mailbox. Zitadel sends real mail for user activation and password # resets, and without somewhere to send it those flows cannot be exercised at # all locally — the Hub's "resend invitation" and "send password reset email" # actions return 200 and the mail goes nowhere. Mailpit accepts everything on # SMTP :1025 and shows it at :8025; nothing leaves the machine. mailpit: image: axllent/mailpit:v1.21 ports: - '127.0.0.1:1025:1025' # SMTP, for Zitadel - '127.0.0.1:8025:8025' # web UI, for a human environment: MP_SMTP_AUTH_ACCEPT_ANY: 'true' MP_SMTP_AUTH_ALLOW_INSECURE: 'true' healthcheck: test: ['CMD', '/mailpit', 'readyz'] interval: 5s timeout: 3s retries: 20 networks: - mesh_network zitadel: image: ghcr.io/zitadel/zitadel:v4.15.3 depends_on: zitadel-db: condition: 'service_healthy' zitadel-machinekey-init: condition: 'service_completed_successfully' mailpit: condition: 'service_healthy' ports: - '127.0.0.1:8080:8080' environment: ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db ZITADEL_DATABASE_POSTGRES_PORT: 5432 ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable ZITADEL_EXTERNALSECURE: false ZITADEL_EXTERNALDOMAIN: localhost ZITADEL_EXTERNALPORT: 8080 ZITADEL_HTTP_CORS_ALLOWORIGIN: "*" ZITADEL_HTTP_CORS_ALLOWHEADERS: "*" ZITADEL_HTTP_CORS_ALLOWMETHODS: "*" ZITADEL_HTTP_CORS_ALLOWCREDENTIALS: true ZITADEL_HTTP_CSRF_DISABLED: true # v4 defaults new instances to the login-v2 UI, which is a SEPARATE # container this stack doesn't run — without this, the authorize flow # redirects to /ui/v2/login and dead-ends. Keep the built-in v1 login. # (The seeder also flips the instance feature via API, belt-and-braces.) ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: false # Platform org (approved Zitadel tenant-auth pattern): `mesh` is the # home of platform-layer creds (mesh-cli project, test users; Hub when # its images land) — same org name as the cloud platform tenant. App # tenants get their own orgs, auto-created by `mesh dev --local` # (ZitadelTenantIdentity parity); an app with no `mesh:tenant` config # lands in the default app-tenant org `local`. ZITADEL_FIRSTINSTANCE_ORG_NAME: mesh ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: admin@local.mesh ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: LocalDev1! ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false # Bootstrap machine user for the `mesh start` seeder: its PAT lands on # the machinekey volume and the CLI reads it (docker compose cp) to # create the Platform project, Mesh CLI OIDC app, and test users. # First-instance settings only apply on the FIRST init — a pre-existing # zitadel volume needs `mesh stop --destroy` to pick this up. ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: local-seeder ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Local Seeder ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: '2099-01-01T00:00:00Z' ZITADEL_FIRSTINSTANCE_PATPATH: /machinekey/pat.txt # SMTP for the default instance, pointed at the local mailbox above. # DEFAULTINSTANCE settings apply at FIRST INIT only, same as the # FIRSTINSTANCE block — a pre-existing zitadel volume keeps whatever it # was initialised with, so `mesh start` also reconciles this over the # admin API on every run (see seed-zitadel.ts). The env is here so a # fresh `mesh start` has working mail before the seeder even runs. ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_HOST: 'mailpit:1025' ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_USER: '' ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_PASSWORD: '' ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_TLS: false ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROM: 'no-reply@local.mesh' ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROMNAME: 'Mesh (local)' command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --init-projections=true' volumes: - zitadel_machinekey:/machinekey networks: - mesh_network # SpiceDB migration (runs to completion before spicedb serves) spicedb-migrate: image: authzed/spicedb:v1.42.1 command: datastore migrate head restart: 'no' depends_on: database: condition: service_healthy environment: SPICEDB_DATASTORE_ENGINE: postgres SPICEDB_DATASTORE_CONN_URI: postgres://postgres:postgres@database:5432/spicedb?sslmode=disable networks: - mesh_network # SpiceDB authorization engine spicedb: image: authzed/spicedb:v1.42.1 command: serve depends_on: spicedb-migrate: condition: service_completed_successfully environment: SPICEDB_GRPC_PRESHARED_KEY: local-dev-key SPICEDB_DATASTORE_ENGINE: postgres SPICEDB_DATASTORE_CONN_URI: postgres://postgres:postgres@database:5432/spicedb?sslmode=disable SPICEDB_HTTP_ENABLED: true ports: - '127.0.0.1:50051:50051' - '127.0.0.1:8443:8443' networks: - mesh_network healthcheck: test: ['CMD', 'grpc_health_probe', '-addr=localhost:50051'] interval: 10s timeout: 5s retries: 5 start_period: 10s # ministack — the local AWS fabric (SSM Parameter Store + Secrets Manager + # S3, all services on one port). MIT-licensed LocalStack replacement (design # §2.5). Point AWS SDK clients at it via AWS_ENDPOINT_URL=http://localhost:4566. # # State persists across restarts (PERSIST_STATE saves to the volume on # graceful shutdown, reloads on boot): the registry and app secrets that # `mesh dev` provisions live ONLY here, so without this a Docker Desktop # restart silently wiped every provisioned app while its services kept # running stale. `mesh start` still re-seeds platform state idempotently; # `mesh stop --destroy` removes the volume like every other data volume. ministack: image: ministackorg/ministack:1.4.1 ports: - '127.0.0.1:4566:4566' environment: MINISTACK_REGION: us-east-2 PERSIST_STATE: '1' STATE_DIR: /var/lib/ministack-state volumes: - ministack_state:/var/lib/ministack-state networks: - mesh_network healthcheck: # Pure-Python probe — the ministack image ships no curl/wget. test: ['CMD', 'python', '-c', "import urllib.request; urllib.request.urlopen('http://localhost:4566/_ministack/health')"] interval: 10s timeout: 5s retries: 10 start_period: 5s # StackPort — local AWS console over the ministack fabric (MIT). Browse and # inspect the registry (SSM), Secrets Manager, and S3 artifacts visually; # E4's terminal complement until the local Hub lands. First consumer of the # x-local-aws-env anchor. Host 4567 (container serves on 8080 = Zitadel's # host port). stackport: image: davireis/stackport:0.3.4 ports: - '127.0.0.1:4567:8080' environment: *local-aws-env depends_on: ministack: condition: service_healthy networks: - mesh_network # ── Observability — the SAME backends the hosted platform runs ────────── # OTel collector → Loki (logs, OTLP) + Tempo (traces, OTLP gRPC), Prometheus # scraping the collector's metrics exporter. hub-api's LOKI_URL / TEMPO_URL / # PROMETHEUS_URL point here, so the Hub's logs/traces/metrics views run the # exact hosted code paths. `mesh dev --local` service logs are tailed from # ${MESH_LOCAL_LOGS} (written via tmux pipe-pane) and labeled with the same # k8s_namespace_name/k8s_deployment_name scheme the in-cluster # k8sattributes processor produces. loki: image: grafana/loki:3.4.2 command: -config.file=/etc/loki/local-config.yaml ports: - '3100:3100' volumes: # The image's default local-config keeps all state under /tmp/loki. - loki_data:/tmp/loki networks: - mesh_network tempo: image: grafana/tempo:2.7.1 command: -config.file=/etc/tempo/tempo.yaml ports: - '3200:3200' volumes: - ./observability/tempo.yaml:/etc/tempo/tempo.yaml - tempo_data:/var/tempo networks: - mesh_network prometheus: image: prom/prometheus:v3.2.1 ports: - '9090:9090' volumes: - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml # External-service probe targets (blackbox file_sd) — written by # `mesh dev --externals` / ExternalService wiring, same labels the hosted # Probe CRs carry (type/tenant/env/app/external_service). - ${MESH_LOCAL_PROBES:-/tmp/mesh-local/probes}:/etc/prometheus/probes - prometheus_data:/prometheus networks: - mesh_network # Blackbox exporter — the same prober the hosted monitoring stack runs for # ExternalService health checks; Prometheus drives it via the # external-services file_sd job. blackbox-exporter: image: prom/blackbox-exporter:v0.25.0 command: --config.file=/etc/blackbox/blackbox.yml ports: - '9115:9115' networks: - mesh_network volumes: # IPv4-pinned http_2xx/tcp_connect modules — host.docker.internal's # IPv6 ULA is unreachable from the container network. - ./observability/blackbox.yml:/etc/blackbox/blackbox.yml extra_hosts: # Probe targets run on the HOST (mesh dev services/mocks). - 'host.docker.internal:host-gateway' otel-collector: image: otel/opentelemetry-collector-contrib:0.121.0 command: --config=/etc/otelcol/config.yaml ports: - '4317:4317' # OTLP gRPC (apps: OTEL_EXPORTER_OTLP_ENDPOINT) - '4318:4318' # OTLP HTTP - '13133:13133' # health_check extension (mesh status probe) volumes: - ./observability/otel-collector.yaml:/etc/otelcol/config.yaml # mesh dev --local writes {namespace}/{service}.log here (the CLI passes # MESH_LOCAL_LOGS on every compose call; the fallback keeps raw # `docker compose` usable). - ${MESH_LOCAL_LOGS:-/tmp/mesh-local/logs}:/var/log/mesh-apps:ro depends_on: - loki - tempo networks: - mesh_network healthcheck: test: ['CMD', '/otelcol-contrib', '--version'] interval: 30s timeout: 5s retries: 3 volumes: postgres_data: driver: local ministack_state: driver: local elasticsearch_data: driver: local zitadel_postgres_data: driver: local zitadel_machinekey: driver: local loki_data: driver: local tempo_data: driver: local prometheus_data: driver: local networks: mesh_network: driver: bridge