Extending the Tooling
This guide covers how to add new services, tasks, and health check types to the federation tooling.
Project Structure
cred-local-workspace/
├── fed # Bash entrypoint (bootstraps venv, delegates to Python)
├── fed.toml # Single source of truth (services, tasks, ports, health)
├── .local-workspace.env # Per-developer path overrides (gitignored)
├── .local-workspace.env.example # Template for the above
├── federation/ # Pure Python package
│ ├── __init__.py # Package metadata
│ ├── __main__.py # Click CLI (start, stop, status, logs)
│ ├── config.py # TOML loading + data models (ServiceConfig, TaskConfig)
│ ├── env.py # Cross-service template resolution
│ ├── orchestrator.py # Startup/shutdown lifecycle + dependency ordering
│ ├── preflight.py # Pre-start validation (Docker, dirs, .env files)
│ ├── health.py # Health check functions
│ ├── compose.py # Docker Compose subprocess wrapper
│ ├── output.py # Rich-based styled console output
│ ├── logs.py # Background log capture to run-output/
│ ├── requirements.txt # Python deps (tomli, rich, click, python-dotenv)
│ └── repo-tools/ # Scripts bridging prod pipelines to local dev
│ └── bootstrap_credentity.py
└── run-output/ # Log capture directory (gitignored, created at startup)
Dependencies: tomli (TOML parsing for Python <3.11), rich (console output), click (CLI framework), python-dotenv (.env file parsing). No heavy frameworks.
Module Responsibilities
| Module | Role |
|---|---|
__main__.py |
CLI entry point. Defines start, stop, status, logs commands via Click |
config.py |
Loads fed.toml, resolves dir templates, produces ServiceConfig and TaskConfig dataclasses |
env.py |
Resolves runtime templates (${env:...}, ${port:...}, ${dir:...}) against live service state |
orchestrator.py |
Topological sort of services, startup/shutdown lifecycle, task execution |
preflight.py |
Validates Docker, directories, .env files, required keys, compose overlays before startup |
health.py |
Pure functions for each health check type, returns (healthy: bool, detail: str) |
compose.py |
Wraps docker compose subprocess calls with environment injection |
output.py |
Rich-based console formatting (status tables, progress, colors) |
logs.py |
Captures service logs to run-output/ during startup |
Adding a New Service
Step 1: Define the Service in fed.toml
Add a [services.<name>] block:
[services.my-service]
label = "My Service"
dir = "${MY_SERVICE_DIR:cred-my-service}"
port = 9000
optional = true
depends_on = ["commercial-api"]
health_type = "http"
health_path = "/health"
compose_up = "docker compose up -d"
compose_down = "docker compose down"
required_env_keys = ["API_KEY"]
Step 2: Configure Environment Injection
If your service needs secrets from other services, add an env_inject block:
[services.my-service.env_inject]
JWT_SECRET = "${env:commercial-api:JWT_SECRET}"
COMMERCIAL_API_URL = "http://host.docker.internal:${port:commercial-api}"
Step 3: Document Configurable Paths
If the directory should be configurable for different workspace layouts, add the variable to .local-workspace.env.example:
# My Service directory (default: cred-my-service)
MY_SERVICE_DIR=cred-my-service
Step 4: Wire Up Dependencies
Add your service to another service's depends_on list if ordering matters:
[services.some-downstream-service]
depends_on = ["my-service", "commercial-api"]
Step 5: Test
Test the new service in isolation (includes transitive dependencies):
./fed start --only my-service
Adding a New Task
Tasks are one-shot commands that run at specific lifecycle points.
Step 1: Define the Task in fed.toml
[tasks.my-task]
label = "My Task"
dir = "${COMMERCIAL_API_DIR:cred-api-commercial}"
command = "docker compose exec -T web yarn my-script"
Step 2: Add Idempotency (Optional but Recommended)
If the task is expensive or should not re-run unnecessarily, add a skip_check:
[tasks.my-task]
label = "My Task"
dir = "${COMMERCIAL_API_DIR:cred-api-commercial}"
command = "docker compose exec -T web yarn my-script"
skip_check = "docker compose exec -T db psql -U cred -d mydb -tAc \"SELECT 1 FROM my_table LIMIT 1\" 2>/dev/null | grep -q 1"
The skip_check is a shell command. Exit code 0 means "already done" and the task is skipped.
Tip
The skip_check command runs in a controlled environment with the same env vars as the task itself. It uses a subprocess with filtered environment variables to avoid leaking host env into Docker containers.
Step 3: Reference the Task in a Service
Add the task name to a service's pre_tasks or post_tasks list:
[services.commercial-api]
pre_tasks = ["model-codegen"]
post_tasks = ["wait-for-db", "fdw-bootstrap", "db-init", "my-task"]
Step 4: Complex Scripts
For tasks with complex logic, write a script in federation/repo-tools/ and reference it via the $FEDERATION_WORKSPACE variable (set by the fed entrypoint):
[tasks.my-complex-task]
label = "My Complex Task"
dir = "${COMMERCIAL_API_DIR:cred-api-commercial}"
command = "python3 $FEDERATION_WORKSPACE/federation/repo-tools/my-script.py ."
Adding a New Health Check Type
Health checks are pure functions in federation/health.py that return a (healthy: bool, detail: str) tuple.
Step 1: Write the Check Function
Add a function to health.py:
def check_mytype(host: str, port: int, path: str, timeout: float) -> tuple[bool, str]:
"""Check health using my custom method."""
try:
# Your check logic here
return True, "Healthy"
except Exception as e:
return False, str(e)
Step 2: Register the Type
Add an elif branch in the run_health_check() dispatcher function in health.py:
elif health_type == "mytype":
return check_mytype(host, port, path, timeout)
Step 3: Use It in fed.toml
[services.my-service]
health_type = "mytype"
health_path = "/custom-endpoint"
The Credentity Bootstrap Story
This is the most complex piece of federation tooling. Understanding it requires context from four repos.
The Production Pipeline
In production, credentity.DataDescription is built by a BigQuery dbt pipeline (cred-commercial-dbt) that merges:
- Model-api entities (Person, Company, etc.) from
cred-model - Commercial entities (Contact, Account, etc.) with decorator columns like
isImportable,fieldType,isReadonlyfromSchemaInfo(generated bygenerate-metadata-table.tsincred-api-commercial)
The Local Gap
Locally, the Foreign Data Wrapper (FDW) only mirrors the model-api's raw DataDescription table. It lacks commercial entities and their metadata columns.
How the Bootstrap Bridges the Gap
federation/repo-tools/bootstrap_credentity.py replicates the production pipeline locally:
- Runs
generate-metadata-table.tsto create theSchemaInfotable - Materializes the FDW foreign table into a local table
- Inserts commercial entity rows with proper ID generation (matching the dbt formulas)
- Adds metadata columns (
isImportable,fieldType,isReadonly)
Without this bootstrap, features that depend on DataDescription having commercial entities (such as contact import) will fail.
Idempotency
The bootstrap checks for the isImportable column in credentity.DataDescription. If the column exists, the task has already run and is skipped.
Known Gaps
- The
isImportabledetection usesfieldType IS NOT NULLas a proxy for decorated fields - A bootstrap status marker (explicit marker table replacing column-sniffing) is planned but not yet implemented
Environment Variable Flow
Understanding how environment variables move through the system:
.local-workspace.env <-- Dir overrides (COMMERCIAL_API_DIR, etc.)
| (config load time)
v
fed.toml dir fields <-- ${VAR:default} -> resolved relative paths
|
v
service .env files <-- Per-repo secrets (JWT_SECRET, DB URLs, etc.)
| (startup time)
v
env_inject templates <-- ${env:svc:VAR}, ${port:svc} -> resolved values
|
v
docker compose subprocess <-- _build_system_env() + env_inject -> controlled env
Key design decision: The compose.py module builds a filtered environment for each subprocess. It does not pass the full host environment to Docker Compose -- only the variables needed by each service. This prevents accidental env var leakage between services.