- Repo scaffolding: .gitignore, .env.example, Makefile, docker-compose.yml, README.md, CHANGELOG.md, pre-commit config. - Three-service stack: api (Flask 3), db (postgres:16-alpine), front (nginx serving the Vite bundle). Named volumes metamorph_db + metamorph_evidence. - Backend skeleton: Flask app factory, JSON structured logging on stdout, GET /api/v1/health, multi-stage Dockerfile, pyproject.toml driven by uv, Pydantic Settings with secret guard rails (refuses to boot in non-dev with placeholders), APP_ENV gating. - Frontend skeleton: Vite + React 18 + TypeScript strict + TailwindCSS, RTOps design tokens from tasks/design.md, self-hosted JetBrains Mono / IBM Plex Sans via @fontsource, base UI primitives (Card/Tag/SectionHeader/FlowNode/ Button), home page wired to /api/v1/health. - Engine-agnostic Makefile: auto-detects docker or podman, picks the matching compose driver. Targets: up/down/build/rebuild/dev/lint/fmt/test/migrate/ seed-mitre/print-install-token/e2e/inspect-health. - Playwright suite: e2e/tests/m0-smoke.spec.ts (8 tests) + HTML + JUnit reports + traces on retry. - Docs: tasks/spec.md (finalized after Q&A), tasks/design.md, tasks/todo.md (14 milestones), tasks/testing-m0.md, tasks/lessons.md. DoD: make up + make health + make e2e all pass on podman 5.x (Fedora) and docker. TLS terminated by external reverse proxy (spec §6 NF-network). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
215 lines
7.5 KiB
Makefile
215 lines
7.5 KiB
Makefile
.DEFAULT_GOAL := help
|
|
SHELL := /bin/bash
|
|
|
|
# Load .env if present so targets can use the same variables as compose.
|
|
ifneq (,$(wildcard ./.env))
|
|
include .env
|
|
export
|
|
endif
|
|
|
|
# === Container engine detection (docker OR podman) ============================
|
|
#
|
|
# Auto-detects on PATH, with docker preferred when both are installed.
|
|
# Override either variable from the environment or the command line:
|
|
# make up ENGINE=podman
|
|
# make up COMPOSE="podman compose"
|
|
#
|
|
ENGINE ?= $(shell \
|
|
if command -v docker >/dev/null 2>&1; then echo docker; \
|
|
elif command -v podman >/dev/null 2>&1; then echo podman; \
|
|
fi)
|
|
|
|
ifeq ($(strip $(ENGINE)),)
|
|
$(error Neither docker nor podman found in PATH. Install one, or set ENGINE=...)
|
|
endif
|
|
|
|
# Pick the right compose driver based on the chosen engine.
|
|
# - docker → "docker compose" (compose v2 plugin)
|
|
# - podman 4.0+ → "podman compose"
|
|
# - older podman → "podman-compose" (legacy Python wrapper)
|
|
ifndef COMPOSE
|
|
ifeq ($(ENGINE),docker)
|
|
COMPOSE := docker compose
|
|
else
|
|
COMPOSE := $(shell \
|
|
if podman compose version >/dev/null 2>&1; then echo "podman compose"; \
|
|
elif command -v podman-compose >/dev/null 2>&1; then echo "podman-compose"; \
|
|
else echo "podman compose"; fi)
|
|
endif
|
|
endif
|
|
|
|
# Project name is mostly used to look up volumes / containers via raw engine calls.
|
|
PROJECT ?= metamorph
|
|
|
|
# Suppress the noisy `>>>> Executing external compose provider …` banner that
|
|
# `podman compose` emits on every invocation (harmless, but spammy in logs).
|
|
export PODMAN_COMPOSE_WARNING_LOGS = false
|
|
|
|
.PHONY: help env engine up down build rebuild logs logs-api ps health shell-api shell-db psql \
|
|
dev dev-api dev-front lint lint-api lint-front fmt test test-api test-front \
|
|
e2e e2e-install e2e-report e2e-up wait-healthy \
|
|
migrate migrate-down migrate-revision migrate-status \
|
|
seed-mitre print-install-token print-install-token-force \
|
|
volumes inspect-health clean
|
|
|
|
help: ## Show this help
|
|
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_0-9-]+:.*##/ {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
|
@printf "\n Container engine in use: \033[33m%s\033[0m | compose: \033[33m%s\033[0m\n" "$(ENGINE)" "$(COMPOSE)"
|
|
|
|
engine: ## Print the detected container engine and compose driver
|
|
@echo "ENGINE=$(ENGINE)"
|
|
@echo "COMPOSE=$(COMPOSE)"
|
|
|
|
env: ## Bootstrap a local .env from .env.example if missing
|
|
@test -f .env || (cp .env.example .env && echo "Created .env — edit it before 'make up'")
|
|
|
|
# === Compose lifecycle ========================================================
|
|
|
|
up: env ## Build (if needed) and start all services
|
|
$(COMPOSE) up -d --build
|
|
|
|
down: ## Stop and remove containers (keep volumes)
|
|
$(COMPOSE) down
|
|
|
|
build: ## Build images without starting
|
|
$(COMPOSE) build
|
|
|
|
rebuild: ## Force rebuild without cache
|
|
$(COMPOSE) build --no-cache
|
|
|
|
logs: ## Tail logs from all services
|
|
$(COMPOSE) logs -f --tail=200
|
|
|
|
logs-api: ## Tail only the api container logs (useful to inspect JSON log lines)
|
|
$(COMPOSE) logs -f --tail=200 api
|
|
|
|
ps: ## List running services
|
|
$(COMPOSE) ps
|
|
|
|
shell-api: ## Shell into the api container
|
|
$(COMPOSE) exec api bash
|
|
|
|
shell-db: ## Shell into the db container
|
|
$(COMPOSE) exec db sh
|
|
|
|
psql: ## Open psql in the db container
|
|
$(COMPOSE) exec db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB)
|
|
|
|
# === Container introspection (engine-agnostic) ================================
|
|
|
|
volumes: ## List the named volumes created by this project
|
|
@$(ENGINE) volume ls --filter "name=$(PROJECT)_"
|
|
|
|
inspect-health: ## Print the health status of every container in the project
|
|
@for c in $(PROJECT)-db $(PROJECT)-api $(PROJECT)-front; do \
|
|
printf "%-30s " "$$c"; \
|
|
$(ENGINE) inspect --format '{{.State.Health.Status}}' "$$c" 2>/dev/null || echo "(no-healthcheck or stopped)"; \
|
|
done
|
|
|
|
# === Local dev (no container) =================================================
|
|
|
|
dev: ## Run api + front locally in parallel (Ctrl-C stops both)
|
|
@$(MAKE) -j2 --no-print-directory dev-api dev-front
|
|
|
|
dev-api: ## Run Flask in dev mode
|
|
cd backend && APP_ENV=dev uv run flask --app app.main run --debug --host 0.0.0.0 --port 8000
|
|
|
|
dev-front: ## Run Vite dev server
|
|
cd frontend && npm run dev
|
|
|
|
# === Quality ==================================================================
|
|
|
|
lint: lint-api lint-front ## Lint everything
|
|
|
|
lint-api:
|
|
cd backend && uv run ruff check . && uv run ruff format --check .
|
|
|
|
lint-front:
|
|
cd frontend && npm run lint && npm run typecheck
|
|
|
|
fmt: ## Auto-format
|
|
cd backend && uv run ruff format .
|
|
cd frontend && npm run format
|
|
|
|
test: test-api test-front ## Run all tests
|
|
|
|
test-api: ## Run backend pytest in an ephemeral container against the live DB
|
|
@echo "Building backend test image (target: test)…"
|
|
@$(ENGINE) build -q --target test -t metamorph-api-test ./backend > /dev/null
|
|
@echo "Running pytest…"
|
|
$(ENGINE) run --rm \
|
|
--network $(PROJECT)_metamorph \
|
|
-e APP_ENV=test \
|
|
-e POSTGRES_DB=$(POSTGRES_DB) \
|
|
-e POSTGRES_USER=$(POSTGRES_USER) \
|
|
-e POSTGRES_PASSWORD=$(POSTGRES_PASSWORD) \
|
|
-e POSTGRES_HOST=db \
|
|
-e POSTGRES_PORT=5432 \
|
|
-e JWT_SECRET=test-only-secret-not-checked-in-this-mode \
|
|
-e LOG_LEVEL=WARNING \
|
|
-e FRONT_ORIGIN=http://localhost:8080 \
|
|
metamorph-api-test
|
|
|
|
test-front:
|
|
cd frontend && npm test --if-present
|
|
|
|
# === End-to-end tests (Playwright) ============================================
|
|
|
|
e2e-install: ## Install Playwright deps + chromium browser (use sudo on Debian/Ubuntu)
|
|
cd e2e && npm install && npx playwright install --with-deps chromium
|
|
|
|
e2e: wait-healthy ## Run the e2e suite against the running stack
|
|
cd e2e && BASE_URL=http://localhost:$(or $(HOST_FRONT_PORT),8080) npm test
|
|
|
|
e2e-report: ## Open the latest Playwright HTML report
|
|
cd e2e && npx playwright show-report
|
|
|
|
e2e-up: ## Bring the stack up, wait healthy, then run e2e
|
|
$(MAKE) up
|
|
$(MAKE) e2e
|
|
|
|
health: ## Curl the health endpoint via the front nginx (one shot)
|
|
@curl -sSf "http://localhost:$(or $(HOST_FRONT_PORT),8080)/api/v1/health" \
|
|
&& echo "" \
|
|
|| (echo " unreachable — is 'make up' done?"; exit 1)
|
|
|
|
wait-healthy: ## Wait until the front+api are reachable (60s timeout)
|
|
@port=$(or $(HOST_FRONT_PORT),8080); \
|
|
echo "Waiting for http://localhost:$$port/api/v1/health …"; \
|
|
for i in $$(seq 1 30); do \
|
|
if curl -sf "http://localhost:$$port/api/v1/health" > /dev/null; then \
|
|
echo " ready after $$((i*2))s"; exit 0; \
|
|
fi; \
|
|
sleep 2; \
|
|
done; \
|
|
echo " timeout after 60s — check 'make ps' and 'make logs'"; exit 1
|
|
|
|
# === App-specific commands (placeholders for later milestones) ================
|
|
|
|
migrate: ## Apply DB migrations (alembic upgrade head, runs inside the api container)
|
|
$(COMPOSE) exec api alembic upgrade head
|
|
|
|
migrate-down: ## Roll back the latest migration
|
|
$(COMPOSE) exec api alembic downgrade -1
|
|
|
|
migrate-revision: ## Generate a new autogenerated migration: make migrate-revision MSG="my message"
|
|
@test -n "$(MSG)" || (echo "Usage: make migrate-revision MSG=\"short description\""; exit 1)
|
|
$(COMPOSE) exec api alembic revision --autogenerate -m "$(MSG)"
|
|
|
|
migrate-status: ## Show current revision and any pending migrations
|
|
$(COMPOSE) exec api alembic current
|
|
@echo "---"
|
|
$(COMPOSE) exec api alembic heads
|
|
|
|
seed-mitre: ## Seed MITRE ATT&CK Enterprise dataset (M4)
|
|
$(COMPOSE) exec api flask --app app.cli metamorph seed-mitre
|
|
|
|
print-install-token: ## Print the bootstrap install token (M2)
|
|
$(COMPOSE) exec api flask --app app.cli metamorph print-install-token
|
|
|
|
print-install-token-force: ## Force-mint a fresh install token (M2, --force)
|
|
$(COMPOSE) exec api flask --app app.cli metamorph print-install-token --force
|
|
|
|
clean: ## Remove containers, networks AND volumes (DESTRUCTIVE)
|
|
$(COMPOSE) down -v
|