Skip to content

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"

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, isReadonly from SchemaInfo (generated by generate-metadata-table.ts in cred-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:

  1. Runs generate-metadata-table.ts to create the SchemaInfo table
  2. Materializes the FDW foreign table into a local table
  3. Inserts commercial entity rows with proper ID generation (matching the dbt formulas)
  4. 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 isImportable detection uses fieldType IS NOT NULL as 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.