diff --git a/Makefile b/Makefile index 8bc5d4e0d7..30d9cde48b 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,24 @@ docker_start_spanner_rebuild: docker_stop_spanner: docker compose -f docker/docker-compose.spanner.yaml down +docker_oneshot_mysql: ## Build & run a stand-alone MySQL Syncserver (curl localhost:8000/__heartbeat__). + docker compose -f docker/docker-compose.one-shot.mysql.yaml up -d --build + +docker_oneshot_mysql_stop: ## Stop the stand-alone MySQL Syncserver. + docker compose -f docker/docker-compose.one-shot.mysql.yaml down + +docker_oneshot_postgres: ## Build & run a stand-alone PostgreSQL Syncserver (curl localhost:8000/__heartbeat__). + docker compose -f docker/docker-compose.one-shot.postgres.yaml up -d --build + +docker_oneshot_postgres_stop: ## Stop the stand-alone PostgreSQL Syncserver. + docker compose -f docker/docker-compose.one-shot.postgres.yaml down + +docker_oneshot_spanner: ## Build & run a stand-alone Spanner-emulator Syncserver, local dev only (curl localhost:8000/__heartbeat__). + docker compose -f docker/docker-compose.one-shot.spanner.yaml up -d --build + +docker_oneshot_spanner_stop: ## Stop the stand-alone Spanner-emulator Syncserver. + docker compose -f docker/docker-compose.one-shot.spanner.yaml down + .ONESHELL: docker_run_mysql_e2e_tests: exit_code=0 diff --git a/config/local.example.spanner.toml b/config/local.example.spanner.toml new file mode 100644 index 0000000000..34ab6488c8 --- /dev/null +++ b/config/local.example.spanner.toml @@ -0,0 +1,101 @@ +# ============================================================================= +# Default out-of-the-box configuration: Spanner build +# ============================================================================= +# +# This is a ready-to-edit template for running syncstorage-rs against the +# standard Spanner build (`make run_spanner`, the `syncstorage-rs-spanner` +# Docker image). This mirrors the production topology: Syncstorage runs against +# Google Cloud Spanner, while Tokenserver runs against MySQL. +# +# Copy it to `config/local.toml` and edit the values marked REQUIRED: +# +# cp config/local.example.spanner.toml config/local.toml +# +# Every setting can also be supplied as an environment variable prefixed with +# `SYNC_` (nested keys use `__`), e.g. `syncstorage.database_url` becomes +# `SYNC_SYNCSTORAGE__DATABASE_URL`. Environment variables take precedence over +# the config file. See the full reference at docs/src/config.md, and the +# annotated `Settings` structs in `*-settings/src/lib.rs`. +# +# To run against the local Spanner emulator instead of real Spanner, set +# `syncstorage.spanner_emulator_host` (see below) and point the +# `GOOGLE_APPLICATION_CREDENTIALS` env var at a service-account key for real +# Spanner only. +# +# For the all-MySQL build, see `config/local.example.toml`. +# ============================================================================= + +# REQUIRED. Secret used to derive the Hawk signing/token secrets. Use a long, +# random string in any real deployment. No default. +master_secret = "INSERT_SECRET_KEY_HERE" + +# Host/port the server binds to. Defaults: host = "127.0.0.1", port = 8000. +# Use "0.0.0.0" inside containers so the port is reachable from the host. +host = "127.0.0.1" +port = 8000 + +# Emit human-readable logs instead of mozlog JSON. Default: false (0). +human_logs = 1 + +# ----------------------------------------------------------------------------- +# Syncstorage (BSO storage service) — Spanner +# ----------------------------------------------------------------------------- + +# Enable the Syncstorage service. Default: true. +syncstorage.enabled = true + +# REQUIRED when syncstorage is enabled. Spanner DSN, of the form +# spanner://projects//instances//databases/. +# No usable default; the server fails fast at startup if it is unset. A +# "spanner://" scheme is what switches the syncstorage backend into Spanner +# mode (see `Settings::uses_spanner`). +syncstorage.database_url = "spanner://projects/SAMPLE_GCP_PROJECT/instances/SAMPLE_SPANNER_INSTANCE/databases/SAMPLE_SPANNER_DB" + +# Local Spanner emulator host. Leave unset for real Spanner. When running the +# emulator (e.g. via docker/docker-compose.spanner.yaml) set this to the +# emulator's gRPC address so the client talks to the emulator instead of GCP. +# syncstorage.spanner_emulator_host = "localhost:9010" + +# Max size of the syncstorage DB connection pool. Default: 10. +# syncstorage.database_pool_max_size = 10 + +# Quota tracking/enforcement (Spanner-only). Both default to false. Enable +# tracking to record per-user usage; additionally enable enforcement to reject +# writes over `limits.max_quota_limit` (default 2 GB). +syncstorage.enable_quota = 0 +# syncstorage.enforce_quota = 0 + +# ----------------------------------------------------------------------------- +# Tokenserver (node assignment + FxA OAuth verification) — MySQL +# ----------------------------------------------------------------------------- + +# Enable Tokenserver. Default: false. +tokenserver.enabled = true + +# REQUIRED when tokenserver is enabled. In the production topology Tokenserver +# uses MySQL even when Syncstorage uses Spanner. No usable default. +tokenserver.database_url = "mysql://sample_user:sample_password@localhost/tokenserver_rs" + +# Backend type reported in the token response (telemetry only). For the Spanner +# topology leave this as the default "spanner". Valid: "mysql"/"postgres"/"spanner". +tokenserver.node_type = "spanner" + +# Run the tokenserver DB migrations on startup. Default: false. +# tokenserver.run_migrations = true + +# FxA environment used to verify OAuth tokens. The defaults below point at the +# FxA *stage* environment; point them at production for a real deployment +# (fxa_email_domain = "api.accounts.firefox.com", +# fxa_oauth_server_url = "https://oauth.accounts.firefox.com"). +tokenserver.fxa_email_domain = "api-accounts.stage.mozaws.net" +tokenserver.fxa_oauth_server_url = "https://oauth.stage.mozaws.net" + +# REQUIRED for real deployments. Secret used to hash users' metrics UIDs so +# they stay anonymous. Default: "secret" (fine for local dev only). +tokenserver.fxa_metrics_hash_secret = "INSERT_SECRET_KEY_HERE" + +# ----------------------------------------------------------------------------- +# CORS (optional; sensible defaults applied when unset) +# ----------------------------------------------------------------------------- +# cors_allowed_origin = "localhost" # Default: "*" +# cors_max_age = 86400 # Default: 1728000 (20 days) diff --git a/config/local.example.toml b/config/local.example.toml index 6a19cf5c1e..2318a6d2bb 100644 --- a/config/local.example.toml +++ b/config/local.example.toml @@ -1,31 +1,95 @@ +# ============================================================================= +# Default out-of-the-box configuration: MySQL build +# ============================================================================= +# +# This is a ready-to-edit template for running syncstorage-rs against the +# standard MySQL build (the default `cargo build` / `make run_mysql` target, +# and the `syncstorage-rs-mysql` Docker image). It runs both Syncstorage and +# Tokenserver against MySQL. +# +# Copy it to `config/local.toml` and edit the values marked REQUIRED: +# +# cp config/local.example.toml config/local.toml +# +# Every setting can also be supplied as an environment variable prefixed with +# `SYNC_` (nested keys use `__`), e.g. `syncstorage.database_url` becomes +# `SYNC_SYNCSTORAGE__DATABASE_URL`. Environment variables take precedence over +# the config file. See the full reference at docs/src/config.md, and the +# annotated `Settings` structs in `*-settings/src/lib.rs`. +# +# For the Spanner build, see `config/local.example.spanner.toml`. +# ============================================================================= + +# REQUIRED. Secret used to derive the Hawk signing/token secrets. Use a long, +# random string in any real deployment. No default; the server cannot +# authenticate requests without it. master_secret = "INSERT_SECRET_KEY_HERE" -# removing this line will default to moz_json formatted logs (which is preferred for production envs) +# Host/port the server binds to. Defaults: host = "127.0.0.1", port = 8000. +# Use "0.0.0.0" inside containers so the port is reachable from the host. +host = "127.0.0.1" +port = 8000 + +# Emit human-readable logs instead of mozlog JSON. Default: false (0). +# Leave JSON logging on for production; it is the format our tooling expects. human_logs = 1 -# Example Syncstorage settings: -# database_url is required when syncstorage is enabled; there is no default, -# and the server fails to start if it is unset. -# Example MySQL DSN: +# ----------------------------------------------------------------------------- +# Syncstorage (BSO storage service) +# ----------------------------------------------------------------------------- + +# Enable the Syncstorage service. Default: true. +syncstorage.enabled = true + +# REQUIRED when syncstorage is enabled. MySQL DSN for the syncstorage database. +# There is no usable default: the server fails fast at startup if it is unset, +# rather than silently connecting to a default host. syncstorage.database_url = "mysql://sample_user:sample_password@localhost/syncstorage_rs" -# Example Spanner DSN: -# syncstorage.database_url="spanner://projects/SAMPLE_GCP_PROJECT/instances/SAMPLE_SPANNER_INSTANCE/databases/SAMPLE_SPANNER_DB" -# enable quota limits + +# Max size of the syncstorage DB connection pool. Default: 10. +# syncstorage.database_pool_max_size = 10 + +# Quota tracking/enforcement are Spanner-only features and are force-disabled +# for non-Spanner backends, so they have no effect on the MySQL build. syncstorage.enable_quota = 0 -# set the quota limit to 2GB. -# max_quota_limit = 200000000 -syncstorage.enabled = true + +# Request/batch limits. Defaults shown in docs/src/config.md (Syncstorage +# Limits). `max_total_records` is also used to cap MySQL `get_bsos` result +# sizes; see issues #298/#333. syncstorage.limits.max_total_records = 9984 -# Example Tokenserver settings: -# database_url is required when tokenserver is enabled; there is no default, -# and the server fails to start if it is unset. -tokenserver.database_url = "mysql://sample_user:sample_password@localhost/tokenserver_rs" +# ----------------------------------------------------------------------------- +# Tokenserver (node assignment + FxA OAuth verification) +# ----------------------------------------------------------------------------- + +# Enable Tokenserver. Default: false. Enable it for a self-contained server +# that both assigns nodes and stores data. tokenserver.enabled = true + +# REQUIRED when tokenserver is enabled. MySQL DSN for the tokenserver database. +# No usable default; the server fails fast at startup if it is unset. +tokenserver.database_url = "mysql://sample_user:sample_password@localhost/tokenserver_rs" + +# Backend type reported in the token response (telemetry only). For the MySQL +# build set this to "mysql". Default: "spanner". Valid: "mysql"/"postgres"/"spanner". +tokenserver.node_type = "mysql" + +# Run the tokenserver DB migrations on startup. Default: false. +# tokenserver.run_migrations = true + +# FxA environment used to verify OAuth tokens. The defaults below point at the +# FxA *stage* environment; point them at production for a real deployment +# (fxa_email_domain = "api.accounts.firefox.com", +# fxa_oauth_server_url = "https://oauth.accounts.firefox.com"). tokenserver.fxa_email_domain = "api-accounts.stage.mozaws.net" -tokenserver.fxa_metrics_hash_secret = "INSERT_SECRET_KEY_HERE" tokenserver.fxa_oauth_server_url = "https://oauth.stage.mozaws.net" -# cors settings -# cors_allowed_origin = "localhost" -# cors_max_age = 86400 +# REQUIRED for real deployments. Secret used to hash users' metrics UIDs so +# they stay anonymous. Default: "secret" (fine for local dev only). +tokenserver.fxa_metrics_hash_secret = "INSERT_SECRET_KEY_HERE" + +# ----------------------------------------------------------------------------- +# CORS (optional; sensible defaults applied when unset) +# ----------------------------------------------------------------------------- +# cors_allowed_origin = "localhost" # Default: "*" +# cors_max_age = 86400 # Default: 1728000 (20 days) diff --git a/docker/docker-compose.one-shot.mysql.yaml b/docker/docker-compose.one-shot.mysql.yaml new file mode 100644 index 0000000000..7a607d4949 --- /dev/null +++ b/docker/docker-compose.one-shot.mysql.yaml @@ -0,0 +1,112 @@ +# One-shot, stand-alone MySQL-backed Syncserver. +# +# Unlike docker-compose.mysql.yaml (which is an end-to-end test harness: +# mock FxA server, prebuilt `app:build` image), this brings up a working +# server in a single command with no manual database setup: +# +# make docker_oneshot_mysql +# # or: +# docker compose -f docker/docker-compose.one-shot.mysql.yaml up -d --build +# +# Then confirm it is serving: +# +# curl http://localhost:8000/__heartbeat__ +# +# Syncstorage applies its schema migrations at startup, +# SYNC_TOKENSERVER__RUN_MIGRATIONS applies the Tokenserver schema, and +# SYNC_TOKENSERVER__INIT_NODE_URL bootstraps the `sync-1.5` service and storage +# node records. +# +# This builds the MySQL server from the local checkout (build context is the +# repo root), so it does not depend on any published image. To use a published +# image instead, replace the syncserver `build:` block with: +# +# image: ghcr.io/mozilla-services/syncstorage-rs/syncstorage-rs-mysql:${SYNCSERVER_VERSION:?set SYNCSERVER_VERSION to a published tag} +# platform: linux/amd64 +# +# Published images are tagged by commit SHA (no `latest`/semver), so +# SYNCSERVER_VERSION must be a tag from the syncstorage-rs-mysql packages page. +# +# Set SYNC_MASTER_SECRET to your own value for anything beyond local +# experimentation; the default below is a placeholder. + +services: + syncserver: + build: + context: .. + args: + SYNCSTORAGE_DATABASE_BACKEND: mysql + TOKENSERVER_DATABASE_BACKEND: mysql + container_name: syncserver + ports: + - "${SYNC_PORT:-8000}:${SYNC_PORT:-8000}" + environment: + SYNC_HOST: "0.0.0.0" + SYNC_PORT: "${SYNC_PORT:-8000}" + SYNC_MASTER_SECRET: "${SYNC_MASTER_SECRET:-changeme_secret_key}" + SYNC_SYNCSTORAGE__DATABASE_URL: "mysql://sync:sync@sync-db:3306/syncstorage" + SYNC_TOKENSERVER__DATABASE_URL: "mysql://sync:sync@tokenserver-db:3306/tokenserver" + SYNC_TOKENSERVER__ENABLED: "true" + SYNC_TOKENSERVER__NODE_TYPE: "mysql" + SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" + SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: "api.accounts.firefox.com" + SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL: "https://oauth.accounts.firefox.com" + SYNC_HUMAN_LOGS: "${SYNC_HUMAN_LOGS:-false}" + RUST_LOG: "${RUST_LOG:-info}" + SYNC_TOKENSERVER__INIT_NODE_URL: "${SYNC_TOKENSERVER__INIT_NODE_URL:-http://localhost:${SYNC_PORT:-8000}}" + depends_on: + sync-db: + condition: service_healthy + tokenserver-db: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:${SYNC_PORT:-8000}/__heartbeat__"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + sync-db: + image: docker.io/library/mysql:8.0 + container_name: syncserver-sync-db + command: --explicit_defaults_for_timestamp + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "yes" + MYSQL_DATABASE: syncstorage + MYSQL_USER: sync + MYSQL_PASSWORD: sync + volumes: + - sync_db_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin -h 127.0.0.1 -usync -psync ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + + tokenserver-db: + image: docker.io/library/mysql:8.0 + container_name: syncserver-tokenserver-db + command: --explicit_defaults_for_timestamp + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "yes" + MYSQL_DATABASE: tokenserver + MYSQL_USER: sync + MYSQL_PASSWORD: sync + volumes: + - tokenserver_db_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin -h 127.0.0.1 -usync -psync ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + +volumes: + sync_db_data: + driver: local + tokenserver_db_data: + driver: local diff --git a/docker/docker-compose.one-shot.postgres.yaml b/docker/docker-compose.one-shot.postgres.yaml new file mode 100644 index 0000000000..9823360e65 --- /dev/null +++ b/docker/docker-compose.one-shot.postgres.yaml @@ -0,0 +1,85 @@ +# One-shot, stand-alone PostgreSQL-backed Syncserver. +# +# Unlike docker-compose.postgres.yaml (which is an end-to-end test harness: +# mock FxA server, prebuilt `app:build` image, separate sync/tokenserver DBs), +# this brings up a working server in a single command with no manual database +# setup: +# +# make docker_oneshot_postgres +# # or: +# docker compose -f docker/docker-compose.one-shot.postgres.yaml up -d --build +# +# Then confirm it is serving: +# +# curl http://localhost:8000/__heartbeat__ +# +# Syncstorage and Tokenserver share a single Postgres database here for +# simplicity; SYNC_TOKENSERVER__RUN_MIGRATIONS applies the Tokenserver schema +# and SYNC_TOKENSERVER__INIT_NODE_URL bootstraps the `sync-1.5` service and +# storage node records. +# +# This builds the Postgres server from the local checkout (build context is the +# repo root), so it does not depend on any published image. To use a published +# image instead, replace the syncserver `build:` block with: +# +# image: ghcr.io/mozilla-services/syncstorage-rs/syncserver-postgres:${SYNCSERVER_VERSION:-latest} +# platform: linux/amd64 +# +# Set SYNC_MASTER_SECRET to your own value for anything beyond local +# experimentation; the default below is a placeholder. + +services: + syncserver: + build: + context: .. + args: + SYNCSTORAGE_DATABASE_BACKEND: postgres + TOKENSERVER_DATABASE_BACKEND: postgres + container_name: syncserver + ports: + - "${SYNC_PORT:-8000}:${SYNC_PORT:-8000}" + environment: + SYNC_HOST: "0.0.0.0" + SYNC_PORT: "${SYNC_PORT:-8000}" + SYNC_MASTER_SECRET: "${SYNC_MASTER_SECRET:-changeme_secret_key}" + SYNC_SYNCSTORAGE__DATABASE_URL: "postgres://sync:sync@postgres:5432/syncserver" + SYNC_TOKENSERVER__DATABASE_URL: "postgres://sync:sync@postgres:5432/syncserver" + SYNC_TOKENSERVER__ENABLED: "true" + SYNC_TOKENSERVER__NODE_TYPE: "postgres" + SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" + SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: "api.accounts.firefox.com" + SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL: "https://oauth.accounts.firefox.com" + SYNC_HUMAN_LOGS: "${SYNC_HUMAN_LOGS:-false}" + RUST_LOG: "${RUST_LOG:-info}" + SYNC_TOKENSERVER__INIT_NODE_URL: "${SYNC_TOKENSERVER__INIT_NODE_URL:-http://localhost:${SYNC_PORT:-8000}}" + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:${SYNC_PORT:-8000}/__heartbeat__"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + postgres: + image: docker.io/library/postgres:18 + container_name: syncserver-postgres + environment: + POSTGRES_USER: sync + POSTGRES_PASSWORD: sync + POSTGRES_DB: syncserver + volumes: + - postgres_data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U sync -d syncserver"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + +volumes: + postgres_data: + driver: local diff --git a/docker/docker-compose.one-shot.spanner.yaml b/docker/docker-compose.one-shot.spanner.yaml new file mode 100644 index 0000000000..f31ad2b0e5 --- /dev/null +++ b/docker/docker-compose.one-shot.spanner.yaml @@ -0,0 +1,113 @@ +# One-shot, stand-alone Spanner-backed Syncserver (LOCAL DEV ONLY). +# +# This mirrors the production shape — Spanner for Syncstorage, MySQL for +# Tokenserver — but runs against the Cloud Spanner *emulator*, so it needs no +# GCP service account. It is for local experimentation only: the emulator is +# in-memory-ish, single-node, unauthenticated, and not suitable for any real +# data. Production uses real Spanner with a service-account key (see +# `make run_spanner`). +# +# Brings up the stack in a single command with no manual database setup: +# +# make docker_oneshot_spanner +# # or: +# docker compose -f docker/docker-compose.one-shot.spanner.yaml up -d --build +# +# Then confirm it is serving: +# +# curl http://localhost:8000/__heartbeat__ +# +# The sync-db-setup container provisions the emulator instance/database from the +# image's bundled schema (scripts/prepare-spanner.sh + /app/schema.ddl) and then +# exits; syncserver waits for it to finish. Tokenserver applies its schema via +# SYNC_TOKENSERVER__RUN_MIGRATIONS, and SYNC_TOKENSERVER__INIT_NODE_URL +# bootstraps the `sync-1.5` service and storage node records. +# +# The Spanner server is built from the local checkout (build context is the repo +# root); sync-db-setup builds the image and syncserver reuses it. +# +# Set SYNC_MASTER_SECRET to your own value for anything beyond local +# experimentation; the default below is a placeholder. + +services: + sync-db: + # Keep in sync with SPANNER_EMULATOR_VER in .github/workflows/main-workflow.yml + image: gcr.io/cloud-spanner-emulator/emulator:1.5.52 + container_name: syncserver-spanner-emulator + ports: + - "9010:9010" + - "9020:9020" + restart: unless-stopped + + sync-db-setup: + build: + context: .. + args: + SYNCSTORAGE_DATABASE_BACKEND: spanner + TOKENSERVER_DATABASE_BACKEND: mysql + image: syncstorage-rs-spanner-oneshot:latest + container_name: syncserver-spanner-setup + depends_on: + - sync-db + restart: "no" + entrypoint: ["/app/scripts/prepare-spanner.sh"] + environment: + # prepare-spanner.sh talks to the emulator's REST admin port (9020). + SYNC_SYNCSTORAGE__SPANNER_EMULATOR_HOST: sync-db:9020 + + tokenserver-db: + image: docker.io/library/mysql:8.0 + container_name: syncserver-tokenserver-db + command: --explicit_defaults_for_timestamp + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "yes" + MYSQL_DATABASE: tokenserver + MYSQL_USER: sync + MYSQL_PASSWORD: sync + volumes: + - tokenserver_db_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin -h 127.0.0.1 -usync -psync ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + + syncserver: + image: syncstorage-rs-spanner-oneshot:latest + container_name: syncserver + ports: + - "${SYNC_PORT:-8000}:${SYNC_PORT:-8000}" + environment: + SYNC_HOST: "0.0.0.0" + SYNC_PORT: "${SYNC_PORT:-8000}" + SYNC_MASTER_SECRET: "${SYNC_MASTER_SECRET:-changeme_secret_key}" + SYNC_SYNCSTORAGE__DATABASE_URL: "spanner://projects/test-project/instances/test-instance/databases/test-database" + # syncserver uses the emulator's gRPC port (9010). + SYNC_SYNCSTORAGE__SPANNER_EMULATOR_HOST: sync-db:9010 + SYNC_TOKENSERVER__DATABASE_URL: "mysql://sync:sync@tokenserver-db:3306/tokenserver" + SYNC_TOKENSERVER__ENABLED: "true" + SYNC_TOKENSERVER__NODE_TYPE: "spanner" + SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" + SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: "api.accounts.firefox.com" + SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL: "https://oauth.accounts.firefox.com" + SYNC_HUMAN_LOGS: "${SYNC_HUMAN_LOGS:-false}" + RUST_LOG: "${RUST_LOG:-info}" + SYNC_TOKENSERVER__INIT_NODE_URL: "${SYNC_TOKENSERVER__INIT_NODE_URL:-http://localhost:${SYNC_PORT:-8000}}" + depends_on: + sync-db-setup: + condition: service_completed_successfully + tokenserver-db: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:${SYNC_PORT:-8000}/__heartbeat__"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + +volumes: + tokenserver_db_data: + driver: local diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index a0e830afdd..777f9259db 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -37,6 +37,7 @@ [Mozilla Accounts Server - FxA](mozilla-accounts.md) - [How To Guides](how-to/index.md) + - [Default Configuration for Spanner & MySQL Builds](how-to/default-config.md) - [Run Your Own Sync-1.5 Server with Docker](how-to/how-to-run-with-docker.md) - [Run Your Own Sync-1.5 Server (legacy)](how-to/how-to-run-sync-server.md) - [Configure Sync Server for TLS (legacy)](how-to/how-to-config-tls.md) diff --git a/docs/src/how-to/default-config.md b/docs/src/how-to/default-config.md new file mode 100644 index 0000000000..4d5dde7392 --- /dev/null +++ b/docs/src/how-to/default-config.md @@ -0,0 +1,68 @@ +# Default Configuration for Spanner & MySQL Builds + +This page describes the out-of-the-box configuration needed to run the two +standard `syncstorage-rs` builds: + +- **MySQL build** — the default `cargo build` / `make run_mysql` target and the + `syncstorage-rs-mysql` Docker image. Both Syncstorage and Tokenserver run + against MySQL. +- **Spanner build** — `make run_spanner` and the `syncstorage-rs-spanner` + Docker image. This mirrors production: Syncstorage runs against Google Cloud + Spanner while Tokenserver runs against MySQL. + +Annotated, copy-paste-ready templates live in the repo: + +| Build | Template | +| --- | --- | +| MySQL | [`config/local.example.toml`](https://github.com/mozilla-services/syncstorage-rs/blob/master/config/local.example.toml) | +| Spanner | [`config/local.example.spanner.toml`](https://github.com/mozilla-services/syncstorage-rs/blob/master/config/local.example.spanner.toml) | + +Copy one to `config/local.toml` and edit the values marked `REQUIRED`: + +```sh +cp config/local.example.toml config/local.toml # MySQL +# or +cp config/local.example.spanner.toml config/local.toml # Spanner +``` + +Every setting can also be supplied as an environment variable prefixed with +`SYNC_` (nested keys use `__`). For example `syncstorage.database_url` becomes +`SYNC_SYNCSTORAGE__DATABASE_URL`. Environment variables take precedence over the +config file. The complete list of options and their defaults is in the +[Application Configuration](../config.md) reference, and the source of truth is +the doc-commented `Settings` structs in the `*-settings` crates. + +## Minimum required settings + +Most settings have sensible defaults. Regardless of backend you must supply: + +| Setting | Why | +| --- | --- | +| [`master_secret`](../config.md#SYNC_MASTER_SECRET) | Derives the Hawk signing/token secrets. No default. | +| [`syncstorage.database_url`](../config.md#SYNC_SYNCSTORAGE__DATABASE_URL) | No usable default; the server fails fast at startup if unset. | +| [`tokenserver.database_url`](../config.md#SYNC_TOKENSERVER__DATABASE_URL) | Required when `tokenserver.enabled = true` (fails fast if unset). | + +Backend-specific notes: + +- **MySQL:** set `tokenserver.node_type = "mysql"`. Syncstorage MySQL schema + migrations run automatically at startup; set + `tokenserver.run_migrations = true` to apply the Tokenserver schema too. +- **Spanner:** `syncstorage.database_url` must use the + `spanner://projects/.../instances/.../databases/...` form — the `spanner://` + scheme is what selects the Spanner backend. Leave `tokenserver.node_type` at + its default (`"spanner"`). To run against the local emulator instead of GCP, + set `syncstorage.spanner_emulator_host` (e.g. `localhost:9010`); for real + Spanner, point `GOOGLE_APPLICATION_CREDENTIALS` at a service-account key. + +## Run it out of the box with Docker + +For a zero-to-running MySQL stack (database + server, schema applied +automatically, ready to serve), see the +[one-shot MySQL `docker compose`](how-to-run-with-docker.md#docker-compose-one-shot-with-mysql) +recipe. It brings up MySQL and the server, runs migrations, and bootstraps the +storage node so that `curl http://localhost:8000/__heartbeat__` succeeds with no +further setup. + +A Spanner stack cannot be fully zero-config: real Spanner requires GCP +credentials, and the local emulator requires extra wiring (see +`docker/docker-compose.spanner.yaml` and `make run_spanner`). diff --git a/docs/src/how-to/how-to-run-with-docker.md b/docs/src/how-to/how-to-run-with-docker.md index b335189b7f..a3544dd090 100644 --- a/docs/src/how-to/how-to-run-with-docker.md +++ b/docs/src/how-to/how-to-run-with-docker.md @@ -10,7 +10,7 @@ Images are available for both and [PostgreSQL](https://github.com/mozilla-services/syncstorage-rs/pkgs/container/syncstorage-rs%2Fsyncstorage-rs-postgres) as the database. Differences in configuration or deployment steps will be -noted. +noted. Tagged release builds are available on ghcr.io. To pin to a specific version, set `SYNCSERVER_VERSION` to the desired release tag (e.g., `SYNCSERVER_VERSION=v1.45.0`) @@ -37,10 +37,10 @@ services: platform: linux/amd64 container_name: syncserver ports: - - "8000:8000" + - "${SYNC_PORT:-8000}:${SYNC_PORT:-8000}" environment: SYNC_HOST: "0.0.0.0" - SYNC_PORT: "8000" + SYNC_PORT: "${SYNC_PORT:-8000}" SYNC_MASTER_SECRET: "${SYNC_MASTER_SECRET}" SYNC_SYNCSTORAGE__DATABASE_URL: "${SYNC_SYNCSTORAGE__DATABASE_URL}" SYNC_TOKENSERVER__DATABASE_URL: "${SYNC_TOKENSERVER__DATABASE_URL}" @@ -48,10 +48,10 @@ services: SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: "api.accounts.firefox.com" SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL: "https://oauth.accounts.firefox.com" - SYNC_TOKENSERVER__INIT_NODE_URL: "${SYNC_TOKENSERVER__INIT_NODE_URL:-http://localhost:${SYNC_PORT}}" + SYNC_TOKENSERVER__INIT_NODE_URL: "${SYNC_TOKENSERVER__INIT_NODE_URL:-http://localhost:${SYNC_PORT:-8000}}" restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:${SYNC_PORT}/__heartbeat__"] + test: ["CMD", "curl", "-f", "http://localhost:${SYNC_PORT:-8000}/__heartbeat__"] interval: 30s timeout: 10s retries: 3 @@ -76,70 +76,77 @@ SYNC_TOKENSERVER__INIT_NODE_URL="http://localhost:8000" \ docker compose -f docker-compose.yaml up -d ``` -## Docker Compose, One-Shot with PostgreSQL +## Docker Compose, One-Shot Stand-Alone Servers -Alternatively, the database can be started through `docker compose` as well. The real service URL can be set with the `INIT_NODE_URL` environment variable. +The repository ships ready-to-run, stand-alone compose files under `docker/` +that bring up a complete server — database(s) included — in a single command, +with no manual database setup. Each builds the server from your local checkout, +so they work directly from a clone without a published image. From the repo +root: -Save the yaml below into a file, e.g. `docker-compose.one-shot.yaml`. +| Backend | Make target | Compose file | +|---|---|---| +| MySQL | `make docker_oneshot_mysql` | `docker/docker-compose.one-shot.mysql.yaml` | +| PostgreSQL | `make docker_oneshot_postgres` | `docker/docker-compose.one-shot.postgres.yaml` | +| Spanner (emulator, local dev only) | `make docker_oneshot_spanner` | `docker/docker-compose.one-shot.spanner.yaml` | + +For example, for MySQL: + +```sh +make docker_oneshot_mysql +# equivalently: +docker compose -f docker/docker-compose.one-shot.mysql.yaml up -d --build +``` + +Once the `syncserver` container reports healthy, confirm it is serving: + +```sh +curl http://localhost:8000/__heartbeat__ +``` + +Stop and remove a stack with the matching `_stop` target, e.g. +`make docker_oneshot_mysql_stop`. + +Syncstorage applies its schema migrations at startup, +`SYNC_TOKENSERVER__RUN_MIGRATIONS` applies the Tokenserver schema, and +`SYNC_TOKENSERVER__INIT_NODE_URL` bootstraps the `sync-1.5` service and storage +node records — so the stack is ready to serve immediately. + +> Set `SYNC_MASTER_SECRET` to your own value for anything beyond local +> experimentation; the compose files default to a placeholder. + +### Backend notes + +- **MySQL / PostgreSQL** are reasonable starting points for a real self-hosted + deployment. The MySQL recipe uses a separate database for Syncstorage and + Tokenserver; the PostgreSQL recipe shares a single database between them for + simplicity. +- **Spanner** runs against the Cloud Spanner *emulator* and is for local + experimentation only — it is unauthenticated, single-node, and not durable. + Production Spanner uses a real instance and a service-account key (see + `make run_spanner`). The recipe mirrors the production split (Spanner for + Syncstorage, MySQL for Tokenserver) and includes a one-time setup container + that provisions the emulator's schema before the server starts. + +### Using a published image instead of building + +Mozilla also publishes prebuilt images on ghcr.io. The MySQL images are +currently tagged by commit SHA — there is **no `latest` or semver tag** — so you +must pin `SYNCSERVER_VERSION` to a tag listed on the +[`syncstorage-rs-mysql` packages page](https://github.com/mozilla-services/syncstorage-rs/pkgs/container/syncstorage-rs%2Fsyncstorage-rs-mysql). +To use one, replace the `syncserver` service's `build:` block in the compose +file with an `image:` reference (published images are `linux/amd64`): ```yaml services: syncserver: - image: ghcr.io/mozilla-services/syncstorage-rs/syncserver-postgres:${SYNCSERVER_VERSION:-latest} + image: ghcr.io/mozilla-services/syncstorage-rs/syncstorage-rs-mysql:${SYNCSERVER_VERSION:?set SYNCSERVER_VERSION to a published tag} platform: linux/amd64 - container_name: syncserver - ports: - - "8000:8000" - environment: - SYNC_HOST: "0.0.0.0" - SYNC_PORT: "8000" - SYNC_MASTER_SECRET: "${SYNC_MASTER_SECRET:-changeme_secret_key}" - SYNC_SYNCSTORAGE__DATABASE_URL: "postgres://sync:sync@postgres:5432/syncserver" - SYNC_TOKENSERVER__DATABASE_URL: "postgres://sync:sync@postgres:5432/syncserver" - SYNC_TOKENSERVER__ENABLED: "true" - SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" - SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: "api.accounts.firefox.com" - SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL: "https://oauth.accounts.firefox.com" - SYNC_HUMAN_LOGS: "${SYNC_HUMAN_LOGS:-false}" - RUST_LOG: "${RUST_LOG:-info}" - SYNC_TOKENSERVER__INIT_NODE_URL: "${SYNC_TOKENSERVER__INIT_NODE_URL:-http://localhost:${SYNC_PORT}}" - depends_on: - postgres: - condition: service_healthy - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:${SYNC_PORT}/__heartbeat__"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 60s - - postgres: - image: postgres:18 - container_name: syncserver-postgres - environment: - POSTGRES_USER: sync - POSTGRES_PASSWORD: sync - POSTGRES_DB: syncserver - volumes: - - postgres_data:/var/lib/postgresql - healthcheck: - test: ["CMD-SHELL", "pg_isready -U sync -d syncserver"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - restart: unless-stopped - -volumes: - postgres_data: - driver: local + # ...the remaining syncserver settings are unchanged ``` -Next, start the service with `docker compose`: - ```sh -docker compose -f docker-compose.one-shot.yaml up -d +SYNCSERVER_VERSION= docker compose -f docker/docker-compose.one-shot.mysql.yaml up -d ``` ## Configuring Firefox (Desktop) diff --git a/docs/src/how-to/index.md b/docs/src/how-to/index.md index 07de25c9a8..c35186c16d 100644 --- a/docs/src/how-to/index.md +++ b/docs/src/how-to/index.md @@ -2,6 +2,7 @@ Collection of How To guides for various Sync-related operations. +- [Default Configuration for Spanner & MySQL Builds](default-config.md) - [Run Your Own Sync-1.5 Server with Docker](how-to-run-with-docker.md) - [Run Your Own Sync-1.5 Server (legacy)](how-to-run-sync-server.md) - [Configure Sync Server for TLS (legacy)](how-to-config-tls.md) diff --git a/syncserver-settings/src/lib.rs b/syncserver-settings/src/lib.rs index bbdf2ebd5c..1feb487c11 100644 --- a/syncserver-settings/src/lib.rs +++ b/syncserver-settings/src/lib.rs @@ -16,7 +16,10 @@ static PREFIX: &str = "sync"; #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub struct Settings { + /// TCP port the server binds to. Default: 8000. pub port: u16, + /// Host address the server binds to. Default: "127.0.0.1". Use "0.0.0.0" + /// inside containers so the port is reachable from the host. pub host: String, /// Keep-alive header value (seconds) pub actix_keep_alive: Option, @@ -25,9 +28,13 @@ pub struct Settings { /// that are used during Hawk authentication. pub master_secret: Secrets, + /// Emit human-readable logs instead of mozlog JSON. Default: false. + /// Production environments should leave this off (JSON is preferred). pub human_logs: bool, + /// Hostname of the StatsD/metrics sink. Default: "localhost". pub statsd_host: Option, + /// Port of the StatsD/metrics sink. Default: 8125. pub statsd_port: u16, /// Whether to include the hostname in metrics, which increases cardinality significantly in /// prod. diff --git a/syncstorage-settings/src/lib.rs b/syncstorage-settings/src/lib.rs index 76e2171add..10d7bcb7d5 100644 --- a/syncstorage-settings/src/lib.rs +++ b/syncstorage-settings/src/lib.rs @@ -75,6 +75,7 @@ pub struct Settings { /// fails fast at startup (see `syncserver_settings::Settings::validate`) /// rather than silently connecting to a default host. pub database_url: String, + /// Max size of the database connection pool. Default: 10. pub database_pool_max_size: u32, /// Pool timeout when waiting for a slot to become available, in seconds pub database_pool_connection_timeout: Option, @@ -92,15 +93,24 @@ pub struct Settings { /// Server-enforced limits for request payloads. pub limits: ServerLimits, + /// StatsD metrics label prefix for syncstorage. Default: "syncstorage". pub statsd_label: String, + /// Track per-user storage quota. Spanner-only; force-disabled on other + /// backends. Default: false. pub enable_quota: bool, + /// Reject writes that exceed `limits.max_quota_limit`. Requires + /// `enable_quota`. Spanner-only; force-disabled on other backends. + /// Default: false. pub enforce_quota: bool, /// Whether Glean telemetry metric emission is enabled. pub glean_enabled: bool, + /// gRPC address of a local Spanner emulator (e.g. "localhost:9010"). + /// Leave unset to use real Spanner. Default: None. pub spanner_emulator_host: Option, + /// Whether the Syncstorage service is enabled. Default: true. pub enabled: bool, /// Fail the `/__lbheartbeat__` healthcheck after running for this duration