diff --git a/.gitignore b/.gitignore index 9fd00d0..a41e8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ ssl/certs/ *.swp *.swo *~ + +# Documentation sources live on the host only; the site publishes the generated +# pages from forail-platform.github.io. +docs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bb0c8d..ef44996 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,15 @@ # Contributing to Forail Platform -This repository is the canonical home of the **full contributing guide** for the whole Forail Platform project — git workflow, commit conventions, coding standards, PR process across all repos. +The **full contributing guide** — git workflow, commit conventions, coding standards, PR process across all repos — is published on the developer docs site. -**Read first:** [docs/10-contributing-guide.md](./docs/10-contributing-guide.md) +**Read first:** + +The markdown sources for that site are kept outside this repository. ## What lives here - Docker Compose deployment stack (production and development overlays) - Install scripts and bootstrap -- Cross-repo documentation (architecture, deployment, ops, contributing) - Release notes for the platform as a whole ## Quick start (deploy stack) @@ -20,7 +21,7 @@ cp .env.example .env docker compose up -d ``` -See [README.md](./README.md) and `docs/` for full setup. +See [README.md](./README.md) for full setup. ## Reporting bugs diff --git a/README.md b/README.md index 0c62826..e2fe489 100644 --- a/README.md +++ b/README.md @@ -88,31 +88,24 @@ forail-devops/ ├── receptor/ # Receptor mesh configuration ├── scripts/ # Backup, restore, health checks ├── settings/ # Django production settings -├── docs/ # All documentation ├── .env.example # Environment template └── .github/workflows/ # GitHub Actions CI/CD ``` ## Documentation -### Deployment -- [Architecture Overview](docs/01-architecture-overview.md) -- [Docker Deployment](docs/07-docker-deployment.md) -- [CI/CD Pipeline](docs/08-ci-cd-pipeline.md) -- [CI Pipeline Reference](docs/ci-pipeline-reference.md) -- [Contributing Guide](docs/10-contributing-guide.md) - -### Plans -- [Separation Plan](docs/plan_separation.md) -- [Development Plan](docs/plan_development.md) -- [Detailed Plan](docs/plan_detailed.md) -- [Future Development](docs/future_development_plan.md) -- [Chat/AI Assistant Plan](docs/chat_plan.md) -- [Mobile App Plan](docs/mobile_plan.md) +Published on the developer and user docs site — the markdown sources are kept +outside this repository: + +- [Architecture Overview](https://forail-platform.github.io/docs/architecture.html) +- [Docker Deployment](https://forail-platform.github.io/docs/deployment.html) +- [CI/CD Pipeline](https://forail-platform.github.io/dev/ci-cd.html) +- [Contributing Guide](https://forail-platform.github.io/dev/contributing.html) +- [User Handbook](https://forail-platform.github.io/docs/user-handbook.html) +- [Administrator Handbook](https://forail-platform.github.io/docs/admin-handbook.html) ### Release -- [Release Notes v2026.03.0](docs/RELEASE_NOTES_v2026.03.0.md) -- [Start & Run Guide](docs/startrun.md) +- [Release notes](https://forail-platform.github.io/docs/) — one page per release ## Backup & Restore diff --git a/docs/00-platform-architecture.md b/docs/00-platform-architecture.md deleted file mode 100644 index 403bc78..0000000 --- a/docs/00-platform-architecture.md +++ /dev/null @@ -1,158 +0,0 @@ -# 00 — Platform Architecture - -This is the **platform-wide** map: how the eight Forail repositories fit -together, how a request flows from the browser down to a managed host, -and how the GitOps control plane (Kubernetes operator) drives Forail -through its REST API. For the internals of a single deployment (Nginx → -uWSGI/Daphne → Postgres/Redis → Receptor), see -[01 — Architecture Overview](01-architecture-overview.md). - ---- - -## Repository Map - -Forail is split into eight repositories, each independently buildable and -releasable, published under the `forail-platform` GitHub org and -`ghcr.io/forail-platform` container registry. - -| Repository | Role | Artifact | Versioning | -| ---------------------- | --------------------------------------------------------------------------- | --------------------------------------- | -------------------------------- | -| **forail-backend** | Django/DRF API, Celery task engine, Receptor mesh, RBAC, EDA, policy, audit | `ghcr.io/forail-platform/forail-backend` | CalVer | -| **forail-frontend** | React 18 + TypeScript + Vite SPA | `ghcr.io/forail-platform/forail-frontend` | CalVer | -| **forail-deploy** | Docker Compose, CI/CD, Nginx/OTel/OPA config, handbooks, this wiki | (compose project) | CalVer | -| **forail-assistant** | Optional AI microservice (Ollama + ChromaDB + FastAPI, RAG) | `ghcr.io/forail-platform/forail-assistant` | CalVer (preview) | -| **forail-operator** | Kubernetes operator, 9 CRDs, OLM bundle (GitOps control plane) | `ghcr.io/forail-platform/forail-operator` | SemVer | -| **forail-helm** | Helm chart that deploys the whole stack on Kubernetes | Helm chart | SemVer chart / CalVer appVersion | -| **forail-dev-cluster** | Vagrant + k3s test cluster (3 control-plane + 4 workers) | (Vagrant env) | CalVer | -| **github-org-profile** | Org `.github` repo: profile README, CoC, PR template | (community health) | — | - -### Dependency direction - -``` - forail-frontend ──┐ - ▼ - forail-backend ◀──── forail-operator (drives via REST /api/v2/) - ▲ │ - forail-assistant ─┘ (optional, SSE) │ - ▼ - forail-deploy ──▶ Docker Compose forail-helm ──▶ Kubernetes - ▲ - forail-dev-cluster (k3s target) -``` - -- **forail-backend** is the center of gravity — every other component - either renders its API (frontend), augments it (assistant), packages it - (deploy/helm), or drives it (operator). -- The **operator never touches the database** — it only calls the public - REST API (`/api/v2/`), exactly like a human admin would. This keeps the - control plane decoupled from backend internals. - ---- - -## Flow A — Interactive request (Browser → API → host) - -``` -Browser ──HTTPS:443──▶ Nginx (external, TLS) ──HTTP:8013──▶ Nginx (internal) - │ - ┌───────────────────────────────────────────┼──────────────┐ - │ /api/ /websocket/ │ / │ - ▼ ▼ ▼ - uWSGI:8050 Daphne:8051 static SPA (frontend) - │ │ - ▼ │ (job events relayed back over WS) - Django (forail-web) │ - │ │ - ┌───────────┼──────────────┘ - ▼ ▼ - Postgres:5432 Redis:6379 ──▶ Celery dispatcher (forail-task) ──▶ Receptor mesh ──▶ managed hosts -``` - -Frontend is served as static assets and talks to the same origin; live -job output streams back over the Django Channels WebSocket (Daphne). - -## Flow B — GitOps control plane (kubectl → Operator → Forail) - -This is the path the **forail-operator** adds. Verified end-to-end on -`forail-dev-cluster` (k3s v1.30.4): - -``` - kubectl apply organization.yaml - │ - ▼ - Kubernetes API server (CR stored, schema-validated by the CRD) - │ watch - ▼ - forail-operator (ns forail-operator) - │ GET/POST http://forail-web.forail.svc.cluster.local:8013/api/v2/organizations/ - │ Authorization: Bearer - ▼ - forail-backend ──▶ Postgres (Organization row created → id returned) - │ - ▼ - operator writes status back to the CR: status.forailID=2, Ready=True ("in sync with Forail") -``` - -The nine CRDs (`forail.forail-platform.io/v1alpha1`) — Organization, Team, -Project, Inventory, Credential, JobTemplate, Schedule, Workflow, and the -multi-cluster `ForailInstance` — each reconcile the same way: read CR -spec, call the matching `/api/v2/` endpoint, record the Forail object ID -and a `Ready`/`Synced` condition, and run a finalizer to delete the -Forail object when the CR is removed. `ForailInstance` lets one operator -drive several Forail backends (a `ClientPool` keyed by `spec.forailInstance`). - ---- - -## Deployment topologies - -| Path | Tooling | Use case | -| ------------------ | ---------------- | ---------------------------------------------------------------- | -| **Docker Compose** | `forail-deploy` | Single-host install, dev, small prod | -| **Helm** | `forail-helm` | Kubernetes deployment of the full stack | -| **Operator** | `forail-operator` | GitOps management of Forail objects (on top of either deployment) | - -The operator is **orthogonal** to how Forail itself is deployed: it -manages Forail _objects_ (orgs, projects, job templates…) declaratively -and can point at a Forail installed by Compose, Helm, or anything else, as -long as it can reach `/api/v2/` with a token. - ---- - -## Versioning policy - -Forail deliberately uses **two** versioning schemes. Which one a repo uses -depends on _what kind of thing it is_, not on preference. - -| Scheme | Format | Repos | Why | -| ---------- | ---------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **CalVer** | `YYYY.MM.PATCH` (e.g. `2026.05.0`) | forail-backend, forail-frontend, forail-deploy, forail-assistant, forail-dev-cluster | These ship together as one **coordinated platform release**. A user runs "the May 2026 platform" — the date _is_ the meaningful version. They have no independent API/compat contract with each other beyond "same platform release". | -| **SemVer** | `MAJOR.MINOR.PATCH` (e.g. `1.0.0`) | forail-operator, forail-helm | These are **independently consumed artifacts** with their own compatibility contracts, governed by ecosystem rules. Helm requires the chart `version` to be SemVer; OLM/OperatorHub builds its upgrade graph from SemVer. Breaking the CRD API or chart values is a SemVer **major**, regardless of the calendar. | - -### How the two bridge - -- **forail-helm** carries both: `version: 1.0.0` (SemVer — the chart's own - version, per the Helm spec) and `appVersion: 2026.05.0` (CalVer — the - platform release it installs). Bump `version` when the chart templates - change; bump `appVersion` when it targets a newer platform release. -- **forail-operator** Chart `version` and `appVersion` are both `1.0.0` - because the chart and the operator binary it deploys version together. - -### When to bump - -- **CalVer repos:** new `YYYY.MM` for each monthly platform release; - `.PATCH` for fixes within that month. -- **forail-operator (SemVer):** _major_ on a breaking CRD-API change - (e.g. a `v1alpha1` → `v1beta1` migration or removed field); _minor_ for - a new CRD or backward-compatible field; _patch_ for bug fixes. -- **forail-helm (SemVer):** _major_ on a breaking `values.yaml` change; - _minor_ for new opt-in values; _patch_ for template fixes. Track the - platform in `appVersion`. - ---- - -## See also - -- [01 — Architecture Overview](01-architecture-overview.md) — single-deployment internals -- [08 — CI/CD Pipeline](08-ci-cd-pipeline.md) — how images are built and released -- [wiki-index](wiki-index.md) — full documentation index -- `forail-operator/README.md` — CRD reference and operator install -- `forail-helm/README.md` — Kubernetes deployment diff --git a/docs/01-architecture-overview.md b/docs/01-architecture-overview.md deleted file mode 100644 index 5741b69..0000000 --- a/docs/01-architecture-overview.md +++ /dev/null @@ -1,231 +0,0 @@ -# 01 — Architecture Overview - -## System Diagram - -![Forail Architecture](img/architecture.png) - -
-Text version (for terminals) - -``` - ┌─────────────────────────────────────────┐ - │ End Users │ - │ (Browser / API Client) │ - └────────────────┬────────────────────────┘ - │ - HTTPS (443) - │ - ┌────────────────▼────────────────────────┐ - │ Nginx (External) │ - │ TLS termination, rate limiting, │ - │ security headers, HTTP→HTTPS redirect │ - └────────────────┬────────────────────────┘ - │ - HTTP (8013) - │ - ┌──────────────────────────▼──────────────────────────┐ - │ Nginx (Internal) │ - │ Routes requests to correct backend │ - ├──────────────┬──────────────────┬───────────────────┤ - │ /static/ │ /api/, /sso/, │ /websocket/ │ - │ (files) │ / (pages) │ (real-time) │ - └──────┬───────┴────────┬─────────┴──────┬────────────┘ - │ │ │ - Static files uWSGI (8050) Daphne (8051) - /var/lib/awx/ Django WSGI Django ASGI - public/static/ (sync HTTP) (WebSocket) - │ │ - ┌──────────────────────▼─────────────────▼────────────┐ - │ Django Application │ - │ REST API ─── Serializers ─── Models ─── Database │ - └──────────────────────┬───────────────────────────────┘ - │ - ┌────────────────▼────────┐ - │ PostgreSQL (5432) │ - └─────────────────────────┘ - - ┌──────────────────────────────────────────────────────┐ - │ Task Container │ - │ │ - │ Dispatcher ──► Ansible Runner ──► Callback Receiver │ - │ │ │ │ - │ │ Receptor WSRelay │ - │ │ (mesh networking) │ │ - └───────┼────────────────────────────────────┼─────────┘ - │ │ - ▼ ▼ - ┌──────────┐ ┌──────────────┐ - │ Redis │ │ WebSocket │ - │ (6379) │ │ Clients │ - └──────────┘ └──────────────┘ -``` - -
- ---- - -## Component Roles - -### Web Container (`forail-web`) - -Handles all HTTP and WebSocket traffic. Runs three processes via `supervisord`: - -| Process | Port | Purpose | -| ---------------- | ---- | ------------------------------------------- | -| Nginx (internal) | 8013 | Routes requests, serves static files | -| uWSGI | 8050 | Django app — API, pages, authentication | -| Daphne | 8051 | WebSocket connections for real-time updates | - -**Watch out:** Internal Nginx is inside the container and handles routing. External Nginx -is a separate container that terminates TLS. Don't confuse them — configuration lives in -two different files: - -- External: `tools/docker-compose-prod/nginx/nginx.conf` -- Internal: `tools/docker-compose-prod/settings/nginx-internal.conf` - -### Task Container (`forail-task`) - -Handles background job execution. Runs four processes: - -| Process | Purpose | -| --------------------- | ------------------------------------------------------ | -| **Dispatcher** | Picks tasks from Redis, manages capacity, starts jobs | -| **Callback Receiver** | Receives events from Ansible Runner, saves to database | -| **WSRelay** | Broadcasts job events to WebSocket clients | -| **Receptor** | Mesh networking for remote job execution | - -**Watch out:** If any of these 4 processes goes down, jobs won't work correctly. -Check with `supervisorctl status` inside the container. - -### PostgreSQL - -Stores all data: models, job events (partitioned), activity stream, RBAC roles, -configuration. Job events are in a **partitioned table** — one partition per job, -which is critical for performance since a single job can have tens of thousands of events. - -### Redis - -Used for two purposes: - -- **DB 0:** Celery message broker + Django Channels (WebSocket) -- **DB 1:** Cache (API response caching, rate limiting) - -**Watch out:** If Redis goes down, jobs won't start and WebSocket won't work. -However, data in PostgreSQL remains safe. - -### Receptor - -Mesh networking for distributed job execution. In a single-node deployment -(default), Receptor runs locally. In a multi-node setup, it routes jobs to -remote execution nodes over TCP port 2222. - ---- - -## Request Flow — What happens when - -### ...a user launches a job template - -1. User clicks "Launch" in the UI or sends POST to `/api/v2/job_templates/{id}/launch/` -2. Django creates a `Job` record in the database with status `pending` -3. A Celery task is placed in the Redis queue -4. Dispatcher picks up the task and transitions the job to `waiting` → `running` -5. Ansible Runner executes the playbook -6. Events flow: Runner → Callback Receiver → database + WSRelay → WebSocket → browser -7. Browser receives events and updates the UI in real-time (no page refresh needed) - -### ...an external system sends a webhook (EDA) - -1. External system (GitHub, Alertmanager, etc.) sends POST to `/api/v2/eda_webhooks//` -2. Nginx forwards to uWSGI (no authentication required on this endpoint) -3. Django verifies the HMAC signature against the EventRule's webhook key -4. An `EventLog` record is created with status `received` -5. A Celery task is dispatched for async rule evaluation -6. The Celery task evaluates Jinja2 conditions against the webhook payload -7. If conditions match: launches job templates, workflows, or sends notifications -8. `EventLog` is updated with results; `AuditEvent` is created for compliance -9. Caller receives `202 Accepted` immediately (processing is async) - -### ...a user opens a page in the browser - -1. Browser sends a GET request -2. Nginx terminates TLS, forwards to internal Nginx (8013) -3. Internal Nginx: - - `/static/*` → serves files directly from disk - - `/websocket/*` → proxies to Daphne (8051) - - Everything else → proxies to uWSGI (8050) -4. Django processes the request: middleware → URL routing → view → serializer → response - ---- - -## Port Reference - -| Port | Service | Description | -| ---- | ---------------- | ---------------------------------------- | -| 443 | Nginx (external) | HTTPS entry point for users | -| 80 | Nginx (external) | HTTP → redirect to HTTPS | -| 8013 | Nginx (internal) | Internal routing (within Docker network) | -| 8050 | uWSGI | Django sync application | -| 8051 | Daphne | Django async (WebSocket) | -| 5432 | PostgreSQL | Database | -| 6379 | Redis | Broker + cache | -| 2222 | Receptor | Mesh networking | - -**Watch out:** Ports 8050 and 8051 are not externally accessible — only internal Nginx -on 8013 communicates with them. Do not expose 8050/8051 outside the Docker network. - ---- - -## Directory Structure — Where things live - -``` -forail/ # Python backend -├── api/ # REST API (views, serializers, urls) -├── main/ # Core (models, tasks, signals, migrations, commands) -├── conf/ # Database-backed settings -├── settings/ # Django settings files -├── sso/ # SSO authentication backends -└── ui_next/ # React frontend - -tools/ -├── docker-compose-prod/ # Production deployment -│ ├── docker-compose.yml # 6 services -│ ├── .env # Environment variables -│ ├── settings/ # Django settings for production -│ ├── nginx/ # Nginx config + SSL certificates -│ ├── receptor/ # Receptor mesh config -│ └── scripts/ # Init, backup, healthcheck -├── ansible/roles/dockerfile/ # Dockerfile generation -└── scripts/ # Vagrant provisioning - -requirements/ # Python dependencies -``` - ---- - -## Key Design Decisions - -### Why two Nginx instances? - -Internal Nginx is part of the Docker image and routes requests to uWSGI/Daphne. -External Nginx is a separate container that handles TLS termination, rate limiting, -and security headers. This separation allows the external Nginx to be replaced -with a load balancer (HAProxy, AWS ALB) without changing the application. - -### Why partitioned job events? - -A single job with 100 hosts and 50 tasks generates ~5,000 events. A system running -100 jobs daily has ~500,000 events per day. Without partitioning, every query would -scan the entire table. With partitioning, a query for a specific job reads only -one partition. - -### Why Receptor instead of SSH? - -Receptor provides mesh networking — hop-by-hop routing for air-gapped environments, -automatic failover, and multiplexing. SSH requires a direct connection to every -node, which doesn't work in complex network topologies. - -### Why database-backed settings? - -Most settings can be changed without restarting the application via the API -(`/api/v2/settings/`). This is critical for production where you don't want -downtime to change a timeout or add an LDAP server. diff --git a/docs/07-docker-deployment.md b/docs/07-docker-deployment.md deleted file mode 100644 index 846da19..0000000 --- a/docs/07-docker-deployment.md +++ /dev/null @@ -1,322 +0,0 @@ -# 07 — Docker & Deployment - -How to build, configure, and deploy Forail Platform to production. - ---- - -## Architecture - -Forail Platform uses a separated architecture with independent Docker images: - -| Service | Image | Purpose | -| -------------- | -------------------------------------- | ----------------------------------------------- | -| forail-web | `ghcr.io/forail-platform/forail-backend` | Django API (uwsgi + daphne + nginx-internal) | -| forail-task | `ghcr.io/forail-platform/forail-backend` | Task execution (dispatcher, callback, receptor) | -| forail-init | `ghcr.io/forail-platform/forail-backend` | One-shot: migrations, admin user, provisioning | -| forail-frontend | `ghcr.io/forail-platform/forail-frontend` | React SPA served by nginx | -| postgres | `postgres:15-alpine` | Database | -| redis | `redis:7-alpine` | Cache and message broker | -| nginx | `nginx:1.27-alpine` | TLS termination, routing | - -### Startup Order - -``` -postgres ──► redis ──► forail-init ──► forail-web ──► forail-task ──► nginx - forail-frontend ──┘ -``` - -Each service waits for the previous one to be healthy before starting. - -### Request Routing (External Nginx) - -| Path | Destination | Description | -| ---------------------- | ----------------- | -------------------- | -| `/api/*` | forail-web:8013 | REST API | -| `/sso/*` | forail-web:8013 | SSO/SAML/LDAP | -| `/api/login/` | forail-web:8013 | Login (rate-limited) | -| `/(api/)?websocket/` | forail-web:8013 | WebSocket (upgrade) | -| `/*` (everything else) | forail-frontend:80 | React SPA | - ---- - -## Building Docker Images - -### Backend - -```bash -cd forail-backend -docker build -t ghcr.io/forail-platform/forail-backend:latest . -docker push ghcr.io/forail-platform/forail-backend:latest -``` - -The Dockerfile is a multi-stage build: - -1. **builder** (Ubuntu 24.04): installs Python deps, builds sdist, runs collectstatic -2. **runtime** (Ubuntu 24.04): minimal image with runtime deps, receptor, supervisor - -### Frontend - -```bash -cd forail-frontend -docker build -t ghcr.io/forail-platform/forail-frontend:latest . -docker push ghcr.io/forail-platform/forail-frontend:latest -``` - -The Dockerfile is a multi-stage build: - -1. **builder** (Node 20 Alpine): `npm ci && npm run build` -2. **runtime** (nginx 1.27 Alpine): serves built assets with SPA fallback - ---- - -## Production Deployment - -### Prerequisites - -- Docker 24+ with Compose v2 -- 8GB+ RAM, 4+ CPU cores -- Domain name with SSL certificate (or self-signed for testing) - -### Quick Start - -```bash -cd forail-deploy - -# 1. Create configuration -cp .env.example .env -# Edit .env — set all REQUIRED values (see below) - -# 2. SSL certificates -mkdir -p nginx/ssl - -# Let's Encrypt (production): -certbot certonly --standalone -d forail.example.com -cp /etc/letsencrypt/live/forail.example.com/fullchain.pem nginx/ssl/ -cp /etc/letsencrypt/live/forail.example.com/privkey.pem nginx/ssl/ - -# Or self-signed (testing): -openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout nginx/ssl/privkey.pem -out nginx/ssl/fullchain.pem \ - -subj "/CN=forail.example.com" - -# 3. Deploy -docker compose up -d - -# 4. Watch initialization -docker compose logs -f forail-init - -# 5. Verify -curl -k https://forail.example.com/api/v2/ping/ -``` - -### Deploy in Vagrant (testing) - -```bash -cd forail-deploy -vagrant up # Ubuntu 24.04 VM + Docker + Compose + SSL + .env auto-generated -vagrant ssh -cd /forail-deploy -docker compose up -d - -# Access from host: https://192.168.56.22/ -``` - ---- - -## Environment Variables - -### Required - -| Variable | Description | Generate with... | -| ---------------------------------- | ----------------- | --------------------------- | -| `POSTGRES_PASSWORD` | DB password | `openssl rand -hex 16` | -| `FORAIL_SECRET_KEY` | Django crypto key | `openssl rand -hex 32` | -| `FORAIL_BROADCAST_WEBSOCKET_SECRET` | WS auth secret | `openssl rand -hex 32` | -| `FORAIL_ADMIN_PASSWORD` | Admin password | Strong password | -| `FORAIL_CSRF_TRUSTED_ORIGINS` | CSRF origins | `https://forail.example.com` | - -### Optional - -| Variable | Default | Description | -| ---------------------- | -------------------------------------- | ----------------------------------- | -| `FORAIL_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Allowed HTTP hosts — list your real hostnames; `*` disables the Host check | -| `FORAIL_ADMIN_USER` | `admin` | Admin username | -| `FORAIL_ADMIN_EMAIL` | `admin@example.com` | Admin email | -| `FORAIL_NODE_NAME` | `forail-node` | Instance hostname | -| `FORAIL_NODE_TYPE` | `hybrid` | `hybrid`, `control`, or `execution` | -| `FORAIL_BACKEND_IMAGE` | `ghcr.io/forail-platform/forail-backend` | Backend Docker image | -| `FORAIL_FRONTEND_IMAGE` | `ghcr.io/forail-platform/forail-frontend` | Frontend Docker image | -| `FORAIL_TAG` | `2026.07.0` | Image tag — pinned to a release, not `latest` | -| `FORAIL_TASK_PRIVILEGED` | `false` | Run `forail-task` privileged. Required for the podman-in-container job path | -| `FORAIL_TASK_CGROUP` | `private` | Set to `host` together with `FORAIL_TASK_PRIVILEGED=true` for job execution | -| `NGINX_HTTP_PORT` | `80` | External HTTP port | -| `NGINX_HTTPS_PORT` | `443` | External HTTPS port | - -### Watch out - -- **`FORAIL_SECRET_KEY` MUST REMAIN THE SAME** between upgrades. If you change it, - all sessions, tokens, and encrypted credentials become invalid. - -- **`FORAIL_CSRF_TRUSTED_ORIGINS` must include the full URL** with `https://`. Without - it, the login form won't work (403 CSRF error). - ---- - -## SSL/TLS - -### Let's Encrypt (recommended for production) - -```bash -certbot certonly --standalone -d forail.example.com -cp /etc/letsencrypt/live/forail.example.com/{fullchain,privkey}.pem nginx/ssl/ -``` - -Auto-renewal (crontab): - -```bash -0 0 1 * * certbot renew && cp /etc/letsencrypt/live/forail.example.com/*.pem /path/to/nginx/ssl/ && docker compose restart nginx -``` - -### Security Notes - -- Nginx is configured for **TLS 1.2 and 1.3** — older versions are disabled -- **HSTS** header is enabled (63072000 seconds) -- **Rate limiting** on `/api/login/` — 5 requests/second, burst 10 -- `client_max_body_size` is **50MB** - ---- - -## Backup & Restore - -### Backup - -```bash -docker compose exec forail-task bash /etc/forail/backup.sh - -# With custom retention (30 days) -docker compose exec forail-task bash /etc/forail/backup.sh 30 -``` - -### Scheduled backup (crontab) - -```bash -0 2 * * * cd /path/to/forail-deploy && docker compose exec -T forail-task bash /etc/forail/backup.sh -``` - -### Restore - -```bash -docker compose stop forail-web forail-task -gunzip -c forail_backup_20260317.sql.gz | docker compose exec -T postgres psql -U forail forail -docker compose start forail-web forail-task -``` - ---- - -## Health Checks - -```bash -# API ping (no auth) -curl -k https://forail.example.com/api/v2/ping/ - -# Instance capacity (auth required) -curl -k -u admin:password https://forail.example.com/api/v2/instances/ - -# Service status -docker compose ps - -# Supervisor processes -docker compose exec forail-web supervisorctl status -docker compose exec forail-task supervisorctl status -``` - ---- - -## Troubleshooting - -### Container won't start - -```bash -docker compose logs forail-init # Check migrations and init -# "database does not exist" → POSTGRES_DB mismatch -# "authentication failed" → POSTGRES_PASSWORD mismatch -``` - -### Can't log in (403 CSRF) - -```bash -# Check FORAIL_CSRF_TRUSTED_ORIGINS in .env -# Must be full URL with https:// (e.g., https://192.168.56.22) -``` - -### Server Error (500 on root page) - -```bash -# Check if frontend container is running -docker compose ps forail-frontend -# Must be healthy - -# Check nginx routing -docker compose logs nginx -``` - -### Jobs not running - -```bash -docker compose exec forail-task supervisorctl status -# All 4 must be RUNNING: receptor, dispatcher, callback-receiver, wsrelay - -docker compose exec forail-web forail-manage list_instances -``` - -### Forgotten admin password - -```bash -docker compose exec forail-web forail-manage update_password --username=admin --password=NewPass123! -``` - ---- - -## Upgrading - -```bash -cd forail-deploy - -# 1. Pull new images -docker compose pull - -# 2. Recreate containers (migrations run automatically via forail-init) -docker compose up -d - -# 3. Verify -docker compose ps -curl -k https://forail.example.com/api/v2/ping/ -``` - ---- - -## Scaling - -### Adding an execution node - -```bash -# On the execution node: -docker run -d --name forail-task \ - -e DATABASE_HOST=db.example.com \ - -e REDIS_HOST=redis.example.com \ - -e FORAIL_NODE_TYPE=execution \ - -e FORAIL_NODE_NAME=exec-node-1 \ - ghcr.io/forail-platform/forail-backend:latest launch_awx_task.sh - -# On the control node: -docker compose exec forail-web forail-manage provision_instance --hostname=exec-node-1 --node-type=execution -docker compose exec forail-web forail-manage register_queue --queuename=default --hostnames=exec-node-1 -``` - -### Recommended Hardware - -| Size | CPU | RAM | Disk | -| -------------------- | --- | ---- | --------- | -| Small (≤100 hosts) | 4 | 8GB | 50GB SSD | -| Medium (≤1000 hosts) | 8 | 16GB | 100GB SSD | -| Large (≤10000 hosts) | 16 | 32GB | 200GB SSD | diff --git a/docs/08-ci-cd-pipeline.md b/docs/08-ci-cd-pipeline.md deleted file mode 100644 index 4efe618..0000000 --- a/docs/08-ci-cd-pipeline.md +++ /dev/null @@ -1,182 +0,0 @@ -# 08 — CI/CD Pipeline - -Forail Platform uses **GitHub Actions** as the public CI/CD pipeline. Each repository has its own workflow in `.github/workflows/` that runs on push and pull request. - ---- - -## Pipeline Overview - -``` -┌──────────┐ ┌────────┐ ┌────────┐ ┌───────────────────────────────┐ -│ Checkout │──►│ Lint │──►│ Test │───────►│ publish │ -└──────────┘ └────────┘ └────────┘ │ build → test in image → push │ - GitHub ruff / pytest / └───────────────────────────────┘ - Actions go vet vitest / envtest ONLY on a v* tag -``` - -Each repo's workflow file: **`.github/workflows/ci.yml`** - -## Pipeline Stages - -The jobs differ per repo. What each one actually runs: - -| Repo | Always (push + PR) | `publish` (v* tags only) | -| ------------------ | ---------------------------------------------------- | --------------------------------------------------------- | -| `forail-backend` | `ruff check` , `pyproject.toml` parse, standalone tests | build image → **run the Django functional tests inside the built image** → push | -| `forail-frontend` | `npm run lint`, `npm test`, `npm run build` | build and push image | -| `forail-operator` | `go vet`, `go build`, controller tests (envtest) | build and push image | -| `forail-assistant` | `pytest -q` | build and push image | - -### Stage Conditions - -| Stage | When it runs | -| ------------ | ------------------------------------------------------------------ | -| Lint, Test | Every push and every PR | -| `publish` | **Only** when the ref is a tag matching `v*` — `if: startsWith(github.ref, 'refs/tags/v')` | - -> **A merge to `main` does not produce an image.** It runs lint and tests only. -> The image on `ghcr.io` changes when, and only when, someone pushes a `v*` tag. -> This is the single most common source of confusion: `main` can be many commits -> ahead of the newest published image, and that is by design. - -### Known gaps - -These are documented as they are, not as they should be: - -- **`ruff check . || true`** in the backend — lint output is informational; lint - errors do not fail the build. -- **No container image scanning** (no Trivy) and **no `pip-audit`** stage in any - repo's workflow today. - ---- - -## GitHub Actions Secrets - -For the release stage, each repo needs the following secret: - -| Secret | Description | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `GITHUB_TOKEN` | Provided automatically by GitHub Actions. Used with the built-in `permissions: packages: write` to push to `ghcr.io/forail-platform/*` | - -No third-party credentials are required — everything runs in the GitHub-hosted runner with built-in tokens. - ---- - -## Docker Images - -| Image | Source | Description | -| --------------------------------------------------- | ----------------------------- | ------------------------------------- | -| `ghcr.io/forail-platform/forail-backend:` | `forail-backend/Dockerfile` | Django API + task engine | -| `ghcr.io/forail-platform/forail-frontend:` | `forail-frontend/Dockerfile` | React SPA + nginx | -| `ghcr.io/forail-platform/forail-operator:` | `forail-operator/Dockerfile` | Kubernetes operator | -| `ghcr.io/forail-platform/forail-assistant:` | `forail-assistant/Dockerfile` | FastAPI + Ollama + ChromaDB (preview) | - -Every image carries **exactly one tag: the version**, taken from the git tag with -the `v` stripped. There is **no `latest` tag** — nothing in the pipeline creates -one, so `docker pull …:latest` fails. Always pull an explicit version. - -Release-candidate tags (`v2026.07.2-rc1`) go through the same `publish` job and -produce a normal image (`…:2026.07.2-rc1`), which is how a build is validated -against a real cluster before a release is cut. - -All images are **public** — no pull secret required for `docker pull` or `helm install`. - -To see what is actually published, ask the registry rather than guessing: - -```bash -TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:forail-platform/forail-backend:pull" \ - | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])") -curl -s -H "Authorization: Bearer $TOKEN" \ - https://ghcr.io/v2/forail-platform/forail-backend/tags/list -``` - ---- - -## Versioning - -Forail uses **CalVer** (Calendar Versioning): - -``` -YYYY.MM.PATCH -2026.03.0 # First release of March 2026 -2026.03.1 # Patch release -2026.04.0 # April release -``` - -**Each repo carries its own tag.** There is no central tag that releases the -platform — `forail-backend`, `forail-frontend`, `forail-operator` and -`forail-assistant` are tagged individually, and only the repos that changed need -a new one. That is why published versions legitimately differ between components -(for example backend `2026.07.1` alongside frontend `2026.07.0`); the Helm chart -is what pins a working set together. - -```bash -cd forail-backend -git tag -a v2026.05.0 -m "Forail 2026.05.0" -git push github v2026.05.0 -# GitHub Actions then: lint → test → build image → test inside the image → push to ghcr.io -``` - ---- - -## Running CI Locally - -### Backend - -```bash -cd forail-backend - -# Lint -ruff check forail/ - -# Tests -DJANGO_SETTINGS_MODULE=forail.settings.development \ - python -m pytest forail/main/tests/unit/ -q - -# Build image (tag it with a version — there is no :latest) -docker build -t ghcr.io/forail-platform/forail-backend:2026.05.0 . -``` - -### Frontend - -```bash -cd forail-frontend - -# Lint -npx tsc --noEmit - -# Tests -npx vitest run - -# Build image (tag it with a version — there is no :latest) -docker build -t ghcr.io/forail-platform/forail-frontend:2026.05.0 . -``` - ---- - -## Release Process - -1. Ensure GitHub Actions is green on `main` for every repo being released -2. Bump `VERSION` in the repos that ship a version, and the Helm chart's - `version` / `appVersion` so it pins the images you are about to publish -3. Write the release notes (`forail-deploy/docs/RELEASE_NOTES_v.md`) and - the docs-site release page -4. Tag each changed repo and push the tag — this is what builds and publishes: - `git tag -a v2026.05.0 -m "Forail 2026.05.0" && git push github v2026.05.0` -5. Install the published chart and images into a clean cluster and run the full - regression before announcing anything -6. Create the GitHub Release - -### Validate before you release - -Cut an rc tag first (`v2026.05.0-rc1`). It publishes a real image through the -same job, so the release candidate can be installed into a clean cluster and put -through the full Cypress suite. Only then cut the real tag. Rc images stay on -`ghcr.io` — harmless, but do not point a chart at one. - -### Watch out - -- **Never release without passing tests.** -- **Tag format must have `v` prefix:** `v2026.05.0`, not `2026.05.0`. -- **Merging to `main` publishes nothing** — only a `v*` tag does. -- **Image visibility** — when a new package is first pushed to `ghcr.io`, GitHub creates it as **private** by default. You must manually flip it to public via the Packages settings (`https://github.com/orgs/forail-platform/packages`). diff --git a/docs/10-contributing-guide.md b/docs/10-contributing-guide.md deleted file mode 100644 index 8ec3cd4..0000000 --- a/docs/10-contributing-guide.md +++ /dev/null @@ -1,211 +0,0 @@ -# 10 — Contributing Guide - -Git workflow, commit conventions, coding standards, and the PR process. - ---- - -## Rules - -1. **All development and testing inside the Vagrant VM** — never install dependencies on the host -2. **Every change must be understood** — if you can't explain why, don't commit it -3. **Author of all commits is Krstan Vjestica** — never attribute tools as authors -4. **Review the diff before committing** — always -5. **Everything must pass before you commit** — not `vagrant validate`, not a syntax - check: the actual thing running. A change to the dev environment is proven by - destroying the VM and building it again; a change to the platform is proven by - a clean install plus the full Cypress regression - ---- - -## Development environment - -- **VirtualBox is the only supported provider.** libvirt/KVM must not be running - on the same host — one hypervisor owns AMD-V per boot, and the loser's guests - die with `Guru Meditation VERR_SVM_IN_USE` or wedge mid-boot. Keep `libvirtd` - masked; `forail-dev-cluster/scripts/up.sh` refuses to start if it is active. - Full root-cause writeup: `forail-dev-cluster/docs/TROUBLESHOOTING-vagrant.md`. -- **Bring the cluster up with `scripts/up.sh`**, not bare `vagrant up` — it goes - node by node in dependency order and recreates any node whose boot wedges. -- **Start from a clean state when you are testing.** `vagrant up` on an existing - VM only boots it; it does **not** re-provision. Mixing a freshly created node - with previously provisioned ones gives two different cluster CAs and a control - plane that never reaches quorum. Use `vagrant destroy -f` first. -- **The dev VMs collide on host ports.** `forail-backend` and `forail-deploy` - both forward `8013` and `8080`, so only one of them can run at a time. - ---- - -## Git Workflow - -### Branch naming - -``` -feature/dynamic-surveys # New feature -fix/job-stuck-pending # Bug fix -refactor/split-serializers # Refactoring -docs/wiki-task-engine # Documentation -test/inventory-api-tests # Tests -chore/update-dependencies # Maintenance -``` - -### Standard flow - -**`main` is the integration branch.** There is no `devel` branch in any Forail -repo — branch from `main` and target `main` in the PR. - -```bash -# 1. Create branch from main -git checkout main -git pull github main -git checkout -b feature/my-feature - -# 2. Make changes, test, commit -vagrant rsync -vagrant ssh -c "cd /awx_devel && forail-test" -git add forail/main/models/my_model.py -git commit -m "feat(models): add Policy model for governance" - -# 3. Push and create PR -git push github feature/my-feature -gh pr create --base main -``` - -### Updating the branch - -```bash -git checkout main && git pull github main -git checkout feature/my-feature -git rebase main -``` - -### Remotes - -Most repos have two: `github` (github.com/forail-platform, the canonical one, -where CI and releases run) and `origin` (the GitLab mirror). Push branches and -tags to **both** when you are done, or the mirror silently falls behind. - ---- - -## Commit Conventions - -### Format - -``` -type(scope): short description -``` - -### Types - -| Type | When | Example | -| ---------- | ------------------ | ------------------------------------------- | -| `feat` | New feature | `feat(api): add /policies/ endpoint` | -| `fix` | Bug fix | `fix(tasks): prevent job stuck in pending` | -| `refactor` | Code restructuring | `refactor(serializers): split into modules` | -| `docs` | Documentation | `docs(wiki): add task engine documentation` | -| `test` | Tests | `test(api): add inventory CRUD tests` | -| `chore` | Maintenance | `chore(deps): update Django to 4.2.18` | - -### Scopes - -`models`, `api`, `tasks`, `ui`, `auth`, `rbac`, `deploy`, `ci`, `deps` - -### Rules - -- First line **under 72 characters** -- **Imperative mood:** "add feature" not "added feature" -- **No period** at the end of the first line -- **Never AI attribution** in commit messages - ---- - -## Coding Standards - -### Python - -- Max line length: **160 characters** (configured in pyproject.toml) -- Imports: standard library → third party → local (separated by blank lines) -- Descriptive variable names: `running_jobs` not `x` -- f-strings for formatting: `f"Job {job.id} failed"` -- Never bare `except:` — always a specific exception type - -### TypeScript/React - -- Functional components with hooks (no class components) -- TypeScript interfaces for props -- TanStack Query for data fetching (not useEffect + fetch) -- `cn()` for conditional Tailwind classes -- Semantic colors (`bg-background`) not hardcoded (`bg-white`) -- Path alias `@/` instead of relative `../../..` - -### Linting (run before every commit) - -```bash -flake8 forail/ --count --statistics -cd forail/ui_next && npx tsc --noEmit -``` - ---- - -## Pull Request Process - -### Before creating a PR - -- [ ] All tests pass -- [ ] No lint errors -- [ ] Commit messages follow conventions -- [ ] Branch is rebased on latest `main` -- [ ] Changes are minimal and focused - -### PR guidelines - -- **Keep PRs small** — ideally under 500 lines. Large PRs are harder to review. -- **One concern per PR** — don't mix a bug fix with refactoring. -- **Include tests** — new features need tests, bug fixes need a regression test. -- **Target `main`** — all PRs merge into `main`. - -### Review Checklist - -**For the author:** - -- [ ] Reviewed my own diff before requesting review -- [ ] No debug code, console.log, or print statements -- [ ] No hardcoded secrets or URLs -- [ ] Error handling is adequate -- [ ] New code has tests - -**For the reviewer:** - -- [ ] Code does what the PR description says -- [ ] Security: no SQL injection, XSS, credential exposure -- [ ] Edge cases are handled (empty lists, null, concurrent access) -- [ ] Follows existing patterns in the codebase -- [ ] Will this be easy to maintain in 6 months? - ---- - -## Quick Reference — Common Development Tasks - -### Add a new API endpoint - -1. Model in `forail/main/models/` → register in `__init__.py` -2. Migration: `forail-manage makemigrations main` -3. Serializer in `forail/api/serializers/` -4. View in `forail/api/views/` -5. URL module in `forail/api/urls/` → register in `urls.py` -6. Access class in `forail/main/access.py` -7. Tests - -### Add a new frontend page - -1. TypeScript types in `src/api/types.ts` -2. API hooks in `src/api/hooks/` -3. Page in `src/pages/` -4. Route in `src/App.tsx` -5. Navigation in `src/components/layout/Sidebar.tsx` -6. Tests - -### Add a management command - -1. File in `forail/main/management/commands/` -2. Implement `Command` class with `handle()` method -3. Test: `forail-manage my_command --help` diff --git a/docs/ADMIN_HANDBOOK.md b/docs/ADMIN_HANDBOOK.md deleted file mode 100644 index cc77203..0000000 --- a/docs/ADMIN_HANDBOOK.md +++ /dev/null @@ -1,937 +0,0 @@ -# Forail Platform — Administrator Handbook - -Operational guide for installing, running, and maintaining a Forail Platform deployment. Each section is step-by-step with concrete example values you can copy. Click any item in the table of contents to jump straight to it. - -For day-to-day UI usage, see the companion [User Handbook](HANDBOOK.md). - ---- - -## Table of Contents - -### Install & First Boot - -- [Prerequisites](#prerequisites) -- [Installation (Docker Compose)](#installation-docker-compose) -- [First-Time Setup](#first-time-setup) -- [TLS / SSL Setup](#tls--ssl-setup) -- [Initial Admin Hardening](#initial-admin-hardening) - -### Day-2 Operations - -- [Starting and Stopping the Stack](#starting-and-stopping-the-stack) -- [Inspecting Logs](#inspecting-logs) -- [Health Checks](#health-checks) -- [Backup](#backup) -- [Restore](#restore) -- [Upgrade](#upgrade) -- [Rolling Back](#rolling-back) - -### Scaling & Topology - -- [Adding an Execution Node](#adding-an-execution-node) -- [Adding a Hop Node](#adding-a-hop-node) -- [Tuning Capacity](#tuning-capacity) -- [Switching to Kubernetes](#switching-to-kubernetes) - -### Observability - -- [Enabling OpenTelemetry](#enabling-opentelemetry) -- [Grafana Dashboards](#grafana-dashboards) -- [Audit Log Export](#audit-log-export) - -### Security - -- [Rotating Secrets](#rotating-secrets) -- [User & SSO Setup](#user--sso-setup) -- [Firewall & Network Hardening](#firewall--network-hardening) -- [Security Updates](#security-updates) - -### Troubleshooting - -- [Stack Won't Come Up](#stack-wont-come-up) -- [Database Connection Errors](#database-connection-errors) -- [Jobs Stuck in Pending](#jobs-stuck-in-pending) -- [Receptor / Mesh Issues](#receptor--mesh-issues) -- [Frontend Returns 502](#frontend-returns-502) -- [Disk Filling Up](#disk-filling-up) -- [Reset Admin Password](#reset-admin-password) - -### Reference - -- [Service Map](#service-map) -- [Environment Variables](#environment-variables) -- [File Layout](#file-layout) - ---- - -# INSTALL & FIRST BOOT - -## Prerequisites - -Hardware and software needed before installing Forail. - -**Step by step** - -1. Provision a host (VM or bare metal) with: - - 4 vCPU, 8 GB RAM, 50 GB disk (minimum) - - 8 vCPU, 16 GB RAM, 200 GB disk (recommended for production) -2. Install **Docker** ≥ 24 and **Docker Compose plugin** ≥ 2.20. -3. Open inbound ports **80** and **443** in your firewall. -4. Register a DNS A record pointing at the host (e.g. `forail.example.com`). -5. Make sure the system clock is in sync with NTP. - -**Example — Ubuntu 24.04 fresh box** - -```bash -sudo apt update -sudo apt install -y docker.io docker-compose-plugin -sudo systemctl enable --now docker -sudo usermod -aG docker $USER -newgrp docker -``` - ---- - -## Installation (Docker Compose) - -The standard installation uses the compose file shipped in this repo. - -**Step by step** - -1. Clone or copy `forail-deploy` to the target host: - - ```bash - git clone https://github.com/forail-platform/forail-devops.git /opt/forail - cd /opt/forail - ``` - -2. Copy the example env file and edit it: - - ```bash - cp .env.example .env - $EDITOR .env - ``` - -3. Fill in the **required** variables (see [Environment Variables](#environment-variables)). -4. Pull the images: - - ```bash - docker compose pull - ``` - -5. Bring the stack up: - - ```bash - docker compose up -d - ``` - -6. Tail the init container until it finishes: - - ```bash - docker compose logs -f forail-init - ``` - -7. When you see `==> Init complete.` you can hit `https://` in a browser. - -**Example `.env` for first install** - -```ini -POSTGRES_PASSWORD=Sup3rS3cret-pg -FORAIL_SECRET_KEY=$(openssl rand -hex 25) -FORAIL_BROADCAST_WEBSOCKET_SECRET=$(openssl rand -hex 25) -FORAIL_ADMIN_PASSWORD=ChangeMe!Now -FORAIL_CSRF_TRUSTED_ORIGINS=https://forail.example.com -FORAIL_ALLOWED_HOSTS=forail.example.com -FORAIL_TAG=2026.04.0 -``` - -> **Note:** generate the two random secrets with `openssl rand -hex 25` before pasting them into the file — do not leave the literal `$()` in `.env`. - ---- - -## First-Time Setup - -What to do the first time you log in. - -**Step by step** - -1. Open `https://forail.example.com`. -2. Log in as `admin` / your `FORAIL_ADMIN_PASSWORD`. -3. Click **Settings → License** and upload the license file (if applicable). -4. Click **Settings → System** → set **Base URL** to `https://forail.example.com`. -5. Click **Organizations → Add** and create your first organization (e.g. `Platform`). -6. Click **Users → Add** and create at least one named admin (do not keep using the default `admin`). -7. Log out, log back in as the new admin, and disable the default admin from **Users → admin → Disable**. - -**Example values** - -| Field | Value | -| ----------- | --------------------------- | -| Base URL | `https://forail.example.com` | -| First org | `Platform` | -| Named admin | `krstan` / strong password | - ---- - -## TLS / SSL Setup - -Forail ships with a self-signed cert. Replace it with a real one before exposing the box. - -**Step by step — Let's Encrypt with certbot** - -1. Stop the nginx service so port 80 is free for the ACME challenge: - - ```bash - docker compose stop nginx - ``` - -2. Run certbot in standalone mode: - - ```bash - sudo certbot certonly --standalone -d forail.example.com - ``` - -3. Copy the new cert/key into the nginx mount point: - - ```bash - sudo cp /etc/letsencrypt/live/forail.example.com/fullchain.pem nginx/ssl/forail.crt - sudo cp /etc/letsencrypt/live/forail.example.com/privkey.pem nginx/ssl/forail.key - ``` - -4. Restart nginx: - - ```bash - docker compose up -d nginx - ``` - -5. Verify: - - ```bash - curl -I https://forail.example.com - ``` - -**Example renewal cron** - -```cron -0 3 * * 1 certbot renew --pre-hook "docker compose -f /opt/forail/docker-compose.yml stop nginx" --post-hook "cp /etc/letsencrypt/live/forail.example.com/fullchain.pem /opt/forail/nginx/ssl/forail.crt && cp /etc/letsencrypt/live/forail.example.com/privkey.pem /opt/forail/nginx/ssl/forail.key && docker compose -f /opt/forail/docker-compose.yml up -d nginx" -``` - ---- - -## Initial Admin Hardening - -Things every new install should do on day one. - -**Step by step** - -1. Change `FORAIL_ADMIN_PASSWORD` in `.env` to a strong value, then `docker compose up -d forail-init` to apply. -2. In **Settings → Authentication**, disable any auth backend you don't use (LDAP, SAML, OIDC). -3. In **Settings → System**, enable **Session Cookie Secure** and set **Session Timeout** to `3600`. -4. Create at least two named admin users so no single account is the only way in. -5. Configure a [backup cron](#backup) before importing real data. -6. Configure at least one [Notification](HANDBOOK.md#notifications) channel for failure alerts. -7. Set `FORAIL_ALLOWED_HOSTS` to the hostnames you actually serve. The default is - loopback-only; `*` turns off Django's Host-header check entirely. -8. Leave `FORAIL_TASK_PRIVILEGED` / `FORAIL_TASK_CGROUP` unset unless you need the - podman-in-container job path. A privileged container with the host cgroup - namespace is a trivial escape to the host — if you do enable it, treat that - host as part of your automation trust boundary and keep it off the public - internet. -9. If you authenticate via SAML, re-check the IdP against the signed-assertion and - role-attribute requirements introduced in 2026.07.0 - (`docs/RELEASE_NOTES_v2026.07.0.md`) — unsigned or SHA-1 assertions are now - rejected by default. - ---- - -# DAY-2 OPERATIONS - -## Starting and Stopping the Stack - -**Step by step** - -```bash -# Start everything -docker compose up -d - -# Stop everything (no data loss) -docker compose stop - -# Stop and remove containers (data in volumes survives) -docker compose down - -# Restart a single service -docker compose restart forail-web -``` - -**Example — restart only the task workers after editing settings** - -```bash -docker compose restart forail-task -``` - ---- - -## Inspecting Logs - -**Step by step** - -```bash -# All services, follow -docker compose logs -f - -# One service -docker compose logs -f forail-web - -# Last 200 lines, no follow -docker compose logs --tail=200 forail-task - -# Filter by timestamp -docker compose logs --since=1h forail-web -``` - -**Example — find the most recent error in the web service** - -```bash -docker compose logs --since=24h forail-web | grep -i error | tail -20 -``` - ---- - -## Health Checks - -The stack ships with two healthcheck scripts you can run manually or from monitoring. - -**Step by step** - -```bash -# Web service -docker compose exec forail-web /scripts/healthcheck-web.sh - -# Task service -docker compose exec forail-task /scripts/healthcheck-task.sh -``` - -**Example — uptime check from outside** - -```bash -curl -fsS https://forail.example.com/api/v2/ping/ && echo OK -``` - ---- - -## Backup - -Daily backups are mandatory. Forail ships `scripts/backup.sh` which dumps Postgres and rotates old archives. - -**Step by step** - -1. Run a one-off backup to verify it works: - - ```bash - docker compose exec postgres /scripts/backup.sh - ``` - -2. Inspect the result: - - ```bash - ls -lh /var/lib/awx/backups/ - ``` - -3. Schedule a nightly cron on the host: - - ```cron - 0 2 * * * docker compose -f /opt/forail/docker-compose.yml exec -T postgres /scripts/backup.sh >> /var/log/forail-backup.log 2>&1 - ``` - -4. Copy the backups off-host (S3, rsync, etc.): - - ```cron - 30 2 * * * aws s3 sync /var/lib/awx/backups/ s3://acme-forail-backups/ - ``` - -**Example output** - -``` -==> Starting backup... -==> Backup saved to /var/lib/awx/backups/forail_backup_20260411_020000.sql.gz -==> Removing backups older than 7 days... -==> Backup complete. -``` - -> **Retention** is controlled by `BACKUP_RETENTION_DAYS` (default 7). Override in `.env` if you need longer. - ---- - -## Restore - -`scripts/restore.sh` reads a `.sql.gz` and pipes it back into Postgres. - -**Step by step** - -1. **Stop** the application services so nothing writes to the DB: - - ```bash - docker compose stop forail-web forail-task - ``` - -2. Run the restore (omit the filename to use the most recent backup): - - ```bash - docker compose exec -T postgres /scripts/restore.sh /var/lib/awx/backups/forail_backup_20260411_020000.sql.gz - ``` - -3. Restart the application: - - ```bash - docker compose up -d forail-web forail-task - ``` - -4. Verify in the UI: log in and check **Activity** for the expected history. - -**Example — restore yesterday's backup** - -```bash -docker compose stop forail-web forail-task -docker compose exec -T postgres /scripts/restore.sh -docker compose up -d forail-web forail-task -``` - -> **Warning:** Restore is destructive. The current database is **overwritten** by the dump. Always take a fresh backup _before_ restoring an old one. - ---- - -## Upgrade - -Upgrading is a tag bump + pull + up. - -**Step by step** - -1. Read the [release notes](RELEASE_NOTES_v2026.04.0.md) for breaking changes. -2. Take a backup (see [Backup](#backup)). -3. Edit `.env` and bump `FORAIL_TAG`: - - ```ini - FORAIL_TAG=2026.05.0 - ``` - -4. Pull the new images: - - ```bash - docker compose pull - ``` - -5. Bring the new stack up — `forail-init` runs migrations automatically: - - ```bash - docker compose up -d - ``` - -6. Tail init: - - ```bash - docker compose logs -f forail-init - ``` - -7. Smoke-test: - - ```bash - curl -fsS https://forail.example.com/api/v2/ping/ - ``` - -8. Watch **Jobs** for 10 minutes — make sure new launches work. - -**Example — minor version bump** - -```bash -sed -i 's/^FORAIL_TAG=.*/FORAIL_TAG=2026.04.1/' .env -docker compose pull && docker compose up -d -``` - ---- - -## Rolling Back - -If an upgrade fails, roll back the tag and restore the pre-upgrade backup. - -**Step by step** - -1. Set the previous tag in `.env`: - - ```ini - FORAIL_TAG=2026.04.0 - ``` - -2. Pull and bring up: - - ```bash - docker compose pull - docker compose up -d - ``` - -3. **If migrations were applied** during the failed upgrade, you must also restore the pre-upgrade DB dump (see [Restore](#restore)). -4. Verify and notify users. - -**Example** - -> Upgraded to `2026.05.0` at 02:30, jobs started failing at 02:45 → set `FORAIL_TAG=2026.04.0` → pull → up → restore `forail_backup_20260411_020000.sql.gz` → service restored at 02:55. - ---- - -# SCALING & TOPOLOGY - -## Adding an Execution Node - -Execution nodes run Ansible jobs. Add more when you exhaust capacity. - -**Step by step** - -1. Provision a new host with Docker. -2. On the new host, install Receptor and bring it up as an execution node, pointing at the control node: - - ```bash - docker run -d --name forail-receptor \ - -e RECEPTOR_NODE_TYPE=execution \ - -e RECEPTOR_PEER=tcp://control.forail.example.com:2222 \ - ghcr.io/forail-platform/forail-receptor:2026.04.0 - ``` - -3. In the UI: **Admin → Instances → Add** and register the new node: - - | Field | Value | - | -------------- | -------------- | - | Hostname | `worker-eu-03` | - | Node Type | `execution` | - | Instance Group | `eu-west-pool` | - -4. Wait until the node shows **Ready** in **Topology**. -5. Existing templates pinned to `eu-west-pool` will start scheduling onto it. - ---- - -## Adding a Hop Node - -Hop nodes relay traffic across network boundaries (e.g. DMZ → internal). - -**Step by step** - -1. Provision the hop host inside the boundary. -2. Run a Receptor container with `RECEPTOR_NODE_TYPE=hop`: - - ```bash - docker run -d --name forail-receptor \ - -e RECEPTOR_NODE_TYPE=hop \ - -e RECEPTOR_PEER=tcp://control.forail.example.com:2222 \ - ghcr.io/forail-platform/forail-receptor:2026.04.0 - ``` - -3. In the UI: **Admin → Instances → Add** with **Node Type = hop**. -4. From any execution node behind the hop, set its peer to the hop instead of the control node. -5. Confirm in **Topology** that the hop appears between the control and the workers. - ---- - -## Tuning Capacity - -**Step by step** - -1. **Settings → Jobs**, edit: - - **Max Concurrent Jobs** — global ceiling - - **Max Forks** — Ansible forks per job -2. Per instance: **Admin → Instances → → Capacity Adjustment** slider (0.0–1.0). -3. Save. Changes take effect within 30 seconds. - -**Example** - -> 16 vCPU control node → set capacity to `1.0` (use all). 4 vCPU shared dev box → set to `0.25`. - ---- - -## Switching to Kubernetes - -The `k8s/` folder contains baseline manifests if you outgrow Docker Compose. - -**Step by step** - -1. Read [`k8s/`](https://github.com/forail-platform/forail-devops/tree/main/k8s) — it includes Deployments, Services, ConfigMap, Secret, Ingress. -2. Create a namespace: - - ```bash - kubectl create namespace forail - ``` - -3. Create the secrets (translate your `.env`): - - ```bash - kubectl -n forail create secret generic forail-env --from-env-file=.env - ``` - -4. Apply the manifests: - - ```bash - kubectl -n forail apply -f k8s/ - ``` - -5. Watch the rollout: - - ```bash - kubectl -n forail get pods -w - ``` - -> Migration from compose to k8s is a one-shot: dump Postgres, import into the k8s-managed Postgres (or external RDS). - ---- - -# OBSERVABILITY - -## Enabling OpenTelemetry - -The stack ships with `forail-otel-collector`. You only need to point it at your backend. - -**Step by step** - -1. Edit `otel/collector-config.yaml`. -2. Set the exporter endpoint: - - ```yaml - exporters: - otlphttp: - endpoint: https://otel.example.com - headers: - authorization: "Bearer YOUR_TOKEN" - ``` - -3. Restart the collector: - - ```bash - docker compose restart forail-otel-collector - ``` - -4. In **Settings → Observability**, set: - - **OTLP Endpoint** = `http://forail-otel-collector:4318` - - **Sampling rate** = `0.1` (10%) -5. Save. Within a minute, traces appear in your APM tool. - ---- - -## Grafana Dashboards - -`grafana/` contains pre-built dashboards JSONs. - -**Step by step** - -1. Open Grafana → **Dashboards → Import**. -2. Upload `grafana/forail-overview.json`. -3. Pick your Prometheus datasource. -4. Save. The dashboard shows job throughput, web/task latency, queue depth. - ---- - -## Audit Log Export - -For SOC 2 / ISO 27001 evidence collection. - -**Step by step** - -1. Open **Audit Log** in the UI. -2. Filter by date range (e.g. last quarter). -3. Click **Export → CSV**. -4. Hash and store the CSV alongside your evidence pack: - - ```bash - sha256sum audit-2026Q1.csv > audit-2026Q1.csv.sha256 - ``` - ---- - -# SECURITY - -## Rotating Secrets - -Secrets to rotate periodically: `FORAIL_SECRET_KEY`, `POSTGRES_PASSWORD`, `FORAIL_ADMIN_PASSWORD`, `FORAIL_BROADCAST_WEBSOCKET_SECRET`. - -**Step by step — rotate `FORAIL_SECRET_KEY`** - -1. **Take a backup first.** -2. Generate a new key: - - ```bash - openssl rand -hex 25 - ``` - -3. Edit `.env`, replace `FORAIL_SECRET_KEY=...`. -4. Restart web + task: - - ```bash - docker compose up -d forail-web forail-task - ``` - -5. **All sessions are invalidated** — users must re-login. Encrypted credentials in the DB are unaffected (they use a separate Fernet key). - -> **Never** rotate the database encryption key (used for credential storage) without first re-encrypting all credentials. That procedure is a separate runbook. - ---- - -## User & SSO Setup - -**Step by step — enable OIDC** - -1. Open **Settings → Authentication → OIDC**. -2. Fill in: - - **Provider URL** — `https://login.example.com` - - **Client ID** — `forail-prod` - - **Client Secret** — _(from IdP)_ - - **Redirect URI** — `https://forail.example.com/sso/complete/oidc/` -3. Save → click **Test** to verify discovery. -4. Test login from a private browser window. -5. Map IdP groups to Forail teams under **Settings → Authentication → Group Mapping**. - ---- - -## Firewall & Network Hardening - -**Step by step** - -1. Allow inbound only on **443** (and optionally 80 for HTTP→HTTPS redirect). -2. Restrict SSH to your bastion / admin range. -3. Block Postgres (5432), Redis (6379), Receptor (2222) from the public internet — they should only be reachable from inside the Docker network. -4. If running on cloud, also configure security groups, not just OS firewall. - -**Example — ufw on Ubuntu** - -```bash -sudo ufw default deny incoming -sudo ufw allow 80/tcp -sudo ufw allow 443/tcp -sudo ufw allow from 10.0.0.0/24 to any port 22 -sudo ufw enable -``` - ---- - -## Security Updates - -**Step by step** - -1. Subscribe to the Forail release announcement channel. -2. Run `docker compose pull` weekly to pick up base-image patches when you bump tag. -3. Patch the host OS monthly (`unattended-upgrades` on Debian/Ubuntu). -4. Run scheduled image scans: - - ```bash - trivy image ghcr.io/forail-platform/forail-backend:2026.04.0 - ``` - ---- - -# TROUBLESHOOTING - -## Stack Won't Come Up - -**Step by step** - -1. Check `docker compose ps` — which service is unhealthy? -2. `docker compose logs ` for the failing one. -3. Common causes: - - Missing or wrong values in `.env` → look for `KeyError` / `ImproperlyConfigured`. - - Port 80/443 already in use → `sudo lsof -i :443`. - - Volume permissions → `sudo chown -R 1000:1000 /var/lib/awx`. -4. Fix and `docker compose up -d` again. - ---- - -## Database Connection Errors - -Symptom: web service logs show `could not connect to server: Connection refused`. - -**Step by step** - -1. Is postgres running? `docker compose ps postgres` -2. Logs: `docker compose logs --tail=100 postgres` -3. Can the web container reach it? - - ```bash - docker compose exec forail-web pg_isready -h postgres -U forail - ``` - -4. If postgres is healthy but web cannot connect → check `POSTGRES_PASSWORD` matches in `.env` and the DB volume. -5. If postgres won't start → look for `PANIC` lines (disk full, corrupt WAL). - ---- - -## Jobs Stuck in Pending - -Symptom: jobs sit in _Pending_ and never start. - -**Step by step** - -1. Check capacity: **Admin → Instances** → is total used == total capacity? -2. Check task workers: `docker compose ps forail-task` — running? -3. Check Redis: `docker compose exec redis redis-cli ping` should return `PONG`. -4. Check Receptor: `docker compose exec forail-task receptorctl status`. -5. As a last resort, restart the task service: - - ```bash - docker compose restart forail-task - ``` - ---- - -## Receptor / Mesh Issues - -**Step by step** - -1. Open **Topology** — any red links? -2. From the control node: - - ```bash - docker compose exec forail-task receptorctl status - ``` - -3. From a worker: - - ```bash - docker exec forail-receptor receptorctl status - ``` - -4. Verify TCP reachability between nodes on **2222**. -5. Restart the affected receptor container. - ---- - -## Frontend Returns 502 - -Symptom: browser shows nginx 502 Bad Gateway. - -**Step by step** - -1. `docker compose ps` — is `forail-frontend` healthy? -2. `docker compose logs --tail=50 forail-frontend` -3. `docker compose logs --tail=50 nginx` -4. Common cause: frontend container OOM-killed → bump memory in compose. -5. Restart: `docker compose restart forail-frontend nginx`. - ---- - -## Disk Filling Up - -**Step by step** - -1. `df -h` — which mount? -2. If `/var/lib/docker` → prune unused images: - - ```bash - docker image prune -af --filter "until=168h" - ``` - -3. If the backup directory → lower `BACKUP_RETENTION_DAYS` and rerun backup. -4. If Postgres data dir → check for runaway audit log growth, vacuum: - - ```bash - docker compose exec postgres psql -U forail -d forail -c "VACUUM FULL VERBOSE;" - ``` - ---- - -## Reset Admin Password - -**Step by step** - -1. Exec into the web container: - - ```bash - docker compose exec forail-web bash - ``` - -2. Run the management command: - - ```bash - awx-manage changepassword admin - ``` - -3. Enter the new password twice. -4. Log in via the UI. - -> If `admin` was disabled and you have no other admin user: -> -> ```bash -> docker compose exec forail-web awx-manage createsuperuser -> ``` - ---- - -# REFERENCE - -## Service Map - -| Service | Image | Port (internal) | Purpose | -| ---------------------- | --------------------- | --------------------------- | -------------------------------------------- | -| `postgres` | `postgres:15` | 5432 | Application database | -| `redis` | `redis:7` | 6379 | Cache + Celery broker | -| `forail-init` | `forail-backend` | — | Migrations + initial setup, exits on success | -| `forail-web` | `forail-backend` | 8050 (uWSGI), 8051 (Daphne) | REST API + WebSocket | -| `forail-task` | `forail-backend` | — | Celery workers + dispatcher + ws relay | -| `forail-frontend` | `forail-frontend` | 80 | Static React UI | -| `forail-opa` | `openpolicyagent/opa` | 8181 | Policy-as-Code sidecar | -| `forail-otel-collector` | `otel/collector` | 4317/4318 | OpenTelemetry pipeline | -| `nginx` | `nginx` | 80 / 443 | TLS terminator + edge router | - ---- - -## Environment Variables - -| Variable | Required | Default | Description | -| ---------------------------------- | -------- | -------------------------------------- | -------------------------------------------- | -| `POSTGRES_PASSWORD` | yes | — | DB password | -| `POSTGRES_USER` | no | `forail` | DB user | -| `POSTGRES_DB` | no | `forail` | DB name | -| `FORAIL_SECRET_KEY` | yes | — | Django `SECRET_KEY` (50+ chars) | -| `FORAIL_BROADCAST_WEBSOCKET_SECRET` | yes | — | WS broadcast secret | -| `FORAIL_ADMIN_USER` | no | `admin` | Bootstrap admin username | -| `FORAIL_ADMIN_PASSWORD` | yes | — | Bootstrap admin password | -| `FORAIL_ADMIN_EMAIL` | no | `admin@example.com` | Bootstrap admin email | -| `FORAIL_CSRF_TRUSTED_ORIGINS` | yes | — | Comma-separated `https://...` origins | -| `FORAIL_ALLOWED_HOSTS` | no | `localhost,127.0.0.1` | Django ALLOWED_HOSTS — list your real hostnames | -| `FORAIL_NODE_NAME` | no | `forail-node` | This node's name in mesh | -| `FORAIL_NODE_TYPE` | no | `hybrid` | `control` / `execution` / `hybrid` | -| `FORAIL_BACKEND_IMAGE` | no | `ghcr.io/forail-platform/forail-backend` | Backend image | -| `FORAIL_FRONTEND_IMAGE` | no | `ghcr.io/forail-platform/forail-frontend` | Frontend image | -| `FORAIL_TAG` | no | `2026.07.0` | Image tag (pin a real version, never `latest`) | -| `FORAIL_TASK_PRIVILEGED` | no | `false` | Privileged `forail-task` — needed for the podman job path | -| `FORAIL_TASK_CGROUP` | no | `private` | Set `host` with `FORAIL_TASK_PRIVILEGED=true` for job execution | -| `BACKUP_RETENTION_DAYS` | no | `7` | Days of backups to keep | - ---- - -## File Layout - -``` -/opt/forail/ -├── docker-compose.yml # primary stack definition -├── .env # local secrets — never commit -├── nginx/ -│ ├── nginx.conf -│ └── ssl/ -│ ├── forail.crt -│ └── forail.key -├── settings/ # Django settings overrides mounted into forail-web/task -├── otel/ -│ └── collector-config.yaml -├── grafana/ -│ └── *.json # importable dashboards -├── scripts/ -│ ├── backup.sh -│ ├── restore.sh -│ ├── healthcheck-web.sh -│ ├── healthcheck-task.sh -│ └── init.sh -├── k8s/ # k8s manifests (alternative to compose) -└── /var/lib/awx/ - ├── projects/ # synced project checkouts - ├── public/ # static web assets - └── backups/ # nightly DB dumps -``` - ---- - -_End of administrator handbook._ diff --git a/docs/HANDBOOK.md b/docs/HANDBOOK.md deleted file mode 100644 index 9116cb0..0000000 --- a/docs/HANDBOOK.md +++ /dev/null @@ -1,1362 +0,0 @@ -# Forail Platform — User Handbook - -A step-by-step guide for everyday use of the Forail Platform UI. Each section follows the left sidebar of the application. Click any item in the table of contents to jump straight to it. - ---- - -## Table of Contents - -### Views - -- [Dashboard](#dashboard) -- [Jobs](#jobs) -- [Schedules](#schedules) -- [Activity](#activity) -- [Audit Log](#audit-log) -- [Analytics](#analytics) - -### Automation - -- [Event Rules](#event-rules) -- [Event Logs](#event-logs) -- [Outbound Webhooks](#outbound-webhooks) - -### Self-Service - -- [Service Portal](#service-portal) -- [My Requests](#my-requests) -- [Approvals](#approvals) -- [Catalog Admin](#catalog-admin) - -### Tenancy - -- [Tenants](#tenants) -- [Quota Events](#quota-events) - -### Compliance - -- [Drift Detections](#drift-detections) -- [Drift Alerts](#drift-alerts) -- [Alert Rules](#alert-rules) -- [Fact Snapshots](#fact-snapshots) -- [Policies](#policies) -- [Policy Decisions](#policy-decisions) -- [Scanners](#scanners) -- [Scan Results](#scan-results) -- [Observability](#observability) - -### Resources - -- [Templates](#templates) -- [Inventories](#inventories) -- [Hosts](#hosts) -- [Projects](#projects) -- [Credentials](#credentials) - -### Access - -- [Organizations](#organizations) -- [Users](#users) -- [Teams](#teams) - -### Admin - -- [Instances](#instances) -- [Instance Groups](#instance-groups) -- [Execution Environments](#execution-environments) -- [Notifications](#notifications) -- [Topology](#topology) -- [Settings](#settings) - ---- - -# VIEWS - -## Dashboard - -![Dashboard](img/handbook/dashboard.png) - -**What each marker means:** - -1. **Page heading** — confirms you are on the Dashboard. -2. **Getting Started banner** — links to the wizard that walks you through the first install (org, project, inventory, credential, template). - -The Dashboard is the landing page after login. It shows the overall health of the platform: recent jobs, success/failure rates, active hosts, and quick links. - -**Step by step** - -1. Log in to Forail. -2. You will land on the Dashboard automatically. If not, click **Dashboard** in the left sidebar. -3. Read the four top tiles: _Total Hosts_, _Total Jobs_, _Active Schedules_, _Recent Failures_. -4. Use the time-range selector (top-right) to switch between **Last 24h**, **Last 7d**, **Last 30d**. -5. Click any tile to drill into the matching list view. - -**Example** - -> Open Dashboard → set range to **Last 7d** → click the _Recent Failures_ tile to see all failed jobs of the past week. - ---- - -## Jobs - -![Jobs](img/handbook/jobs.png) - -**What each marker means:** - -1. **Search box** — filter the jobs list by name or id, e.g. type `Deploy` to find every run of the Deploy Webapp template. -2. **Refresh** — re-fetch the list (auto-refresh runs on a timer too). - -Jobs are individual runs of a Job Template. Use this view to launch, monitor, and inspect playbook executions. - -**Step by step — launch a job** - -1. Click **Jobs** in the sidebar. -2. Click the **Launch** button (top-right). -3. Pick a **Job Template** from the dropdown. -4. Fill in any required survey variables. -5. Click **Launch**. -6. The job opens in the live output view — watch the play-by-play log. - -**Example survey input** - -```yaml -target_env: staging -package_version: 1.4.2 -restart_service: true -``` - -**Step by step — inspect a finished job** - -1. Click any row in the Jobs list. -2. Read the **Details** tab (status, duration, executed by). -3. Open the **Output** tab for the full Ansible log. -4. Open **Hosts** to see per-host success/failure. - ---- - -## Schedules - -![Schedules](img/handbook/schedules.png) - -**What each marker means:** - -1. **Create Schedule** — opens the form below. - -Schedules trigger Job Templates automatically on a cron-like cadence. - -**Create Schedule** - -![Create Schedule](img/handbook/schedules_new.png) - -**What each marker means:** - -1. **Name** — short, descriptive id, e.g. `nightly-db-backup`. -2. **Template** — the Job Template to launch on the schedule, e.g. `Backup PostgreSQL`. -3. **Frequency** — None / Minute / Hour / Day / Week / Month — pick the cadence. -4. **Start Date/Time** — when the first run should fire, e.g. `2026-04-12 02:00`. -5. **Create Schedule** — submits the form. - -**Step by step** - -1. Click **Schedules** → **Add**. -2. Fill in: - - **Name** — short, descriptive - - **Job Template** — what to run - - **Start Date / Time** - - **Frequency** — None / Minute / Hour / Day / Week / Month - - **Repeat Frequency** — every _N_ of the chosen unit -3. Click **Save**. -4. Toggle the **Enabled** switch to activate. - -**Example** - -| Field | Value | -| ------------ | ------------------- | -| Name | `nightly-db-backup` | -| Job Template | `Backup PostgreSQL` | -| Start Date | `2026-04-12 02:00` | -| Frequency | Day | -| Repeat | every `1` day | - ---- - -## Activity - -![Activity](img/handbook/activity.png) - -**What each marker means:** - -1. **Page heading** — Activity is read-only; use the column headers and filters to narrow the feed. - -A chronological feed of platform actions: who did what and when. - -**Step by step** - -1. Click **Activity**. -2. Filter by **User**, **Object Type**, or **Date Range**. -3. Click any row to open the actor and the affected object side by side. - -**Example** - -> Filter: User = `alice`, Date Range = _Today_ → see every change Alice made today. - ---- - -## Audit Log - -![Audit Log](img/handbook/audit_log.png) - -**What each marker means:** - -1. **Page heading** — use the column header filters to narrow by event type, severity, actor, or date range. - -Tamper-evident security log used for compliance reporting (SOC 2, ISO 27001). - -**Step by step** - -1. Click **Audit Log**. -2. Use filters: **Event Type** (`login`, `permission_change`, `credential_access`, …), **Severity**, **Actor**. -3. Click **Export → CSV** to download for audit. - -**Example** - -> Filter: Event Type = `credential_access`, Date Range = _Last 30d_ → export CSV → attach to audit ticket. - ---- - -## Analytics - -![Analytics](img/handbook/analytics.png) - -**What each marker means:** - -1. **Page heading** — switch tabs across the top to see Overview / Job Trends / Host Health / Top Failures. - -Visual KPIs across the whole platform: job throughput, MTTR, top failing templates. - -**Step by step** - -1. Click **Analytics**. -2. Pick a tab: **Overview**, **Job Trends**, **Host Health**, **Top Failures**. -3. Hover any chart point for the exact number. -4. Click **Download Report → PDF** for a printable snapshot. - -**Example** - -> Open _Top Failures_ → identify the template with most failures → click it → land on its Jobs list filtered to failed runs. - ---- - -# AUTOMATION - -## Event Rules - -![Event Rules](img/handbook/event_rules.png) - -**What each marker means:** - -1. **Create Event Rule** — opens the form below. - -Event Rules listen for incoming events (webhooks, alerts, message bus) and launch a Job Template when conditions match. - -**Create Event Rule** - -![Create Event Rule](img/handbook/event_rules_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `restart-on-prometheus-crit`. -2. **Source Type** — webhook / kafka / alertmanager / etc. -3. **Webhook Path** — URL suffix the source will hit, e.g. `/prom-crit`. -4. **Add Condition** — add JSONPath / regex match rules. -5. **Add Action** — what to launch when conditions match (Job Template, Workflow). -6. **Create Event Rule** — submits the form. - -**Step by step** - -1. Click **Event Rules** → **Create Rule**. -2. Fill in: - - **Name** - - **Source** (webhook, kafka, prometheus alertmanager, …) - - **Match condition** (JSONPath / regex) - - **Action** — Job Template to launch - - **Variables to forward** -3. Click **Save** → toggle **Enabled**. - -**Example** - -```yaml -name: restart-on-prometheus-crit -source: alertmanager -match: "$.alerts[?(@.labels.severity=='critical')]" -action_template: Restart Failed Service -forward_vars: - host: "{{ alert.labels.instance }}" - service: "{{ alert.labels.service }}" -``` - ---- - -## Event Logs - -![Event Logs](img/handbook/event_logs.png) - -**What each marker means:** - -1. **Page heading** — read-only feed of received events; click a row to see the raw payload. - -Read-only history of every event the platform received and what rule (if any) consumed it. - -**Step by step** - -1. Click **Event Logs**. -2. Filter by **Source**, **Status** (`matched`, `unmatched`, `error`), **Date**. -3. Click a row to see the raw event payload and which rule fired. - -**Example** - -> Filter: Status = `unmatched` → find events nothing reacted to → write a new Event Rule for them. - ---- - -## Outbound Webhooks - -![Outbound Webhooks](img/handbook/outbound_webhooks.png) - -**What each marker means:** - -1. **Create Outbound Webhook** — opens the form below. - -Send platform events to external systems (Slack, PagerDuty, ServiceNow). - -**Create Outbound Webhook** - -![Create Outbound Webhook](img/handbook/outbound_webhooks_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `slack-failed-jobs`. -2. **Target URL** — full https URL of the receiving endpoint, e.g. `https://hooks.slack.com/services/T0/B0/XYZ`. -3. **Trigger toggles** (Job Started / Succeeded / Failed / etc.) — pick which events fire the webhook. -4. **Create Outbound Webhook** — submits the form. - -**Step by step** - -1. Click **Outbound Webhooks** → **Add**. -2. Fill in: - - **Name** - - **Target URL** - - **HTTP Method** (POST / PUT) - - **Headers** (auth tokens) - - **Trigger Events** (job-failed, job-success, drift-detected, …) - - **Payload Template** (Jinja2) -3. Click **Test** → **Save**. - -**Example** - -```yaml -name: slack-failed-jobs -url: https://hooks.slack.com/services/T0/B0/XYZ -method: POST -headers: - Content-Type: application/json -trigger: job_failed -payload: | - {"text": ":x: Job *{{ job.name }}* failed on {{ job.host }}"} -``` - ---- - -# SELF-SERVICE - -## Service Portal - -![Service Portal](img/handbook/service_portal.png) - -**What each marker means:** - -1. **Page heading** — end-user catalog. Browse tiles or use search. - -End-user catalog: lets non-admins request pre-approved automations without touching templates directly. - -**Step by step (as end user)** - -1. Click **Service Portal**. -2. Browse the catalog tiles or use the search bar. -3. Click a tile (e.g., _New Dev VM_). -4. Fill in the request form. -5. Click **Submit**. - -**Example form** - -| Field | Value | -| ----------- | --------------------- | -| VM name | `dev-alice-01` | -| OS | Ubuntu 24.04 | -| Size | small (2 vCPU / 4 GB) | -| Owner email | `alice@example.com` | - ---- - -## My Requests - -![My Requests](img/handbook/my_requests.png) - -**What each marker means:** - -1. **Page heading** — your submitted requests with their current status. - -Tracks the status of requests you submitted from the Service Portal. - -**Step by step** - -1. Click **My Requests**. -2. Read the status column: `pending`, `approved`, `rejected`, `running`, `completed`, `failed`. -3. Click any row → **Output** tab to read the job log. - -**Example** - -> Submit a _New Dev VM_ request → open **My Requests** → wait for `approved` → wait for `completed` → copy the VM IP from output. - ---- - -## Approvals - -![Approvals](img/handbook/approvals.png) - -**What each marker means:** - -1. **Page heading** — pending requests waiting for an approver. Click a row to read details and Approve / Reject. - -Queue of items waiting for an approver. Items appear here when a Service Portal entry is configured to require approval. - -**Step by step (as approver)** - -1. Click **Approvals**. -2. Click a pending item. -3. Read the request details and the requester comment. -4. Click **Approve** or **Reject**. If rejecting, provide a reason. - -**Example** - -> Open the queue → click `Provision Production DB` → review parameters → click **Approve** → job auto-launches. - ---- - -## Catalog Admin - -![Catalog Admin](img/handbook/catalog_admin.png) - -**What each marker means:** - -1. **Add Item** — opens the form below. - -Where admins create and manage Service Portal entries. - -**Create Catalog Item** - -![Create Catalog Item](img/handbook/catalog_admin_new.png) - -**What each marker means:** - -1. **Name** — what end users will see in the Service Portal, e.g. `New Dev VM`. -2. **Category** — groups items in the portal, e.g. `Compute`. -3. **Underlying template** — the Job Template that will run when a request is approved. -4. **Requires approval** — toggle on to route requests through Approvals; off to auto-launch. -5. **Save** — publishes the catalog item. - -**Step by step** - -1. Click **Catalog Admin** → **Add Item**. -2. Fill in: - - **Title** - - **Description** - - **Category** - - **Underlying Job Template** - - **Survey form** - - **Approval required?** (yes / no — pick approver group) -3. Click **Publish**. - -**Example** - -| Field | Value | -| ----------------- | -------------- | -| Title | New Dev VM | -| Category | Compute | -| Template | `Provision VM` | -| Approval required | No | - ---- - -# TENANCY - -## Tenants - -![Tenants](img/handbook/tenants.png) - -**What each marker means:** - -1. **Create Tenant** — opens the form below. - -A tenant is an isolated workspace (org-of-orgs) with its own data, quotas, and members. Used for multi-tenant deployments. - -**Create Tenant** - -![Create Tenant](img/handbook/tenants_new.png) - -**What each marker means:** - -1. **Name** — slug-friendly id, e.g. `acme-corp`. -2. **Admin username** — bootstrap admin login for the tenant, e.g. `acme-admin`. -3. **Admin password** — strong password for the bootstrap admin. -4. **Max concurrent jobs** — quota cap for parallel jobs, e.g. `20`. -5. **Save** — provisions the tenant atomically (org + admin + team + quotas). - -**Step by step** - -1. Click **Tenants** → **Create Tenant**. -2. Fill in: - - **Name** — slug-friendly - - **Display Name** - - **Quota** — max jobs/day, max hosts, max storage - - **Owner user** -3. Click **Create**. - -**Example** - -```yaml -name: acme-corp -display_name: ACME Corp -quota: - jobs_per_day: 500 - hosts: 200 - storage_gb: 50 -owner: acme-admin@example.com -``` - ---- - -## Quota Events - -![Quota Events](img/handbook/quota_events.png) - -**What each marker means:** - -1. **Page heading** — read-only audit feed of quota changes and quota-exceeded events. - -Audit feed of quota changes and quota-exceeded events per tenant. - -**Step by step** - -1. Click **Quota Events**. -2. Filter by **Tenant** and **Event Type** (`exceeded`, `raised`, `lowered`). -3. Click a row to see the resource that hit the limit. - -**Example** - -> Filter: Tenant = `acme-corp`, Event = `exceeded` → see when ACME hit their daily job ceiling → contact them or raise the quota. - ---- - -# COMPLIANCE - -## Drift Detections - -![Drift Detections](img/handbook/drift_detections.png) - -**What each marker means:** - -1. **Page heading** — list of drift detection runs. Click any row for the per-host findings. - -Compares the current state of a host against a known-good fact baseline. - -**Step by step** - -1. Click **Drift Detections** → **Run Detection**. -2. Pick: - - **Inventory** - - **Baseline snapshot** - - **Hosts** (or _All_) -3. Click **Run**. -4. Open the result row when finished. - -**Example** - -> Inventory: `prod-web` · Baseline: `baseline-2026-04-01` · Hosts: All → Run. - ---- - -## Drift Alerts - -![Drift Alerts](img/handbook/drift_alerts.png) - -**What each marker means:** - -1. **Page heading** — open alerts opened by drift detections. Use status filter to narrow by `open` / `acked` / `resolved`. - -Alerts opened automatically when a drift detection finds a mismatch. - -**Step by step** - -1. Click **Drift Alerts**. -2. Filter by **Severity** (`info`, `warn`, `crit`) or **Status** (`open`, `acked`, `resolved`). -3. Open an alert → click **Acknowledge** or **Resolve**. - -**Example** - -> Filter: Severity = `crit`, Status = `open` → ack each one → assign to oncall. - ---- - -## Alert Rules - -![Alert Rules](img/handbook/alert_rules.png) - -**What each marker means:** - -1. **Create Alert Rule** — opens the form below. - -Rules that decide which drift findings become alerts and at which severity. - -**Create Alert Rule** - -![Create Alert Rule](img/handbook/alert_rules_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `ssh-port-must-be-22`. -2. **Host Filter** — fnmatch pattern (e.g. `prod-*`) to scope the rule. -3. **Minimum Severity** — info / warn / crit — minimum severity that opens an alert. -4. **Create Alert Rule** — submits the form. - -**Step by step** - -1. Click **Alert Rules** → **Add Rule**. -2. Fill in: - - **Name** - - **Match** (fact key + expected value or regex) - - **Severity** - - **Notify** (which Notification target) -3. **Save**. - -**Example** - -```yaml -name: ssh-port-must-be-22 -match: - fact: ansible_facts.ssh.port - not_equal: 22 -severity: crit -notify: slack-secops -``` - ---- - -## Fact Snapshots - -![Fact Snapshots](img/handbook/fact_snapshots.png) - -**What each marker means:** - -1. **Page heading** — list of point-in-time fact captures used as drift baselines. - -A point-in-time capture of host facts. Used as drift baselines. - -**Step by step** - -1. Click **Fact Snapshots** → **Create Snapshot**. -2. Pick **Inventory** and **Hosts**. -3. Add a **Label** (the snapshot’s name). -4. Click **Capture**. - -**Example** - -| Field | Value | -| --------- | --------------------- | -| Inventory | `prod-web` | -| Hosts | All | -| Label | `baseline-2026-04-01` | - ---- - -## Policies - -![Policies](img/handbook/policies.png) - -**What each marker means:** - -1. **Create Policy** — opens the form below. - -Policy-as-Code rules (OPA / Rego) that gate job execution and approvals. - -**Create Policy** - -![Create Policy](img/handbook/policies_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `dba-only-drop-db`. -2. **Enforcement** — `enforce` (block on deny) or `warn` (record only). -3. **Rego module** — the OPA Rego source that returns deny / allow. -4. **Save** — validates and stores the policy. - -**Step by step** - -1. Click **Policies** → **Create Policy**. -2. Fill in: - - **Name** - - **Scope** (`pre-job`, `pre-approval`, `pre-deploy`) - - **Rego source** -3. Click **Validate** → **Save** → **Enable**. - -**Example Rego** - -```rego -package forail.prejob - -deny[msg] { - input.template.name == "Drop Database" - input.user.team != "dba" - msg := "only DBAs can drop databases" -} -``` - ---- - -## Policy Decisions - -![Policy Decisions](img/handbook/policy_decisions.png) - -**What each marker means:** - -1. **Page heading** — read-only history of every OPA evaluation. Filter by Verdict / User / Policy. - -History of every policy evaluation: who triggered it, what the input was, and the verdict. - -**Step by step** - -1. Click **Policy Decisions**. -2. Filter by **Policy**, **Verdict** (`allow` / `deny`), **User**. -3. Click a row to see the full input JSON and the rule that fired. - -**Example** - -> Filter: Verdict = `deny`, Last 24h → see who got blocked yesterday and why. - ---- - -## Scanners - -![Scanners](img/handbook/scanners.png) - -**What each marker means:** - -1. **Create Scanner** — opens the form below. - -Configured IaC / image / dependency scanners (Trivy, Checkov, …). - -**Create Scanner** - -![Create Scanner](img/handbook/scanners_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `trivy-prod`. -2. **Tool** — Trivy / Checkov / ansible-lint / pip-audit. -3. **Severity threshold** — minimum severity that fails a job, e.g. `high`. -4. **Save** — registers the scanner. - -**Step by step** - -1. Click **Scanners** → **Add Scanner**. -2. Fill in: - - **Type** (Trivy / Checkov / Custom) - - **Target** (Project, Inventory, or Image registry) - - **Schedule** (cron) -3. **Save** → click **Run Now** to test. - -**Example** - -| Field | Value | -| -------- | -------------------------- | -| Type | Trivy | -| Target | Project: `infra-terraform` | -| Schedule | `0 3 * * *` | - ---- - -## Scan Results - -![Scan Results](img/handbook/scan_results.png) - -**What each marker means:** - -1. **Page heading** — read-only findings from configured scanners. Filter by severity / scanner / date. - -Findings from the configured scanners. - -**Step by step** - -1. Click **Scan Results**. -2. Filter by **Severity** (`crit`, `high`, `med`, `low`), **Scanner**, **Date**. -3. Click a finding for the file path, line number, and remediation hint. -4. Click **Mark as Fixed** after remediating. - -**Example** - -> Filter: Severity = `crit`, Scanner = `trivy` → triage every critical CVE. - ---- - -## Observability - -![Observability](img/handbook/observability.png) - -**What each marker means:** - -1. **Page heading** — live OpenTelemetry view (traces, metrics, logs). Pick a tab and search. - -Live OpenTelemetry view of the platform itself: traces, metrics, logs. - -**Step by step** - -1. Click **Observability**. -2. Pick a tab: **Traces**, **Metrics**, **Logs**. -3. Use the search bar (Trace ID / Service / Metric name). -4. Click **Open in Grafana** for the deep view. - -**Example** - -> Tab: Traces → search `service=forail-web` → click the slowest span → open in Grafana. - ---- - -# RESOURCES - -## Templates - -![Templates](img/handbook/templates.png) - -**What each marker means:** - -1. **Search box** — filter templates by name, e.g. type `Deploy`. -2. **+ Job Template** — opens the create form below. - -Job Templates wrap a project + playbook + inventory + credentials + survey into a launchable unit. - -**Create Job Template** - -![Create Job Template](img/handbook/templates_new.png) - -**What each marker means:** - -1. **Name** — short, descriptive id, e.g. `Deploy Webapp`. -2. **Inventory** — pick the target hosts, e.g. `Production Web`. -3. **Project** — the Git project that contains the playbook, e.g. `Demo Project`. -4. **Playbook** — the YAML file inside the project to run, e.g. `deploy.yml`. -5. **Limit** — host pattern to scope the run, e.g. `webservers` or `webservers:!web03`. -6. **Create Template** — submits the form. - -**Step by step** - -1. Click **Templates** → **Add Job Template**. -2. Fill in: - - **Name** - - **Job Type** (`run` / `check`) - - **Inventory** - - **Project** - - **Playbook** - - **Credentials** - - **Limit** (host pattern, optional) - - **Variables** (YAML / JSON) -3. **Save** → click **Launch** to test. - -**Example** - -```yaml -name: deploy-webapp -job_type: run -inventory: prod-web -project: webapp-iac -playbook: deploy.yml -credentials: - - ssh-prod -limit: webservers -variables: - app_version: 2.3.1 -``` - ---- - -## Inventories - -![Inventories](img/handbook/inventories.png) - -**What each marker means:** - -1. **Search box** — filter the inventories list. -2. **Create Inventory** — opens the form below. - -Logical group of hosts. May be static, sourced from a file, or synced from a cloud (AWS / Azure / GCP). - -**Create Inventory** - -![Create Inventory](img/handbook/inventories_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `Production Web`. -2. **Description** — free-text purpose, e.g. `Web tier — production hosts`. -3. **Organization** — which org owns this inventory, e.g. `Default`. -4. **Inventory Type** — `Standard` for a static list, `Smart` for a dynamic host filter. -5. **Create Inventory** — submits the form. - -**Step by step — create static inventory** - -1. Click **Inventories** → **Add → Inventory**. -2. Fill in **Name**, **Organization**, **Description**. -3. Click **Save**. -4. Open the **Hosts** tab → **Add** to attach hosts. -5. Open the **Groups** tab → **Add** to organize hosts. - -**Example** - -| Field | Value | -| ------------ | --------------------------------- | -| Name | `prod-web` | -| Organization | Platform | -| Hosts | `web01`, `web02`, `web03` | -| Group | `webservers` (contains all three) | - ---- - -## Hosts - -![Hosts](img/handbook/hosts.png) - -**What each marker means:** - -1. **Page heading** — flat list of all hosts across all inventories. Click a host to see its facts and group memberships. - -Individual machines (or endpoints) that belong to one or more inventories. - -**Step by step** - -1. Click **Hosts** → **Add**. -2. Fill in: - - **Hostname / IP** - - **Inventory** - - **Variables** (YAML — host-specific overrides) -3. **Save**. - -**Example** - -```yaml -name: web01.example.com -inventory: prod-web -variables: - ansible_user: deploy - http_port: 8080 -``` - ---- - -## Projects - -![Projects](img/handbook/projects.png) - -**What each marker means:** - -1. **Search box** — filter projects by name. -2. **Create Project** — opens the form below. - -A Project is a Git checkout containing playbooks, roles, and collections. - -**Create Project** - -![Create Project](img/handbook/projects_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `webapp-iac`. -2. **SCM Type** — `git` (GitHub / GitLab / Bitbucket / self-hosted). -3. **SCM URL** — clone URL, e.g. `git@github.com:acme/webapp-iac.git`. -4. **SCM Branch** — branch / tag / sha to check out, e.g. `main`. -5. **Create Project** — submits the form and triggers the first sync. - -**Step by step** - -1. Click **Projects** → **Add**. -2. Fill in: - - **Name** - - **Organization** - - **SCM Type** (`git`) - - **SCM URL** - - **SCM Branch** - - **SCM Credential** - - **Update Options** (clean, delete on update, update on launch) -3. **Save**. The first sync starts automatically. - -**Example** - -| Field | Value | -| ---------- | ------------------------------------ | -| Name | webapp-iac | -| SCM Type | git | -| URL | `git@github.com:acme/webapp-iac.git` | -| Branch | main | -| Credential | `github-deploy-key` | - ---- - -## Credentials - -![Credentials](img/handbook/credentials.png) - -**What each marker means:** - -1. **Search box** — filter credentials. -2. **Create Credential** — opens the form below. - -Encrypted secret store: SSH keys, passwords, cloud tokens, vault keys. - -**Create Credential** - -![Create Credential](img/handbook/credentials_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `ssh-prod`. -2. **Credential Type** — Machine / SCM / Vault / AWS / Azure / GCP / etc. -3. **Organization** — which org owns this credential. -4. **Create Credential** — submits the form (encrypted at rest). - -**Step by step** - -1. Click **Credentials** → **Add**. -2. Pick a **Credential Type** (Machine, Source Control, Vault, AWS, Azure, GCP, …). -3. Fill in the required fields (varies by type). -4. **Save** — the secret is encrypted at rest. - -**Example — SSH key for production** - -| Field | Value | -| -------------------- | ------------- | -| Name | `ssh-prod` | -| Type | Machine | -| Username | `deploy` | -| SSH Private Key | _(paste key)_ | -| Privilege Escalation | sudo | - ---- - -# ACCESS - -## Organizations - -![Organizations](img/handbook/organizations.png) - -**What each marker means:** - -1. **Create Organization** — opens the form below. - -Top-level container for users, teams, projects, inventories, templates. - -**Create Organization** - -![Create Organization](img/handbook/organizations_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `Platform`. -2. **Description** — purpose of the org, e.g. `Core platform engineering org`. -3. **Max Hosts** — soft cap for the org, leave 0 for unlimited. -4. **Create Organization** — submits the form. - -**Step by step** - -1. Click **Organizations** → **Add**. -2. Fill in **Name** and **Description**. -3. **Save**. -4. Open the org → **Access** tab → add users / teams. - -**Example** - -> Name: `Platform` · Description: `Core platform engineering org`. - ---- - -## Users - -![Users](img/handbook/users.png) - -**What each marker means:** - -1. **Create User** — opens the form below. - -Local and SSO-mapped users. - -**Create User** - -![Create User](img/handbook/users_new.png) - -**What each marker means:** - -1. **Username** — login id, e.g. `alice`. -2. **Email** — contact address, e.g. `alice@example.com`. -3. **Password** — strong initial password (user can change later). -4. **Superuser toggle** — grants full admin rights; leave off for normal users. -5. **Create User** — submits the form. - -**Step by step — create a local user** - -1. Click **Users** → **Add**. -2. Fill in **Username**, **Email**, **First / Last name**, **Password**. -3. Pick **User Type** (Normal / System Auditor / System Admin). -4. **Save**. -5. Open the user → **Permissions** → grant role on objects. - -**Example** - -| Field | Value | -| --------- | ------------------------------- | -| Username | `alice` | -| Email | `alice@example.com` | -| User Type | Normal | -| Role | `Project Admin` on `webapp-iac` | - ---- - -## Teams - -![Teams](img/handbook/teams.png) - -**What each marker means:** - -1. **Create Team** — opens the form below. - -A Team groups users so permissions can be granted in bulk. - -**Create Team** - -![Create Team](img/handbook/teams_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `webapp-devs`. -2. **Description** — purpose, e.g. `Engineers who deploy the webapp`. -3. **Organization** — parent org for the team. -4. **Create Team** — submits the form. - -**Step by step** - -1. Click **Teams** → **Add**. -2. Fill in **Name**, **Organization**, **Description**. -3. **Save**. -4. Open the team → **Access** tab → add members. -5. Grant the team a role on a project / template / inventory. - -**Example** - -> Team: `webapp-devs` · Org: `Platform` · Members: `alice`, `bob` · Role: `Execute` on template `deploy-webapp`. - ---- - -# ADMIN - -## Instances - -![Instances](img/handbook/instances.png) - -**What each marker means:** - -1. **Page heading** — list of cluster nodes. Click any row to enable / disable a node or adjust its capacity. - -Physical or virtual nodes that run jobs (control plane + execution plane). - -**Step by step** - -1. Click **Instances**. -2. Read the table: **Hostname**, **Node Type**, **Capacity**, **Used**, **Status**. -3. Click an instance → **Disable** (drain) or **Enable**. - -**Example** - -> Drain `worker-03` before reboot → mark Disabled → wait for jobs to drain → reboot → mark Enabled. - ---- - -## Instance Groups - -![Instance Groups](img/handbook/instance_groups.png) - -**What each marker means:** - -1. **Page heading** — logical pools of instances. Click a group to manage members and policy. - -Logical pools of instances. Templates can be pinned to a group (e.g., `gpu-pool`, `eu-west-pool`). - -**Step by step** - -1. Click **Instance Groups** → **Add**. -2. Fill in **Name** and **Policy** (min instances, max idle). -3. **Save**. -4. Open the group → **Instances** tab → attach instances. -5. Open a Job Template → set its **Instance Group** to this one. - -**Example** - -| Field | Value | -| ------------------------ | ------------------------------ | -| Name | `eu-west-pool` | -| Policy instances minimum | 2 | -| Members | `worker-eu-01`, `worker-eu-02` | - ---- - -## Execution Environments - -![Execution Environments](img/handbook/execution_environments.png) - -**What each marker means:** - -1. **Page heading** — container images that hold the runtime for jobs. Click any row to edit pull policy or registry credential. - -Container images that hold the runtime (Python, collections, binaries) used to run jobs. - -**Step by step** - -1. Click **Execution Env** → **Add**. -2. Fill in: - - **Name** - - **Image** (`registry/repo:tag`) - - **Pull Policy** (`always` / `missing` / `never`) - - **Registry credential** -3. **Save**. -4. Reference it from a Job Template’s **Execution Environment** field. - -**Example** - -```yaml -name: ee-aws-2026.04 -image: registry.example.com/forail/ee-aws:2026.04 -pull: always -credential: harbor-pull -``` - ---- - -## Notifications - -![Notifications](img/handbook/notifications.png) - -**What each marker means:** - -1. **Create** — opens the form below. - -Channels Forail can send messages to (email, Slack, PagerDuty, MS Teams, webhooks). - -**Create Notification Template** - -![Create Notification Template](img/handbook/notifications_new.png) - -**What each marker means:** - -1. **Name** — short id, e.g. `slack-secops`. -2. **Notification Type** — Email / Slack / PagerDuty / Webhook / MS Teams / etc. -3. **Create** — submits the form (next step asks for type-specific fields like Slack token + channel). - -**Step by step** - -1. Click **Notifications** → **Add**. -2. Pick **Type** (Email / Slack / PagerDuty / Webhook / …). -3. Fill in the type-specific fields. -4. Click **Test** → **Save**. -5. Attach the notification to a Job Template, Project, or Workflow on its **Notifications** tab. - -**Example — Slack** - -| Field | Value | -| ------- | ---------------- | -| Name | `slack-secops` | -| Type | Slack | -| Token | `xoxb-…` | -| Channel | `#secops-alerts` | - ---- - -## Topology - -![Topology](img/handbook/topology.png) - -**What each marker means:** - -1. **Page heading** — visual mesh map of the cluster. Drag to pan, scroll to zoom, click nodes / links for detail. - -Visual map of the cluster: control nodes, hop nodes, execution nodes, mesh links. - -**Step by step** - -1. Click **Topology**. -2. Drag to pan, scroll to zoom. -3. Click any node for details (capacity, version, peers). -4. Click any link to see latency and link status. - -**Example** - -> Suspect a mesh issue → open Topology → spot a red link between `hop-eu` and `worker-eu-02` → click it → read the error. - ---- - -## Settings - -![Settings](img/handbook/settings.png) - -**What each marker means:** - -1. **Page heading** — global platform configuration grouped by category (Authentication, System, Jobs, UI, Logging, License). - -Global platform configuration: auth, system, jobs, UI, logging, license. - -**Step by step** - -1. Click **Settings**. -2. Pick a category (Authentication, System, Jobs, UI, Logging, License). -3. Edit the field, click **Save**. -4. Some changes (e.g., auth) require a service reload — banner will indicate. - -**Example — bump max concurrent jobs** - -> Settings → Jobs → set **Max Concurrent Jobs** to `200` → Save. - ---- - -# Appendix — Common Workflows End-to-End - -### A) Provision a new dev VM (self-service path) - -1. [Catalog Admin](#catalog-admin) — admin publishes the _New Dev VM_ item. -2. End user opens [Service Portal](#service-portal) → submits the form. -3. Item appears in [Approvals](#approvals) → approver approves. -4. Forail launches the [Templates](#templates) job behind the item. -5. End user watches it under [My Requests](#my-requests) and [Jobs](#jobs). - -### B) Catch and fix configuration drift - -1. Take a [Fact Snapshot](#fact-snapshots) of the production inventory. -2. Define an [Alert Rule](#alert-rules). -3. Run [Drift Detections](#drift-detections) on a schedule. -4. Mismatches open [Drift Alerts](#drift-alerts). -5. Fix via a [Templates](#templates) remediation job. - -### C) Deploy a new release - -1. Update playbooks in the Git repo behind your [Project](#projects). -2. Click **Sync** on the project. -3. Open the [Templates](#templates) deploy job → **Launch** with the new version variable. -4. Watch in [Jobs](#jobs). -5. Confirm in [Analytics](#analytics) → success rate stays green. - ---- - -_End of handbook._ diff --git a/docs/RELEASE_NOTES_v2026.03.0.md b/docs/RELEASE_NOTES_v2026.03.0.md deleted file mode 100644 index a71d108..0000000 --- a/docs/RELEASE_NOTES_v2026.03.0.md +++ /dev/null @@ -1,144 +0,0 @@ -# Forail 2026.03.0 — Release Notes - -**Release date:** 2026-03-11 -**Based on:** AWX 24.6.1 -**License:** Apache License 2.0 - ---- - -## Overview - -Forail 2026.03.0 is the first official release — a complete modernization of AWX 24.6.1 across 9 development phases spanning 25 weeks. Every layer has been upgraded: dependencies, backend, frontend, Docker images, deployment stack, and CI/CD. - ---- - -## Highlights - -### Modern Stack - -- **Python 3.12** (from 3.11) -- **Node.js 20** (from 18) -- **Django 4.2.17** with 40+ upgraded packages -- **React 18 + TypeScript + Vite + Tailwind CSS** (new Forail UI) -- **Channels 4 / Daphne 4 / Cython 3** migration completed - -### New Forail UI - -- Complete rewrite in React 18 with TypeScript -- Vite build system (replaces CRA/react-scripts) -- Tailwind CSS for styling (replaces PatternFly 4 + Styled Components) -- Dashboard with real-time job status, host counts, project health -- Full CRUD for all major resources (templates, credentials, projects, inventories, hosts, organizations, teams, users, notification templates, schedules) -- Job output streaming with ANSI color support -- Workflow visualizer (read-only) -- Survey editor for job/workflow templates -- Copy/clone support for templates, credentials, notifications -- Settings management UI with per-category editing -- Force password change on first superuser login -- Legacy UI preserved at `/ui_legacy/` - -### Production Docker Compose - -- 6-service deployment: PostgreSQL 15, Redis 7, init, web, task, Nginx -- TLS termination via Nginx reverse proxy -- Automated backup/restore scripts -- Health checks on all services -- Environment-based configuration (`.env.example` template) - -### Dual Docker Images - -- **CentOS Stream 9** — 882MB (`forail:2026.03.0-centos`) -- **Ubuntu 24.04** — 932MB (`forail:2026.03.0-ubuntu`) - -### CI/CD Pipelines - -- **GitLab CI** (`.gitlab-ci.yml`) — 5 stages: lint, test, build, security, release -- **Jenkins** (`Jenkinsfile`) — equivalent pipeline with parallel stages -- Version derived from git tag (`v2026.03.0` → `2026.03.0`) -- Automated Harbor registry push on tag release -- pip-audit + Trivy security scanning integrated - ---- - -## Quality Metrics - -| Category | Result | -| ----------------------- | --------------------------------- | -| Python unit tests | 1237 passed, 0 failed | -| Frontend unit tests | 42 passed, 0 failed | -| Functional API tests | 989 passed, 0 failed, 1 skipped | -| Python lint (flake8) | 0 errors | -| Frontend lint (tsc) | 0 errors | -| Python CVEs (pip-audit) | 15 remaining (0 critical runtime) | -| Container CVEs (Trivy) | 0 CRITICAL | - ---- - -## Breaking Changes - -- **Minimum Python version**: 3.12 (was 3.11) -- **Minimum Node.js version**: 20 (was 18) -- **CalVer versioning**: version scheme changed from SemVer (`24.6.x`) to CalVer (`2026.03.0`) -- **aioredis removed**: replaced by redis-py (channels 4 migration) -- **async-timeout removed**: Python 3.12 has `asyncio.timeout` built-in -- **Frontend**: new Forail UI at `/`, legacy AWX UI moved to `/ui_legacy/` - ---- - -## Dependency Changes (Major) - -| Package | Before | After | -| ------------------ | ------- | ------- | -| Django | 4.2.10 | 4.2.17 | -| channels | 3.0.5 | 4.1.0 | -| daphne | 3.0.2 | 4.1.2 | -| cryptography | 41.0.7 | 42.0.8 | -| Cython | 0.29.37 | 3.0.11 | -| grpcio | 1.62.2 | 1.67.1 | -| twisted | 24.3.0 | 24.7.0 | -| boto3 | 1.34.42 | 1.35.36 | -| redis | 5.0.1 | 5.2.1 | -| aiohttp | 3.9.3 | 3.10.10 | -| psutil | 5.9.8 | 6.1.0 | -| pip (build) | 21.2.4 | 24.0 | -| setuptools (build) | 69.0.2 | 70.0.0 | - ---- - -## Build Fixes Applied - -1. OpenSSL version pin removed (CentOS Stream 9 repo changes) -2. Rsyslog version pin removed -3. django-ansible-base pinned to stable `2024.9.4` (was `@devel`) -4. Argparse crash fix for Python 3.12.8+ (`_parse_known_args` intermixed parameter) -5. Ubuntu Dockerfile: npm package, pkg-config, bash brace expansion, storage.conf lookup -6. pip hash reset after Node.js upgrade in Dockerfile - ---- - -## Deployment - -### Quick Start (Docker Compose) - -```bash -cd tools/docker-compose-prod -cp .env.example .env -# Edit .env with your secrets -docker compose up -d -``` - -### Docker Images - -```bash -# CentOS (default) -docker pull forail:2026.03.0 - -# Ubuntu -docker pull forail:2026.03.0-ubuntu -``` - ---- - -## Full Changelog - -See [CHANGELOG.md](https://github.com/forail-platform/forail-devops/blob/main/CHANGELOG.md) for the complete list of changes across all 9 phases. diff --git a/docs/RELEASE_NOTES_v2026.04.0.md b/docs/RELEASE_NOTES_v2026.04.0.md deleted file mode 100644 index 61d675a..0000000 --- a/docs/RELEASE_NOTES_v2026.04.0.md +++ /dev/null @@ -1,185 +0,0 @@ -# Forail 2026.04.0 — Release Notes - -**Release date:** 2026-04-03 -**Based on:** Forail 2026.03.0 + new features -**License:** Apache License 2.0 - ---- - -## Overview - -Forail 2026.04.0 delivers the remaining Tier 1 features from the post-release roadmap: -Event-Driven Automation (EDA), AI Assistant, Dynamic Surveys, and Improved Audit Trail. - ---- - -## New Features - -### Event-Driven Automation (EDA) - -Webhook-based event routing with user-defined rules. External systems (GitHub, GitLab, -Alertmanager, PagerDuty, Datadog, CloudWatch, or any generic HTTP source) can trigger -automated job launches, workflow executions, or notification dispatches. - -- **EventRule model:** Conditions (Jinja2) + Actions (launch job, workflow, notification) -- **Public webhook receiver:** `/api/v2/eda_webhooks//` with HMAC signature verification -- **Jinja2 condition engine:** Sandboxed evaluation against webhook payloads -- **Outbound webhooks:** Push job status changes to external systems -- **EventLog:** Full audit trail of received webhooks and rule evaluation results -- **Frontend:** New "Automation" sidebar section with Event Rules, Event Logs, Outbound Webhooks pages -- **Security:** HMAC verification (SHA-256/SHA-1), rate limiting via throttle, payload size limits, deduplication -- **Dry-run test endpoint:** Evaluate conditions without firing actions - -### AI Assistant (Ollama RAG) - -Optional microservice providing context-aware chat assistance using local LLMs. - -- **forail-assistant** repository: FastAPI + Ollama + ChromaDB -- **SSE streaming** for real-time token delivery -- **Frontend chat panel** with floating button, markdown rendering, context awareness -- **Privacy-first:** All processing on-premises, no cloud API calls - -### Dynamic Surveys - -Survey questions with choices populated at launch time from three sources: -database queries, external API calls, and Jinja2 templates. - -### Improved Audit Trail - -- **ActivityStream enhanced:** Now captures `actor_ip`, `actor_user_agent`, `actor_session_id` -- **AuditEvent model:** Immutable append-only security log for compliance -- **Frontend Audit Log page:** Filters, expandable rows, CSV export -- **SIEM export:** Flat JSON format for Splunk/ELK/Datadog - ---- - -## New API Endpoints - -| Endpoint | Method | Description | -| --------------------------------------- | ------------------ | --------------------------------- | -| `/api/v2/event_rules/` | GET, POST | List/create event rules | -| `/api/v2/event_rules/{id}/` | GET, PATCH, DELETE | Event rule CRUD | -| `/api/v2/event_rules/{id}/webhook_key/` | GET, POST | Get/rotate webhook key | -| `/api/v2/event_rules/{id}/event_logs/` | GET | Logs for this rule | -| `/api/v2/event_rules/{id}/test/` | POST | Dry-run condition test | -| `/api/v2/event_rules/{id}/enable/` | POST | Enable rule | -| `/api/v2/event_rules/{id}/disable/` | POST | Disable rule | -| `/api/v2/event_logs/` | GET | List all event logs | -| `/api/v2/event_logs/{id}/` | GET | Event log detail | -| `/api/v2/outbound_webhooks/` | GET, POST | List/create outbound webhooks | -| `/api/v2/outbound_webhooks/{id}/` | GET, PATCH, DELETE | Outbound webhook CRUD | -| `/api/v2/outbound_webhooks/{id}/test/` | POST | Send test payload | -| `/api/v2/eda_webhooks/{path}/` | POST | Public webhook receiver (no auth) | -| `/api/v2/audit_events/` | GET | List audit events | -| `/api/v2/audit_events/?format=csv` | GET | Export as CSV | -| `/api/v2/audit_events/?format=siem` | GET | Export for SIEM | - ---- - -## New Database Tables - -| Table | Description | -| ---------------------- | ---------------------------------------------- | -| `main_eventrule` | Webhook rules with conditions and actions | -| `main_eventlog` | Incoming webhook events and evaluation results | -| `main_outboundwebhook` | Outbound webhook configurations | -| `main_auditevent` | Immutable security audit log | - ---- - -## Frontend Changes - -- New **Automation** sidebar section: Event Rules, Event Logs, Outbound Webhooks -- New **Audit Log** page with filters, expandable rows, CSV export -- New **AI Assistant** floating chat panel -- **Dynamic survey** support in launch dialog and survey editor -- 10 new pages, 3 new API hooks, TypeScript interfaces for all EDA types - ---- - -## Quality Metrics - -| Metric | Value | -| ------------------------------ | --------- | -| Backend tests (standalone EDA) | 38 passed | -| Frontend tests (vitest) | 58 passed | -| TypeScript compilation | 0 errors | - ---- - -## Documentation - -New documentation files: - -- `docs/13-dynamic-surveys.md` — Dynamic survey system -- `docs/14-audit-trail.md` — Audit trail and compliance logging -- `docs/15-event-driven-automation.md` — EDA architecture, API, security, quick start - -Updated documentation: - -- `01-architecture-overview.md` — Added EDA webhook flow -- `02-backend-django.md` — Added EDA models reference -- `03-frontend-react.md` — Added EDA routes -- `04-task-engine.md` — Added EDA as job launch source -- `06-database-schema.md` — Added EDA tables and ER relationships -- `09-testing-guide.md` — Added standalone test suite -- `11-api-reference.md` — Added all EDA endpoints -- `wiki-index.md` — Added docs 13, 14, 15 -- `future_development_plan.md` — Updated competitive landscape (EDA, Dynamic Surveys, AI Assistant, Audit Trail: Planned → Yes) - ---- - -## v2026.04.0-patch1 (2026-04-13) - -### Improvements - -- **AI Assistant redesign** — Floating chat widget with welcome message, minimize/maximize, message timestamps, streaming responses -- **Assistant RAG documentation** — 14 knowledge base files (850 lines) covering all features: EDA, Drift, Policy, Scanner, Service Catalog, Tenancy, WebAuthn, Recommendations, Observability, Wizards, API reference, common errors -- **ChromaDB client upgrade** — Updated from 0.5.23 to 1.5.7 for compatibility with ChromaDB server 1.4.x -- **Nginx proxy** — Added `/assistant/` proxy route for AI Assistant API with SSE streaming support - -### Bug Fixes - -- **Migration ordering** — Fixed `_OrgAdmin_to_use_ig.py` to use `apps.get_model()` instead of direct model import, preventing schema mismatch on fresh deploys -- **Missing migrations** — Added `0204_audit_event` migration for AuditEvent model and ActivityStream audit fields (actor_ip, actor_user_agent, actor_session_id) -- **Migration chain** — Reordered migrations so audit_event (0204) precedes RLS policies (0206) which references the table -- **Assistant docker-compose** — Fixed healthchecks for Ollama and ChromaDB containers (curl not available in images) -- **Assistant registry path** — Fixed image path from `forail-platform` to `forail-platform` - -### Testing - -- **Backend** — Added `test_comprehensive.py`: 97 standalone tests covering SimpleDAG (cycle detection, topological sort), K8s CPU/memory parsing, Jinja sanitization, safe YAML dump, string coercion, vars validation -- **Frontend** — Added `statusConfig.test.ts`, `client.test.ts`, `app.test.ts`: 72 tests for status mappings, API error flattening, route completeness (all 70+ routes verified) -- **CI pipeline** — Jenkinsfile now runs 4 parallel test stages: Backend Standalone, Backend Unit, Frontend, Assistant -- **Total test count** — Backend 404 + Frontend 212 = 616 tests passing - -### Refactoring - -- **forail-assistant** — Extracted shared ChromaDB client and embedding functions into `app/db.py`, eliminating 32 lines of duplicated code between `rag.py` and `indexer.py` - -### Quality Metrics - -| Metric | Value | -| ------------------------ | -------------------------- | -| Backend standalone tests | 404 passed | -| Frontend tests (vitest) | 212 passed | -| Assistant knowledge base | 14 docs, 92 chunks indexed | -| TypeScript compilation | 0 errors | - ---- - -## Competitive Landscape Update - -| Feature | Forail | AWX | AAP 2.5+ | Ascender | Semaphore | -| ----------------------- | ------- | --- | -------- | -------- | --------- | -| Dynamic surveys | **Yes** | No | No | No | No | -| Event-driven (EDA) | **Yes** | No | Yes | No | No | -| AI assistant (RAG) | **Yes** | No | Yes | No | No | -| Audit trail (immutable) | **Yes** | No | Partial | No | No | -| IaC scanning | **Yes** | No | No | No | No | -| Policy-as-Code (OPA) | **Yes** | No | No | No | No | -| Drift detection | **Yes** | No | No | No | No | -| Self-service portal | **Yes** | No | Partial | No | No | -| Multi-tenancy (RLS) | **Yes** | No | Partial | No | No | -| WebAuthn/Passkey MFA | **Yes** | No | No | No | No | -| Smart recommendations | **Yes** | No | No | No | No | diff --git a/docs/RELEASE_NOTES_v2026.05.0.md b/docs/RELEASE_NOTES_v2026.05.0.md deleted file mode 100644 index 272e4b9..0000000 --- a/docs/RELEASE_NOTES_v2026.05.0.md +++ /dev/null @@ -1,277 +0,0 @@ -# Forail 2026.05.0 — Release Notes - -**Release date:** 2026-05-22 -**Based on:** Forail 2026.04.0 + new features -**License:** Apache License 2.0 - ---- - -## Overview - -Forail 2026.05.0 is the platform's GA milestone. The Kubernetes operator -graduates to **v1.0.0** with a complete resource model (9 CRDs) and -multi-cluster control-plane support, the dev-cluster moves to a -production-shaped 3-master/4-worker HA k3s topology, and the AI -Assistant is repackaged as a single all-in-one image that runs on a -single PVC. - ---- - -## Component Versions - -| Component | Version | Notes | -| ----------------- | --------- | ------------------------------------------------------------------ | -| forail-backend | 2026.05.0 | Migration `0208` fix for `DriftAlertRule` audit fields | -| forail-frontend | 0.1.0 | No changes this cycle (UI from v2026.04.0 still current) | -| forail-assistant | 2026.05.0 | All-in-one image (Ollama + ChromaDB embedded), `gemma3:1b` default | -| forail-operator | **1.0.0** | 5 new CRDs, multi-cluster, OLM bundle | -| forail-helm | **1.0.0** | `appVersion: 2026.05.0` | -| forail-dev-cluster | — | 3m+4w k3s 1.30 (was 2m+2w kubeadm) | - ---- - -## New Features - -### Forail Operator v1.0.0 — Complete Resource Model + Multi-Cluster - -The Kubernetes operator now covers the full Forail object graph and can -fan out to multiple Forail backends from a single control plane. - -**5 new CRDs:** - -- **`Project`** — SCM-backed source of playbooks, with optional - Credential + ExecutionEnvironment references. -- **`Organization`** — top-level tenant container with max-host quota - and a default-EE reference. -- **`Team`** — namespaced team within an Organization. `spec.users[]` - is reconciled declaratively against - `/api/v2/teams/{id}/users/` (add/remove users to match spec). -- **`Workflow`** — `workflow_job_template` wrapper with a declarative - DAG of nodes (`spec.nodes[]` keyed by `identifier`) and three edge - types (`successNodes`, `failureNodes`, `alwaysNodes`). The - reconciler diffs against - `/workflow_job_template_nodes/` + each node's sub-relations. -- **`ForailInstance`** — describes a Forail backend (URL + bearer - token via `tokenSecretRef`) that other CRs target by name via - `spec.forailInstance`. - -**Multi-cluster (`forailapi.ClientPool`):** - -Per-CR resolution of which Forail backend to write to. CRs without -`spec.forailInstance` fall back to the default client supplied via -`--forail-url` / `--forail-token`. Generation-gated cache invalidation -on the ForailInstance reconciler rebuilds the client lazily when the -target URL or secret reference changes. - -**OLM packaging:** - -- `config/manifests/bases/forail-operator.clusterserviceversion.yaml` - — CSV with `alm-examples`, `customresourcedefinitions.owned` - entries for all 9 CRDs, deployment spec, cluster-scoped RBAC. -- `bundle.Dockerfile` + `bundle/{manifests,metadata}` for OperatorHub - catalog builds. -- Makefile targets: `bundle`, `bundle-build`, `catalog-build`. - -**Resource model summary:** - -| CRD | Scope | Reconciles to | -| ----------------- | ---------- | --------------------------------------------- | -| Inventory | Namespaced | `/api/v2/inventories/` | -| Credential | Namespaced | `/api/v2/credentials/` | -| JobTemplate | Namespaced | `/api/v2/job_templates/` | -| Schedule | Namespaced | `/api/v2/schedules/` | -| **Project** | Namespaced | `/api/v2/projects/` | -| **Organization** | Cluster | `/api/v2/organizations/` | -| **Team** | Namespaced | `/api/v2/teams/` + user membership | -| **Workflow** | Namespaced | `/api/v2/workflow_job_templates/` + DAG nodes | -| **ForailInstance** | Namespaced | (control-plane only; no upstream call) | - -### Forail Assistant — All-in-One Image - -The AI Assistant is repackaged from three Compose services (Ollama + -ChromaDB + FastAPI + a setup container) into **one container** with a -single `/data` volume. - -- **`entrypoint.sh`** orchestrates startup: `ollama serve`, conditional - model pull (the configured chat model + `nomic-embed-text`), - `chroma run`, document indexing, then `uvicorn`. -- Default chat model: **`gemma3:1b`** (was `mistral:7b`). Smaller and - faster; answer quality reduced for general questions but adequate - for short RAG-grounded responses against the in-tree docs. -- Default `top_k` lowered from 5 to 3. -- Default config hosts switched from `ollama` / `chromadb` to - `localhost` to match the single-container layout. -- Explicit `httpx.Timeout(connect=10, read=300, write=10, pool=10)` - so the long read timeout no longer applies to connection setup. -- New RAG corpus under `docs_to_index/deployment/` (architecture, - Docker deployment, CI/CD, contributing, admin/user handbooks, - startup walkthrough) so the assistant can answer operational - questions. - -The Helm chart (`assistant.enabled=true`) provisions a single -Deployment + PVC + Service (default 20 GiB volume, -1 GiB/250m requests, 4 GiB/2 vCPU limits, `startupProbe` -`failureThreshold: 30` ≈ 5 min boot budget for the first-pull model -download). - -### Forail Helm 1.0.0 - -- Chart version bumped to **1.0.0** (from 0.3.0) marking platform GA - alongside `forail-operator` v1.0.0. -- `appVersion: 2026.05.0` tracks the backend release that ships the - `0208_driftalertrule_audit_fields` migration fix. -- 5 new operator CRDs added under `helm/crds/` so `helm install -forail-operator` provisions the complete schema. - -### Dev-Cluster — 3-Master / 4-Worker HA k3s - -The Vagrant test cluster (`forail-dev-cluster`) was rebuilt for -production-shaped HA: - -- Topology: **3 control-plane (k8s-m1..m3) + 4 worker (k8s-w1..w4)** - on `192.168.56.30-36`. Per-VM resources bumped to 2 vCPU / 4 GB - (was 2 vCPU / 2 GB) → 14 vCPU, 28 GB total. -- **Switched distribution from kubeadm to k3s** (v1.30.4+k3s1). k3s - bundles Traefik, local-path-provisioner, klipper-lb (servicelb), - CoreDNS, and metrics-server, so `post-cluster-setup.sh` collapsed - from 4 stages to creating just the `forail` namespace + Harbor - pull-secret + self-signed TLS cert. -- **HA control plane via embedded etcd**: first server runs - `k3s server --cluster-init`, the other two join with `--server`. - TLS SANs cover all 3 server IPs/hostnames so `kubectl` works - against any master. 3-node etcd quorum tolerates a single master - failure (was 2-node quorum, which lost the cluster on any master - loss). -- `--flannel-iface=eth1` passed explicitly on every node — fixes the - long-standing wrong-NIC binding that broke pod networking on the - kubeadm setup. - -Provisioning scripts renamed: `master-init.sh` / `master-join.sh` / -`worker-join.sh` → `server-init.sh` / `server-join.sh` / -`agent-join.sh`. - ---- - -## Bug Fixes - -### Backend — `DriftAlertRule` cascade-delete (migration `0208`) - -`DriftAlertRule` rows could not be cascade-deleted from an -Organization: the original `0198_drift_models` migration omitted the -`created_by` / `modified_by` FK columns inherited from -`PrimordialModel`, so any ORM query joining the audit columns blew up -with `psycopg.UndefinedColumn`. - -- Symptom in the wild: `DELETE /api/v2/organizations/{id}/` returned - HTTP 500 and the `forail-operator` Organization finalizer hung - forever. -- Migration `0208_driftalertrule_audit_fields` backfills both columns - as nullable + `SET_NULL`. -- New schema-level regression test - (`tests_standalone/test_drift_audit_fields_schema.py`) parses the - migration sequence and asserts the columns exist so the gap can't - re-open. - ---- - -## Upgrade Path - -### From 2026.04.0 → 2026.05.0 - -**Backend (`forail-helm` upgrade):** - -```sh -helm repo update -helm upgrade forail forail-platform/forail -n forail \ - --version 1.0.0 \ - --reuse-values -kubectl -n forail exec deploy/forail-web -- forail-manage migrate -``` - -The `0208` migration is forward-compatible (nullable column add, -SET_NULL FKs). No downtime; existing `DriftAlertRule` rows backfill -with `NULL` audit fields. - -**Operator (`forail-operator` upgrade):** - -If upgrading the operator from 0.3.x to 1.0.0: - -```sh -# 1. Apply the new CRDs first (Helm hooks won't re-install CRDs). -kubectl apply -f https://github.com/forail-platform/forail-operator/releases/download/v1.0.0/crds.yaml - -# 2. Upgrade the operator chart. -helm upgrade forail-operator forail-platform/forail-operator -n forail-operator \ - --version 1.0.0 \ - --reuse-values -``` - -Existing `Inventory` / `Credential` / `JobTemplate` / `Schedule` CRs -continue to work unchanged. - -**Multi-cluster (optional):** - -To start fanning out to multiple Forail backends, create a -`ForailInstance` per backend and reference it in your CRs: - -```yaml -apiVersion: forail.forail-platform.io/v1alpha1 -kind: ForailInstance -metadata: - name: forail-staging -spec: - url: https://forail-staging.example.com - tokenSecretRef: - name: forail-staging-token - key: token ---- -apiVersion: forail.forail-platform.io/v1alpha1 -kind: JobTemplate -metadata: - name: deploy-staging -spec: - forailInstance: forail-staging # routes to the staging backend - ... -``` - -CRs without `spec.forailInstance` continue to use the default -operator-wide URL/token. - ---- - -## Documentation - -New / updated documentation: - -- **`forail-platform.github.io/docs/operator-v1.html`** — Dedicated - page for v1.0.0: multi-cluster, Workflow DAG model, OLM bundle, - upgrade path. -- **`forail-platform.github.io/docs/kubernetes.html`** — Refreshed for - v1.0.0 (9-CRD table, sidebar reorganized). -- **`forail-assistant/docs_to_index/deployment/`** — 8 new operational - markdown files indexed into the RAG corpus. - ---- - -## Known Issues - -- **OLM bundle warnings:** 4 of the older CRDs (Inventory, Credential, - JobTemplate, Schedule) don't yet have `alm-examples` entries in the - CSV, and the CSV lacks `spec.icon`. Both are cosmetic - (`operator-sdk bundle validate` warnings, not errors) and do not - block submission to OperatorHub. Slated for `1.0.1`. -- **`forail-assistant` first boot** can take 3–5 minutes on the - initial pod start while `gemma3:1b` and `nomic-embed-text` are - pulled into the PVC. The `startupProbe` (`failureThreshold: 30`) - budgets ~5 minutes; raise it if your registry mirror is slow. - ---- - -## Quality Metrics - -| Metric | Value | -| ------------------------------------------------------------------------------------- | -------------------------------------- | -| Operator e2e (live 3m+4w k3s) | 9/9 CRDs reconcile cleanly | -| Backend regression (`test_drift_audit_fields_schema`) | passing | -| Helm chart (`helm lint` + `helm template`, both default and `assistant.enabled=true`) | passing | -| Operator OLM bundle (`operator-sdk bundle validate`) | passing (4 warnings, see Known Issues) | diff --git a/docs/RELEASE_NOTES_v2026.06.0.md b/docs/RELEASE_NOTES_v2026.06.0.md deleted file mode 100644 index 84a6f27..0000000 --- a/docs/RELEASE_NOTES_v2026.06.0.md +++ /dev/null @@ -1,64 +0,0 @@ -# Forail 2026.06.0 — Release Notes - -**Release date:** 2026-06-14 -**Based on:** Forail 2026.05.0 -**License:** Apache License 2.0 - ---- - -## Overview - -Forail 2026.06.0 is the **project rename release**. The platform — -previously published as *Forge* under the `forgeplatform` GitHub -organization — is now **Forail**, under the **`forail-platform`** -organization. This was a deliberate move to a unique, unambiguous name -that does not collide with the many existing "forge"-named projects. - -There are **no functional changes** in this release. Every component was -verified to build and deploy unchanged after the rename (full stack -brought up on a 5-node k3s cluster, operator reconciling, REST API -serving `HTTP 200`, database migrations applied). - -## What changed - -- **Name:** `forge` → `forail`, organization `forgeplatform` → - `forail-platform` (note the hyphen). -- **Container images:** now published to - `ghcr.io/forail-platform/forail-`. The old - `ghcr.io/forgeplatform/forge-*` packages are retired. -- **Python package:** `forge` → `forail`; CLI `forge-manage` → - `forail-manage` (the `awx-manage` compatibility alias is retained). -- **Kubernetes operator:** Go module - `github.com/forail-platform/forail-operator`; CRD API group - `forge.forgeplatform.io` → **`forail.forail-platform.io`**; the - `ForgeInstance` kind is now **`ForailInstance`**. All 9 CRDs renamed. -- **Helm chart / Compose:** image references and the `forail.lan` - ingress host updated; values pinned to the `2026.06.0` images. -- **Mobile:** Kotlin package `io.forailplatform.mobile` (no hyphen — - hyphens are invalid in JVM package identifiers). -- **Versioning:** all platform components are unified on CalVer - `2026.06.0` for this coordinated release. - -## Upgrade guide - -This is a rename, not a data-model change, so existing installs keep -working on the old images until you re-point them. - -1. **Kubernetes (Helm):** - ```bash - helm upgrade forail oci://ghcr.io/forail-platform/forail-helm \ - --version 2026.6.0 -n forail - ``` -2. **Operator:** because the CRD API group changed - (`forge.forgeplatform.io` → `forail.forail-platform.io`), this is a - **breaking change for existing CRs**. Re-apply your resources against - the new group. The operator is republished as `forail-operator`. -3. **Docker Compose:** pull `FORAIL_TAG=2026.06.0` and - `ghcr.io/forail-platform/forail-*` images (see `.env.example`). - -## Notes - -- GitHub automatically redirects the old organization/repository URLs, - so existing links continue to resolve. -- A fresh OperatorHub.io submission under the `forail-operator` name is - planned to replace the retired `forge-operator` listing. diff --git a/docs/RELEASE_NOTES_v2026.07.0.md b/docs/RELEASE_NOTES_v2026.07.0.md deleted file mode 100644 index 26294d5..0000000 --- a/docs/RELEASE_NOTES_v2026.07.0.md +++ /dev/null @@ -1,413 +0,0 @@ -# Forail 2026.07.0 — Release Notes - -**Release date:** 2026-07-25 -**Based on:** Forail 2026.06.0 -**License:** Apache License 2.0 - ---- - -## Overview - -2026.07.0 is a **security-hardening and migration** release. It tightens several -authentication and audit defaults (some of which are **breaking** for existing -SAML deployments), makes tenant isolation fail closed, replaces the insecure -defaults in the Helm chart and Compose stack (**breaking** — installs now require -an explicit admin password), and introduces a one-shot **AWX → Forail importer** -so teams can migrate off AWX/AAP without rebuilding their configuration by hand. - -Kubernetes installs also gain the pod RBAC and receptor worktype that in-cluster -job execution needs — see *Fixed*. - -There are no data-model changes. Two idempotent migrations (`0209`, `0210`) ship -with the tenancy work; both only drop and re-create PostgreSQL row-level-security -policies, so they apply to an existing database without touching table schemas or -rows. - -## ⚠️ Known issue — upgrading breaks job execution - -> **Fixed in 2026.07.1.** Upgrade to it instead of applying the workaround -> below — see the [2026.07.1 release notes](RELEASE_NOTES_v2026.07.1.md). The -> rest of this section describes what happens if you stay on 2026.07.0. - -**If you upgrade an existing 2026.06.0 install, jobs stop running: they are -accepted and then stay in `pending` indefinitely.** The only hint is the job's -`job_explanation`, *"This job is not ready to start because there is not enough -available capacity"* — accurate, but it does not point at the cause. Fresh -installs are unaffected. The workaround below is verified on a live cluster. - -The `default` instance group has to satisfy two conditions at once for a job to -run locally, and an upgrade breaks both: - -1. **It must contain an instance.** The chart's init Job calls `register_queue - --queuename=default`, which on an upgrade finds the group already there, - prints `Instance Group already registered default` and assigns nothing. -2. **That instance must be able to execute.** The task pod re-registers itself - as `node_type=control` on every start, and a control node only orchestrates. - -Either one alone is enough to hang every launch — both were measured -individually, holding the other fixed. - -Project updates keep working, because they run in `controlplane`, which does -have the instance. The install therefore looks healthy right up until someone -launches a job. - -**Workaround, after `helm upgrade` completes:** - -```bash -kubectl -n forail exec deploy/forail-web -- forail-manage shell -c " -from forail.main.models import Instance, InstanceGroup -i = Instance.objects.get(hostname='forail-node') -i.node_type='hybrid'; i.save(update_fields=['node_type']) -InstanceGroup.objects.get(name='default').instances.add(i)" -``` - -Substitute your own instance hostname if you did not install with the chart -defaults. Any job already sitting in `pending` starts on its own within about a -minute; a fresh short job should return a normal `PLAY RECAP` in a few seconds. - -> **The workaround does not survive a restart of `forail-task`.** That pod -> re-runs `provision_instance` every time it starts — after a node reboot, an -> eviction, or the next `helm upgrade` — and that call resets both `node_type` -> and the group's execution mode. Re-apply it, and re-check job execution, after -> any task-pod restart until the fix ships. - -**Also re-apply your role assignments after upgrading.** Any role assignment -attempted on 2026.06.0 failed silently (the `ScanFinding` / -`TenantIsolationEvent` `FieldDoesNotExist` bug fixed in this release, see -*Fixed*). The upgrade fixes the cause but does not recreate the assignments that -were lost, and *Fixed* saying "no data migration is required" refers to the -schema only. Check the members of every role you rely on and re-grant what is -missing. - -What the upgrade does do correctly: it succeeds, migrations `0209` and `0210` -apply cleanly, and no data is lost — object counts and names are identical -before and after. - -## Security advisories - -**Upgrade from 2026.06.0 or earlier is strongly recommended.** Several of the -fixes below are exploitable on a default install of an earlier release; the -detail is in *Security hardening* and *Breaking changes* further down. - -Severity is this project's own assessment of impact on a default deployment. It -is not CVSS, and no CVE identifiers have been requested. "Affected" means every -release up to and including 2026.06.0. - -| Issue | Severity | Who is exposed | What to do | -|---|---|---|---| -| Deployment artifacts shipped working default credentials | **Critical** | Any install that did not override the shipped secrets | Upgrade; rotate `forailAdminPassword`, `postgresPassword`, `forailSecretKey` and the websocket secret | -| SSO account takeover — accounts associated by email address, not provider UID | **Critical** | Installs using SAML/OIDC/social auth where an IdP can assert an arbitrary email | Upgrade; audit existing SSO-linked accounts for unexpected associations | -| Tenant isolation gate could essentially never fire; RLS failures degraded to global row visibility | **High** | Multi-tenant installs | Upgrade; review `TenantIsolationEvent` records and cross-tenant access in the audit trail | -| `forail-task` ran privileged with a host cgroup mount by default — a container escape to node-root | **High** | Kubernetes and Compose installs using shipped defaults | Upgrade; job execution now needs an explicit opt-in, ideally on dedicated tainted nodes | -| `ALLOWED_HOSTS` defaulted to `*` and session cookies were not `Secure` | **Medium** | Internet-reachable installs | Upgrade; set your real ingress host(s) and terminate TLS | -| Audit records stored the raw session key; `X-Forwarded-For` was trusted unconditionally for the audit source IP | **Medium** | Any install; higher where audit logs are broadly readable | Upgrade; configure `PROXY_IP_ALLOWED_LIST`; treat historical audit rows as sensitive | -| IaC scanner could be pointed outside the project checkout via a job template's `playbook` field | **Medium** | Installs where non-admins can edit job templates | Upgrade | -| Operator held cluster-wide `get/list/watch` on every Secret | **Medium** | Kubernetes installs running the operator | Upgrade the operator to **2026.07.1**; its Secret access is now a namespaced `Role` | -| OAuth `refresh_token` was recorded in activity-stream entries; superuser grant/revoke was not separately audited | **Low** | Any install | Upgrade; consider rotating OAuth tokens that appear in historical activity entries | - -Upgrading does not retroactively clean data written by an earlier release — -rotate the credentials above, and treat pre-upgrade audit and activity rows as -potentially containing secrets. - -## Added - -### AWX → Forail migration importer - -A new backend management command migrates configuration from an existing AWX (or -AAP) installation via its REST API: - -```bash -forail-manage import_from_awx \ - --url https://awx.example.com \ - --token "$AWX_TOKEN" \ - --dry-run # preview; remove to apply -``` - -- Imports Organizations, Users, Teams, Credential Types, Credentials, Projects, - Inventories (with group hierarchy and host membership), Inventory Sources, - Job Templates, Workflow Job Templates (with their node DAG), Schedules, - Notification Templates, and RBAC role assignments, in dependency order. -- **Idempotent** — re-running matches existing objects by natural key (name - within organization; username for users) and updates rather than duplicating. -- `--dry-run` previews all changes inside a rolled-back transaction. -- `--resource ` (repeatable) limits the run to specific resource types. -- Auth via `--token` (preferred) or `--username/--password`; `--insecure` skips - source TLS verification. - -**Secrets are not migrated.** The AWX API never returns secret credential inputs -(it sends `$encrypted$`), and user passwords are not exported. The importer -brings over credential *structure* and non-secret inputs, creates users with an -unusable password, and reports exactly how many secret fields need manual -re-entry afterwards. Notification-template secrets are stripped the same way. - -RBAC role assignments are migrated where the target framework allows it: user -grants and team→object-role grants are applied; organization-member grants to -teams (rejected by the access framework) are skipped with a warning rather than -aborting the run. - -## Security hardening - -- Superuser grant/revoke is now written to the dedicated audit log - (independently of the activity stream). -- Audit records store a SHA-256 hash of the session key, never the raw key. -- `X-Forwarded-For` is trusted for the audit source IP only behind a configured - trusted proxy (`PROXY_IP_ALLOWED_LIST`). -- OAuth `refresh_token` is redacted from activity-stream entries. -- Tenant concurrency-quota errors are logged instead of silently swallowed. - -### Tenant isolation now fails closed - -- The RLS middleware **aborts the request (HTTP 500)** if it cannot install the - tenant scope, instead of continuing with global row visibility. -- The strict-isolation gate resolves the target organization with the caller's - RLS scope removed — previously the lookup ran *inside* the caller's scope, so a - cross-tenant object was invisible and the gate could essentially never fire. It - now denies by default when a covered resource's organization cannot be - determined, and records a `TenantIsolationEvent`. -- RLS coverage extended to `main_eventlog`, and every policy now casts the tenant - GUC via `NULLIF(current_setting(...), '')::int` so the empty "no scope" - sentinel cannot raise (migrations `0209`, `0210`). -- The tenancy rate limiter logs Redis outages loudly and honours - `TENANCY_RATE_LIMIT_FAIL_CLOSED` (default open, for availability). - -### Trust boundaries around import and SSO - -- `import_from_awx` no longer carries privilege across the boundary implicitly: - superuser / system-role promotion requires `--grant-superusers`, custom - credential-type injectors are dropped for admin re-approval unless - `--trust-injectors` is passed, and secrets are read from `AWX_TOKEN` / - `AWX_PASSWORD` in preference to argv. -- **SSO account takeover fixed**: `associate_by_email` was removed from the auth - pipeline — accounts associate by provider UID, never by matching email. -- Tenant provisioning refuses to silently reuse an existing username (which - discarded the supplied password and cross-linked accounts) unless - `attach_existing_admin` is set. -- The IaC scanner can no longer be pointed outside the project checkout via a job - template's `playbook` field (absolute paths / `..`). - -### Deployment defaults — Helm chart and Compose - -Both deployment artifacts shipped working credentials and a privileged worker by -default. That is over: - -- **Helm**: `postgresPassword`, `forailSecretKey` and - `forailBroadcastWebsocketSecret` are auto-generated on first install and reused - across upgrades; `forailAdminPassword` is **required**. `forail-task` runs - non-privileged with no host cgroup mount unless you opt in. Session cookies are - `Secure`, `allowedHosts` is the ingress host plus loopback (not `"*"`), and an - opt-in `NetworkPolicy` plus per-workload `securityContext` knobs are available. -- **Compose**: `FORAIL_TASK_PRIVILEGED` / `FORAIL_TASK_CGROUP` default off, - `FORAIL_ALLOWED_HOSTS` defaults to `localhost,127.0.0.1` instead of `*`, and - `FORAIL_TAG` pins to `2026.07.0` rather than `:latest`. -- **Operator**: the manager no longer holds cluster-wide `get/list/watch` on - every Secret — the credential reconciler's access is a namespaced - `Role`/`RoleBinding` in the operator's own namespace. - - > **Use operator `2026.07.1`.** In `2026.07.0` that narrowing left `Credential` - > CRs working only in the operator's own namespace, because a Credential - > resolves `spec.inputsFrom` in *its* namespace and the Secret cache covered - > only one. `2026.07.1` adds `secretNamespaces` to the operator chart, which - > renders both the per-namespace Secret `Role` and the matching cache scope: - > - > ```sh - > helm install forail-operator ... --set 'secretNamespaces={team-a,team-b}' - > ``` - > - > Still no cluster-wide secrets grant. Everything else in this release is - > unchanged at `2026.07.0`. -- **Assistant**: a wildcard CORS origin no longer combines with credentials, and - `/api/v1/chat` accepts an optional shared bearer token - (`FORAIL_ASSISTANT_CHAT_TOKEN`) with a concurrency cap - (`FORAIL_ASSISTANT_CHAT_MAX_CONCURRENCY`, 429 on overload). - -See **Breaking changes — deployment defaults** below for the upgrade actions. - -## ⚠️ Breaking changes — SAML - -Two SAML defaults changed. They affect installs that rely on the previous, -weaker behavior. - -### 1. Signed assertions + SHA-256 now required by default - -`SOCIAL_AUTH_SAML_SECURITY_CONFIG` now defaults to: - -```json -{ - "requestedAuthnContext": false, - "wantMessagesSigned": true, - "wantAssertionsSigned": true, - "rejectUnsolicitedResponsesWithInResponseTo": true, - "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" -} -``` - -**Impact:** If your IdP sends unsigned responses/assertions, or signs with -SHA-1, logins will be rejected after upgrade. - -**Action:** -- Preferred: reconfigure your IdP to sign responses and assertions with SHA-256. -- Temporary fallback: explicitly set `SOCIAL_AUTH_SAML_SECURITY_CONFIG` via - `PATCH /api/v2/settings/saml/` for a legacy IdP (not recommended for - production). - - > ⚠️ **Setting this replaces the whole dict — it does not merge with the - > secure defaults.** When the setting is unset, Forail's hardened defaults - > apply; the moment you set it, your value is used verbatim and any key you - > omit falls back to the **weak** python-saml/OneLogin default (unsigned - > assertions, SHA-1). So to relax a single key you must re-specify the full - > secure dict with only that key changed, e.g. to accept unsigned assertions - > from one legacy IdP while keeping every other protection: - > - > ```json - > { - > "requestedAuthnContext": false, - > "wantMessagesSigned": true, - > "wantAssertionsSigned": false, - > "rejectUnsolicitedResponsesWithInResponseTo": true, - > "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - > "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256" - > } - > ``` - > - > Clearing the setting (back to null) restores the full hardened defaults. - -### 2. SAML role-attribute grants require an explicit value - -Granting `is_superuser` / `is_system_auditor` from a SAML attribute now requires -a non-empty `is_superuser_value` / `is_system_auditor_value`. - -**Impact:** A configuration that sets only `is_superuser_attr` (with no value) -previously granted superuser to **every** user the IdP sent with that attribute. -That now fails safe (no grant) and logs a warning. - -**Action:** Set the required attribute value(s) for the flags you intend to grant. - -## ⚠️ Breaking changes — deployment defaults - -### 1. `helm install` requires an admin password - -`secrets.forailAdminPassword` has no default and no generated fallback; the chart -fails to render without it. The other three secrets (`postgresPassword`, -`forailSecretKey`, `forailBroadcastWebsocketSecret`) are generated on first -install and looked up on subsequent upgrades, so leave them empty unless you pin -them deliberately. - -**Action:** pass `--set secrets.forailAdminPassword=''` on -install. Automation that renders the chart (CI `helm lint` / `helm template`) -needs a throwaway value for the same reason. - -### 2. `forail-task` is no longer privileged by default - -The podman-in-pod execution path needs a privileged container and the host cgroup -namespace; both now default **off**, because a privileged pod with a host cgroup -mount is a trivial container escape. - -**Action, Kubernetes:** `--set task.privileged=true --set task.hostCgroup=true` -(ideally pinning those workers to dedicated, tainted nodes). -**Action, Compose:** `FORAIL_TASK_PRIVILEGED=true FORAIL_TASK_CGROUP=host`. - -### 3. Allowed hosts and secure cookies - -`forail.allowedHosts` / `FORAIL_ALLOWED_HOSTS` no longer default to `"*"`, and -session cookies are `Secure` by default — a deployment served over plain HTTP -will not keep a session. - -**Action:** set your real ingress host(s), and **keep `127.0.0.1,localhost` in the -list** — the in-cluster health probes call the API on loopback. Terminate TLS in -front of the ingress, or set `forail.cookieSecure: "false"` for a lab install. - -## Fixed - -- **The task dispatcher crash-looped on 2026.06.0, so no job could finish.** - The periodic schedule runs `update_active_jobs_gauge_task` every 30 seconds - unconditionally, but in 2026.06.0 that function carried Celery's - `@shared_task` instead of Forail's own `@task()`. Dispatching it raised - `ValueError: ... is not decorated with @task()`, the dispatcher exited, and - whatever was running died with *"Task was canceled due to receiving a - shutdown signal"* — typically surfacing as a failed project update and a job - in `error`. Measured on a fresh 2026.06.0 install: the dispatcher restarted - roughly every 50 seconds, indefinitely. **Anyone still on 2026.06.0 should - upgrade**; there is no configuration that avoids this, since the schedule - entry is not conditional. Fixed by registering the task properly - (`@task(queue=get_task_queuename)`). -- **In-cluster job execution.** Two pieces were missing from the chart, and each - failed a launch on its own. Note this is not "out of the box": project updates - and control-plane jobs still run through podman inside the task pod, so they - also need the privileged opt-in from *Breaking changes* #2 above. Without it - every job dies moments after launch with `mount - /var/lib/containers/storage/overlay: permission denied`, and the only symptom - in the UI is a project or job stuck in `Pending`. The chart now prints a - warning to that effect at install time when the flags are off. The two chart - fixes were: - - No pod RBAC. Jobs run as pods in a Kubernetes container group, and receptor - manages them with the task pod's ServiceAccount — which had no pod - permissions, so every launch failed with - `pods is forbidden ... cannot list resource "pods"` and the job hung pending. - The chart now ships a `forail` ServiceAccount plus a namespaced - `forail-job-runner` `Role`/`RoleBinding` (`pods`, `pods/log|attach|exec`) and - a `MY_POD_NAMESPACE` downward-API env so job pods land in the release - namespace. - - The receptor mesh config declared only the `local` worktype, so launches - errored at 0s with `unknown work type kubernetes-incluster-auth`. The - `kubernetes-incluster-auth` worktype (`authmethod: incluster`) is now - registered. -- **`forail-web` crash-loop after the allowed-hosts change.** The liveness and - readiness probes call `http://127.0.0.1:8013/api/v2/ping/`; with only the - ingress host allowed, Django answered `400 DisallowedHost`, the probe failed and - the pod restarted in a loop. The chart default keeps the loopback names. -- **Tenancy audit events were never persisted.** `TenantQuotaEvent` and - `TenantIsolationEvent` inherit `CreatedModifiedModel`, which lacks the - `description` column that migration `0205` declares `NOT NULL` — every insert - raised `IntegrityError`. Both models now declare the field (no new migration). -- **RBAC role assignment was broken in 2026.06.0.** `ScanFinding` and - `TenantIsolationEvent` were registered with the default - `parent_field_name='organization'`, but neither model has an `organization` - field. The resulting `FieldDoesNotExist` aborted **every** role-assignment - operation across the platform. They are now registered against their real - parents (`scan_result` and `accessed_organization` respectively). Anyone on - 2026.06.0 who relies on role assignment should upgrade. No data migration is - required — the fix is in model registration only. -- `pytest.ini` referenced the pre-rename `awx.main.tests.settings_for_test` - (no longer exists), which prevented the backend test suite from starting. -- Tenant queue router referenced pre-rename `awx.main.tasks.*` task names. - -## Upgrade - -No schema migrations. Migrations `0209` and `0210` re-create RLS policies only -and are idempotent, so the standard image re-point applies — but the chart's new -required/secure defaults have to be supplied: - -```bash -helm upgrade forail oci://ghcr.io/forail-platform/forail-helm \ - --version 2026.7.0 -n forail \ - --set secrets.forailAdminPassword='' \ - --set 'forail.allowedHosts=forail.example.com\,127.0.0.1\,localhost' \ - --set task.privileged=true --set task.hostCgroup=true # only if you run jobs in-pod -``` - -> **Escape the commas.** Helm's `--set` splits unescaped commas into a list, so -> `--set forail.allowedHosts='a,b,c'` fails to parse. Either escape them as above -> (`a\,b\,c`, inside single quotes so the shell keeps the backslashes) or put the -> value in a values file, where no escaping is needed: -> -> ```yaml -> forail: -> allowedHosts: "forail.example.com,127.0.0.1,localhost" -> ``` - -Before upgrading: - -- **Any deployment that runs jobs** — read **Known issue — upgrading breaks job - execution** at the top of these notes, and plan to apply the workaround (and - re-apply role assignments) as part of the upgrade. Without it the platform - comes up healthy but executes nothing. -- **SAML deployments** — review **Breaking changes — SAML** above and reconfigure - the IdP if needed. -- **Any deployment** — review **Breaking changes — deployment defaults**; an - upgrade that omits the admin password will not render, and one that drops the - loopback hosts will fail its health probes. -- **Multi-tenant deployments** — tenant isolation now fails closed. A request - whose tenant scope cannot be installed is rejected rather than served with - global visibility; verify your tenants resolve correctly in a staging install - first. diff --git a/docs/RELEASE_NOTES_v2026.07.1.md b/docs/RELEASE_NOTES_v2026.07.1.md deleted file mode 100644 index afbb068..0000000 --- a/docs/RELEASE_NOTES_v2026.07.1.md +++ /dev/null @@ -1,90 +0,0 @@ -# Forail 2026.07.1 — Release Notes - -**Release date:** 2026-07-26 -**Based on:** Forail 2026.07.0 -**License:** Apache License 2.0 - ---- - -## Overview - -2026.07.1 is a **patch release with one purpose**: an upgrade no longer leaves a -platform that accepts jobs and runs none of them. It fixes the known issue -published with 2026.07.0, and it removes the manual workaround that release -asked for. - -Nothing else changes. There are no migrations, no configuration changes, no -breaking changes. The frontend, the operator and the assistant are unchanged and -keep their 2026.07.0 / 2026.07.1 / 2026.06.0 versions respectively; only the -backend image and the Helm chart move. - -| Component | Version | -|---|---| -| `forail-backend` | **2026.07.1** | -| Helm chart | **2026.7.1** (pins the backend above) | -| `forail-frontend` | 2026.07.0, unchanged | -| `forail-operator` | 2026.07.1, unchanged | -| `forail-assistant` | 2026.06.0, unchanged | - -## Fixed - -### Jobs no longer stop running after an upgrade or a restart - -Upgrading 2026.06.0 → 2026.07.0 left every launch sitting in `pending` -indefinitely, with only `job_explanation` — *"This job is not ready to start -because there is not enough available capacity"* — to explain it. Project -updates kept working, so the install looked healthy right up until someone -launched a job. - -**What was wrong.** For jobs to run on the node itself, the `default` instance -group has to be a regular group that contains an execution-capable instance. -Two things conspired against that, and either one alone was enough to hang every -launch: - -- `register_queue` assigns instances only when it *creates* a group. On an - upgrade the group already exists, so it assigned nothing and left it empty. -- The task pod re-ran `provision_instance` on **every start** — restart, - eviction, rolling upgrade — and that call hardcoded `node_type='control'` and - re-registered `default` as a ContainerGroup, overwriting whatever the - installer had configured. A control node only orchestrates; it does not - execute. - -The second half is why the 2026.07.0 workaround did not stick: the next task-pod -restart quietly undid it. - -**What changed.** Registration now takes its intent from `FORAIL_NODE_TYPE` — -which the Helm chart and the Compose stack already set — and derives the default -queue from it. An execution-capable pod (`hybrid`, `execution`) gets a regular -instance group containing itself; a control-only pod keeps the ContainerGroup, -exactly as before. Both defaults are unchanged when the variable is unset, so a -multi-node install that has no opinion behaves as it did. The chart's init Job -additionally asserts group membership rather than trusting `register_queue`, so -a newer chart paired with an older image still converges. - -**Verified, not assumed.** On a freshly created cluster: install 2026.06.0, -`helm upgrade` to this release, launch a job — successful in 78 s with a normal -`PLAY RECAP`, with no manual intervention at any point. Two subsequent -`kubectl rollout restart deploy/forail-task` left the state untouched and the -next job succeeded as well. - -## Upgrade - -From **2026.07.0**, a straight image re-point. No migrations, no new required -values: - -```bash -helm upgrade forail oci://ghcr.io/forail-platform/forail-helm \ - --version 2026.7.1 -n forail \ - --set secrets.forailAdminPassword='' \ - --set 'forail.allowedHosts=forail.example.com\,127.0.0.1\,localhost' \ - --set task.privileged=true --set task.hostCgroup=true # only if you run jobs in-pod -``` - -**If you applied the 2026.07.0 workaround**, you can leave it in place — it sets -exactly the state this release converges to on its own. Nothing needs to be -undone. - -From **2026.06.0**, read the [2026.07.0 release -notes](RELEASE_NOTES_v2026.07.0.md) first: the breaking changes, the required -admin password and the SAML defaults all still apply. The known issue documented -there no longer does. diff --git a/docs/chat_plan.md b/docs/chat_plan.md deleted file mode 100644 index 4f95885..0000000 --- a/docs/chat_plan.md +++ /dev/null @@ -1,616 +0,0 @@ -# Forail AI Assistant — Ollama RAG Chat - -> **Status: DELIVERED in v2026.04.0** (shipped as `forail-assistant`; all-in-one -> image with Ollama + ChromaDB + FastAPI consolidated in v2026.05.0). This -> document is retained as the historical design record — it is not pending work. - -Plan for an integrated AI assistant within the Forail platform that uses a local Ollama LLM with RAG (Retrieval Augmented Generation) for fast answers about the platform. - ---- - -## Overview - -``` -┌──────────────────────────────────────────────────────────┐ -│ Forail Web UI │ -│ │ -│ ┌────────────────────────────────┐ ┌────────────────────┐ │ -│ │ Dashboard / Jobs / │ │ AI Assistant │ │ -│ │ Templates / Settings │ │ │ │ -│ │ │ │ User: How do I │ │ -│ │ │ │ create a scheduled │ │ -│ │ │ │ job? │ │ -│ │ │ │ │ │ -│ │ │ │ Bot: To create a │ │ -│ │ │ │ schedule, go to │ │ -│ │ │ │ Templates > ... │ │ -│ │ │ │ │ │ -│ │ │ │ [____________] Ask │ │ -│ └────────────────────────────────┘ └────────────────────┘ │ -└──────────────────────────────────────────────────────────┘ - │ │ - ▼ ▼ -┌──────────────────┐ ┌───────────────────────┐ -│ Forail API │ │ AI Backend │ -│ (Django) │ │ (Django endpoint) │ -│ │ │ │ -│ /api/v2/... │ │ /api/v2/assistant/ │ -└──────────────────┘ └───────────┬───────────┘ - │ - ┌──────────┴──────────┐ - │ │ - ┌────▼─────┐ ┌──────▼──────┐ - │ ChromaDB │ │ Ollama │ - │ (Vector) │ │ (LLM) │ - │ │ │ │ - │ AWX docs │ │ llama3.1:8b │ - │ API help │ │ mistral:7b │ - │ Playbooks│ │ codellama │ - └──────────┘ └─────────────┘ -``` - ---- - -## Key Features - -| Feature | Description | -| ------------------------ | ---------------------------------------------------- | -| **Contextual help** | Knows which page you're on, gives relevant tips | -| **Documentation search** | Searches AWX/Forail docs, API reference, Ansible docs | -| **Error explanation** | Explains errors from job output or API responses | -| **Playbook assistance** | Suggests Ansible modules, fixes YAML syntax | -| **Admin guide** | RBAC, credential setup, inventory management | -| **Streaming responses** | Token-by-token response display (SSE) | - ---- - -## Architecture - -### Tech Stack - -| Component | Technology | Reason | -| -------------- | ------------------------------------ | ------------------------------------------------ | -| **LLM** | Ollama (llama3.1:8b or mistral:7b) | Local, free, data privacy | -| **Vector DB** | ChromaDB | Lightweight, Python native, no external service | -| **Embeddings** | `nomic-embed-text` (Ollama) | Local embedding model, fast | -| **Backend** | Django endpoint (existing Forail API) | No new service, same auth | -| **Frontend** | React chat component | Integrated into existing UI | -| **Streaming** | Server-Sent Events (SSE) | Simpler than WebSocket for unidirectional stream | - -### Why Ollama + RAG? - -1. **Privacy** — All data stays on the server, nothing goes to the cloud -2. **Free** — No API keys, no monthly costs -3. **Offline** — Works without internet -4. **Accuracy** — RAG ensures answers based on actual documentation -5. **Speed** — Local model on GPU gives a response in 2-5 seconds - ---- - -## Phase 1: Ollama Setup and RAG Pipeline (Week 1) - -### 1.1 Ollama Installation - -```bash -# In Docker Compose — add Ollama service -# docker-compose.yml -services: - ollama: - image: ollama/ollama:latest - ports: - - "11434:11434" - volumes: - - ollama_data:/root/.ollama - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: 1 - capabilities: [gpu] - # Fallback for CPU-only: - # Without the GPU section, use a smaller model (tinyllama, phi3:mini) - - chromadb: - image: chromadb/chroma:latest - ports: - - "8000:8000" - volumes: - - chroma_data:/chroma/chroma - -volumes: - ollama_data: - chroma_data: -``` - -### 1.2 Model Selection - -| Model | RAM | Speed | Quality | Recommendation | -| ---------------- | ---- | ------- | ------------ | --------------------------- | -| `tinyllama:1.1b` | 2 GB | Fastest | Basic | CPU-only, small server | -| `phi3:mini` | 4 GB | Fast | Good | CPU with 8GB+ RAM | -| `mistral:7b` | 6 GB | Medium | Excellent | GPU with 8GB+ VRAM | -| `llama3.1:8b` | 8 GB | Medium | Best | GPU with 10GB+ VRAM | -| `codellama:7b` | 6 GB | Medium | Code-focused | For Ansible/YAML assistance | - -Recommendation: **mistral:7b** for a balance of speed and quality. Fallback to **phi3:mini** for CPU-only. - -```bash -# Pull models -ollama pull mistral:7b -ollama pull nomic-embed-text # For RAG embeddings -``` - -### 1.3 RAG Document Pipeline - -```python -# awx/main/management/commands/index_docs.py -# Django management command for indexing documentation - -""" -Documents to index: -1. AWX API Reference (/api/v2/ — all endpoints with descriptions) -2. AWX User Guide (docs/ directory) -3. Ansible Module Index (most common modules) -4. Forail-specific documentation -5. Common error messages and solutions -6. RBAC model and permission explanations -""" - -# Pipeline: -# 1. Load documents (Markdown, RST, HTML) -# 2. Split into chunks (500 tokens, 50 overlap) -# 3. Generate embeddings with nomic-embed-text -# 4. Save to ChromaDB collection -``` - -### 1.4 Document Sources - -``` -docs_to_index/ -├── api_reference/ # Auto-generated from DRF schema -│ ├── jobs.md -│ ├── templates.md -│ ├── inventories.md -│ ├── credentials.md -│ ├── projects.md -│ ├── users.md -│ └── settings.md -├── user_guide/ # User instructions -│ ├── getting_started.md -│ ├── job_templates.md -│ ├── schedules.md -│ ├── workflows.md -│ ├── rbac.md -│ ├── notifications.md -│ └── troubleshooting.md -├── ansible/ # Ansible help -│ ├── common_modules.md -│ ├── playbook_syntax.md -│ ├── inventory_format.md -│ └── vault.md -└── errors/ # Known errors and solutions - ├── common_errors.md - └── debug_guide.md -``` - ---- - -## Phase 2: Django Backend API (Week 1-2) - -### 2.1 API Endpoint - -```python -# awx/api/views/assistant.py - -class AssistantView(APIView): - """ - POST /api/v2/assistant/ - { - "message": "How do I create a scheduled job?", - "context": { - "page": "/templates", - "selected_id": 42 - } - } - - Response (SSE stream): - data: {"token": "To"} - data: {"token": " create"} - data: {"token": " a"} - data: {"token": " scheduled"} - data: {"token": " job"} - data: {"token": ","} - data: {"token": " navigate"} - ... - data: {"done": true, "sources": ["user_guide/schedules.md"]} - """ -``` - -### 2.2 RAG Query Flow - -```python -# awx/main/services/assistant.py - -class ForailAssistant: - def __init__(self): - self.chroma = chromadb.HttpClient(host="chromadb", port=8000) - self.collection = self.chroma.get_collection("forail_docs") - self.ollama_url = "http://ollama:11434" - - def query(self, message: str, context: dict = None): - # 1. Generate embedding for the question - embedding = self._embed(message) - - # 2. Find relevant documents (top 5) - results = self.collection.query( - query_embeddings=[embedding], - n_results=5 - ) - - # 3. Compose prompt with context - docs_context = "\n\n".join(results["documents"][0]) - - system_prompt = f"""You are Forail Assistant, an AI helper for the Forail -infrastructure automation platform (based on AWX/Ansible Tower). - -Answer questions using ONLY the following documentation context. -If you don't know the answer, say so — don't make things up. - -Be concise and practical. Give step-by-step instructions when helpful. -If the user is on a specific page, tailor your answer to that context. - -Documentation context: -{docs_context}""" - - # 4. Add page context if available - if context and context.get("page"): - system_prompt += f"\n\nUser is currently on page: {context['page']}" - - # 5. Stream response from Ollama - yield from self._stream_chat(system_prompt, message) - - def _embed(self, text: str) -> list[float]: - resp = requests.post(f"{self.ollama_url}/api/embeddings", json={ - "model": "nomic-embed-text", - "prompt": text - }) - return resp.json()["embedding"] - - def _stream_chat(self, system: str, message: str): - resp = requests.post( - f"{self.ollama_url}/api/chat", - json={ - "model": "mistral:7b", - "messages": [ - {"role": "system", "content": system}, - {"role": "user", "content": message} - ], - "stream": True - }, - stream=True - ) - for line in resp.iter_lines(): - if line: - data = json.loads(line) - if not data.get("done"): - yield data["message"]["content"] -``` - -### 2.3 Django URL Config - -```python -# awx/api/urls/assistant.py -urlpatterns = [ - path('assistant/', AssistantView.as_view(), name='assistant'), - path('assistant/history/', AssistantHistoryView.as_view(), name='assistant-history'), -] -``` - -### 2.4 Chat History Model (Optional) - -```python -# awx/main/models/assistant.py - -class ChatMessage(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE) - role = models.CharField(max_length=10) # 'user' or 'assistant' - content = models.TextField() - sources = models.JSONField(default=list) - page_context = models.CharField(max_length=255, blank=True) - created = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ['created'] -``` - ---- - -## Phase 3: Frontend Chat Component (Week 2-3) - -### 3.1 Chat Widget - -``` -┌──────────────────────────────────────────────┐ -│ Forail Assistant ─ × │ -├──────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ 🤖 Hi! I'm your Forail assistant. │ │ -│ │ Ask me anything about the platform. │ │ -│ └─────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ 👤 How do I create a job template │ │ -│ │ with survey variables? │ │ -│ └─────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ 🤖 To create a job template with │ │ -│ │ survey variables: │ │ -│ │ │ │ -│ │ 1. Go to **Templates** > **Add** │ │ -│ │ 2. Fill in name, project, playbook │ │ -│ │ 3. Enable **Survey Enabled** │ │ -│ │ 4. Click **Add Survey** to define │ │ -│ │ variables (text, password, etc) │ │ -│ │ 5. Save the template │ │ -│ │ │ │ -│ │ 📎 Source: user_guide/templates.md │ │ -│ └─────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ 🤖 ▊ (typing...) │ │ -│ └─────────────────────────────────────────┘ │ -│ │ -├──────────────────────────────────────────────┤ -│ ┌────────────────────────────────┐ ┌────┐ │ -│ │ Ask anything... │ │Send│ │ -│ └────────────────────────────────┘ └────┘ │ -└──────────────────────────────────────────────┘ -``` - -### 3.2 Frontend Files - -``` -awx/ui_next/src/ -├── components/ -│ └── assistant/ -│ ├── AssistantPanel.tsx # Main chat panel (slide-in sidebar) -│ ├── ChatMessage.tsx # Individual message (user/bot) -│ ├── ChatInput.tsx # Input field + send button -│ └── AssistantButton.tsx # Floating button to open -├── api/hooks/ -│ └── useAssistant.ts # SSE streaming hook -└── stores/ - └── assistant.ts # Zustand store (history, open/close) -``` - -### 3.3 SSE Streaming Hook - -```typescript -// useAssistant.ts -export function useAssistant() { - const [messages, setMessages] = useState([]); - const [isStreaming, setIsStreaming] = useState(false); - - async function sendMessage(text: string, pageContext?: string) { - // Add user message - setMessages((prev) => [...prev, { role: "user", content: text }]); - setIsStreaming(true); - - // Open SSE stream - const response = await fetch("/api/v2/assistant/", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: text, context: { page: pageContext } }), - }); - - const reader = response.body!.getReader(); - const decoder = new TextDecoder(); - let botMessage = ""; - - // Add empty bot message - setMessages((prev) => [...prev, { role: "assistant", content: "" }]); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - // Parse SSE data lines - for (const line of chunk.split("\n")) { - if (line.startsWith("data: ")) { - const data = JSON.parse(line.slice(6)); - if (data.token) { - botMessage += data.token; - // Update last message - setMessages((prev) => { - const updated = [...prev]; - updated[updated.length - 1] = { - role: "assistant", - content: botMessage, - }; - return updated; - }); - } - } - } - } - - setIsStreaming(false); - } - - return { messages, sendMessage, isStreaming }; -} -``` - ---- - -## Phase 4: Documentation Indexing (Week 3) - -### 4.1 Management Command - -```bash -# Index all documents -awx-manage index_docs - -# Re-index after an update -awx-manage index_docs --rebuild - -# Test RAG search -awx-manage query_docs "how to create inventory" -``` - -### 4.2 Auto-generating API Documentation - -```python -# awx/main/management/commands/generate_api_docs.py -# Iterates through all DRF ViewSets and generates Markdown documentation -# with endpoints, parameters, example request/response - -# Result: docs_to_index/api_reference/*.md -``` - -### 4.3 Suggested Documents to Write - -Priority 1 (most common questions): - -- How to create a Job Template -- How to create an Inventory (manually, from a source) -- How to use Credentials -- How to set up a Schedule -- How RBAC works (Organizations, Teams, Roles) -- What is an Execution Environment - -Priority 2: - -- Workflow visual editor -- Notification templates -- Smart inventories -- Survey variables -- Webhook integration -- Troubleshooting FAQ - ---- - -## Phase 5: Advanced Features (Week 3-4) - -### 5.1 Contextual Hints - -The assistant knows which page the user is on and gives relevant tips: - -``` -Page: /templates/job_template/new -→ "Need help creating a job template? I can explain each field." - -Page: /jobs/42 (failed job) -→ "I see this job failed. Want me to analyze the error output?" - -Page: /settings/authentication -→ "I can help you configure LDAP, SAML, or OAuth2 authentication." -``` - -### 5.2 Error Analysis - -The user can send an error from the job output and get an explanation: - -``` -User: Job #42 failed with "No hosts matched" -Bot: This error means the inventory doesn't contain hosts matching - your "limit" pattern. Check: - 1. Your inventory has hosts added - 2. The "Limit" field in the template matches actual host names - 3. Host patterns use correct syntax (e.g., "web*", "group1:&group2") -``` - -### 5.3 Ansible Playbook Helper - -``` -User: How do I copy a file to remote hosts? -Bot: Use the `ansible.builtin.copy` module: - - - name: Copy config file - ansible.builtin.copy: - src: files/app.conf - dest: /etc/app/app.conf - owner: root - mode: '0644' -``` - ---- - -## Hardware Requirements - -### Minimum (CPU-only, phi3:mini) - -- CPU: 4 core -- RAM: 8 GB (4 for the model + 4 for everything else) -- Disk: 5 GB for the model + 2 GB for ChromaDB -- Response time: 10-20 seconds - -### Recommended (GPU, mistral:7b) - -- CPU: 4+ core -- RAM: 16 GB -- GPU: NVIDIA with 8+ GB VRAM (RTX 3060+, T4, A10) -- Disk: 10 GB for the model + 2 GB for ChromaDB -- Response time: 2-5 seconds - -### Production (GPU, llama3.1:8b) - -- CPU: 8+ core -- RAM: 32 GB -- GPU: NVIDIA with 12+ GB VRAM (RTX 4070+, A100) -- Disk: 15 GB for the model + 5 GB for ChromaDB -- Response time: 1-3 seconds - ---- - -## Integration with Mobile Application - -The chat will also be available in the Forail Mobile app: - -``` -Android: -- Same API endpoint (/api/v2/assistant/) -- Chat screen in bottom navigation -- Voice input (Android Speech-to-Text) -- Offline fallback: download FAQ locally -``` - ---- - -## Timeline - -``` -Week 1: Ollama + ChromaDB setup, RAG pipeline, document indexing -Week 1-2: Django backend API (SSE streaming, RAG query) -Week 2-3: Frontend chat component (panel, streaming, markdown render) -Week 3: Documentation — writing/generating docs for indexing -Week 3-4: Advanced features (context hints, error analysis) -Week 4: Testing, optimization, polish -``` - -**Total: ~4 weeks** - ---- - -## MVP Priorities - -Minimum viable product: - -1. **Ollama service** in Docker Compose with mistral:7b -2. **ChromaDB** with indexed AWX documentation -3. **Django endpoint** /api/v2/assistant/ with SSE streaming -4. **Frontend chat panel** with send/receive and markdown rendering -5. **10-15 documents** indexed (API ref + user guide basics) - -Post-MVP: - -- Chat history (save conversations) -- Contextual hints per page -- Error analysis from job output -- Voice input in the mobile app -- Fine-tuning the model on Forail-specific data -- Multi-language support diff --git a/docs/ci-pipeline-reference.md b/docs/ci-pipeline-reference.md deleted file mode 100644 index 4cef4aa..0000000 --- a/docs/ci-pipeline-reference.md +++ /dev/null @@ -1,224 +0,0 @@ -# Forail CI/CD Pipeline — Reference Document - -This document describes the intended production CI/CD pipeline for Forail. -The pipeline enforces code quality gates — code cannot be merged unless -all required stages pass. - ---- - -## Pipeline Flow - -``` -push / MR git tag v* - │ │ - ▼ ▼ -┌────────┐ ┌────────┐ -│ Lint │ │ Lint │ -│ python │ ◄── parallel ──► │frontend│ -└───┬────┘ └───┬────┘ - │ must pass │ must pass - ▼ ▼ -┌────────┐ ┌────────┐ -│ Test │ │ Test │ -│ python │ ◄── parallel ──► │frontend│ -└───┬────┘ └───┬────┘ - │ must pass │ must pass - ▼ ▼ -┌────────────────────────────────────┐ -│ Build │ (only on default branch + tags) -│ CentOS image │ Ubuntu image │ -└───────────┬────────────────────────┘ - │ must pass - ▼ -┌────────────────────────────────────┐ -│ Security │ (allow_failure: true) -│ pip-audit │ Trivy │ -└───────────┬────────────────────────┘ - │ - ▼ -┌────────────────────────────────────┐ -│ Release │ (only on tags v*) -│ GitLab Registry │ Harbor │ -│ (automatic) │ (manual) │ -└────────────────────────────────────┘ -``` - ---- - -## Stage Details - -### Lint (required — blocks merge if fails) - -| Job | Image | What it checks | Command | -| --------------- | ------------------ | ------------------------------------- | ------------------------------------ | -| `lint:python` | `python:3.12-slim` | PEP8, undefined names, unused imports | `flake8 forail/ --count --statistics` | -| `lint:frontend` | `node:20-slim` | TypeScript type errors | `npx tsc --noEmit` | - -**Triggers:** Every push, every MR, every tag. - -### Test (required — blocks merge if fails) - -| Job | Image | Services | What it checks | Command | -| -------------------- | ------------------ | ---------------------- | ----------------- | ------------------------------------- | -| `test:python-unit` | `python:3.12-slim` | PostgreSQL 15, Redis 7 | 1083 unit tests | `pytest forail/main/tests/unit/ -x -q` | -| `test:frontend-unit` | `node:20-slim` | — | Vitest test suite | `npx vitest run` | - -**Triggers:** Every push, every MR, every tag. - -**Watch out:** Python tests need real PostgreSQL and Redis services. The `-x` flag -stops on the first failure for faster feedback. - -### Build (only default branch + tags) - -| Job | Base image | Output | -| -------------- | --------------- | ------------------------------------------------------ | -| `build:centos` | CentOS Stream 9 | `${IMAGE}:${VERSION}-centos`, `${IMAGE}:centos-latest` | -| `build:ubuntu` | Ubuntu 24.04 | `${IMAGE}:${VERSION}-ubuntu`, `${IMAGE}:ubuntu-latest` | - -**Does not run on feature branches** — only when merged to default branch or tagged. - -Both images are multi-stage builds: - -1. Node.js stage: builds React frontend (`npm run build`) -2. Python stage: installs dependencies, builds sdist package -3. Runtime stage: minimal image with only runtime dependencies - -### Security (informational — does NOT block merge) - -| Job | Tool | What it scans | -| -------------------- | --------- | --------------------------------- | -| `security:pip-audit` | pip-audit | Known CVEs in Python dependencies | -| `security:trivy` | Trivy | Known CVEs in Docker image layers | - -Both have `allow_failure: true` — they report vulnerabilities but don't block the pipeline. - -### Release (only on git tags `v*`) - -| Job | Destination | Trigger | -| ------------------------- | ------------------------- | ---------------------------- | -| `release:gitlab-registry` | GitLab Container Registry | Automatic on tag | -| `release:harbor` | Harbor (`ghcr.io`) | **Manual** (click to deploy) | - -The CentOS image is tagged as the primary (`:latest`, `:${VERSION}`). -Ubuntu is available as `:${VERSION}-ubuntu`. - ---- - -## Workflow Rules — When the Pipeline Runs - -```yaml -workflow: - rules: - # Always run for release tags - - if: $CI_COMMIT_TAG =~ /^v/ - - # Skip entirely if ONLY docs/images changed - - changes: - - "docs/**/*" - - "**/*.md" - - "**/*.png" - - "**/*.jpg" - - "**/*.svg" - when: never - - # Run for everything else - - when: always -``` - -**What this means:** - -- Push changes to `forail/`, `requirements/`, `tools/`, etc. → pipeline runs, must pass -- Push changes to only `docs/`, `*.md`, `*.png` → pipeline does NOT run -- Push a tag `v2026.03.0` → full pipeline including release stage -- Mix of code + docs changes → pipeline runs (code changes take priority) - ---- - -## CI Variables (GitLab Settings → CI/CD → Variables) - -### Required for build stage - -| Variable | Source | Description | -| ---------------------- | ------------------ | ----------------------------- | -| `CI_REGISTRY` | GitLab (automatic) | GitLab container registry URL | -| `CI_REGISTRY_USER` | GitLab (automatic) | Registry username | -| `CI_REGISTRY_PASSWORD` | GitLab (automatic) | Registry password | -| `CI_REGISTRY_IMAGE` | GitLab (automatic) | Full image path | - -### Required for Harbor release - -| Variable | Set manually | Description | -| ----------------- | ------------ | ---------------------------- | -| `HARBOR_USER` | Yes | Harbor registry username | -| `HARBOR_TOKEN` | Yes (masked) | Harbor registry access token | -| `HARBOR_REGISTRY` | Yes | Registry URL (`ghcr.io`) | - ---- - -## Version Resolution - -``` -git tag v2026.03.0 → VERSION=2026.03.0 (release build) -no tag → VERSION=abc1234 (dev build, commit SHA) -``` - -The version is used for: - -- Docker image tags -- `SETUPTOOLS_SCM_PRETEND_VERSION` build arg (Python package version) -- Release notes - ---- - -## Caching - -| Cache | Key | What it caches | -| ----- | --------------------------- | ------------------------------------------- | -| pip | `pip-${CI_COMMIT_REF_SLUG}` | Python packages (`.cache/pip`) | -| npm | `npm-${CI_COMMIT_REF_SLUG}` | Node modules (`forail/ui_next/node_modules`) | -| trivy | `trivy` | Vulnerability database (`.trivycache/`) | - -Caches are per-branch. The lint stage uses `pull-push` (reads and writes), -test stage uses `pull` (reads only) for npm to avoid cache conflicts. - ---- - -## How to Enforce Pipeline in GitLab - -To make the pipeline a hard requirement for merging: - -1. **Settings → Repository → Protected branches** - - Protect `devel` and `main`/`modernization` - - Set "Allowed to merge" to Maintainers - -2. **Settings → Merge requests** - - Enable "Pipelines must succeed" - - This blocks the Merge button until lint + test pass - -3. **Settings → CI/CD → General pipelines** - - Set timeout to 30 minutes (prevents hung jobs) - -With these settings, no code reaches the main branches without passing -flake8, TypeScript checks, Python unit tests, and frontend tests. - ---- - -## Running the Pipeline Locally - -If you want to verify before pushing: - -```bash -# Python lint -flake8 forail/ --count --statistics --max-line-length=160 - -# TypeScript check -cd forail/ui_next && npx tsc --noEmit - -# Python tests (inside Vagrant VM) -python -m pytest forail/main/tests/unit/ -x -q --tb=short - -# Frontend tests -cd forail/ui_next && npx vitest run -``` - -If all four pass locally, the pipeline will pass on GitLab. diff --git a/docs/future_development_plan.md b/docs/future_development_plan.md deleted file mode 100644 index a1c0c80..0000000 --- a/docs/future_development_plan.md +++ /dev/null @@ -1,633 +0,0 @@ -# Forail — Future Development Plan - -Post-release roadmap for Forail beyond v2026.03.0. -Organized by priority tiers with estimated effort and dependencies. - ---- - -## Tier 1: High Impact, Near-Term (Q2-Q3 2026) - -Features that address the most common community pain points and provide -immediate competitive advantage over AWX and alternatives. - -### 1.1 Dynamic Surveys --- COMPLETED (v2026.04.0) - -**Problem:** AWX surveys only support static, hardcoded choices. Users cannot -populate dropdown options from inventory, host facts, or external APIs. -This is the single most upvoted feature request in the AWX community. - -**Solution:** - -- Add a `dynamic_choices` field to survey question spec -- Support three sources: Jinja2 template (from inventory/facts), API endpoint - (external URL returning JSON array), and database query (hosts, groups, projects) -- Evaluate choices at launch time, not at template save time -- Cache results with configurable TTL to avoid slow launches -- Frontend: async dropdown that fetches choices when the launch dialog opens - -**Effort:** 2-3 weeks -**Files:** `awx/main/models/jobs.py`, `awx/api/views/job_templates.py`, -`awx/ui_next/src/components/LaunchDialog.tsx` - ---- - -### 1.2 Event-Driven Automation (EDA) --- COMPLETED (v2026.04.0) - -**Problem:** AWX can only run jobs on schedules or manual triggers. There is no -way to react to real-time events (monitoring alerts, Git pushes, cloud events). -AAP 2.5+ has this as an exclusive feature. - -**Solution:** - -- Add an event router service that accepts inbound webhooks and evaluates - them against user-defined rules (YAML rulebooks) -- Rule format: `source` (webhook path/filter) + `condition` (Jinja2 expression) - - `action` (launch job template, workflow, or notification) -- Integrate with common sources: GitHub/GitLab webhooks, Alertmanager, - PagerDuty, Datadog, CloudWatch, generic HTTP POST -- Store rules as a new model (`EventRule`) with RBAC -- Outbound webhooks: push job status/completion to external systems - -**Effort:** 4-6 weeks -**Dependencies:** None (builds on existing webhook receiver in AWX) - ---- - -### 1.3 Drift Detection and Change Tracking --- COMPLETED (v2026.04.0) - -**Problem:** AWX runs playbooks but does not track what changed between runs. -There is no way to detect configuration drift or prove compliance. -Ascender's "Ledger" product fills this gap commercially. - -**Solution:** - -- Capture host facts (`ansible_facts`) after each playbook run and store - as snapshots in a `HostFactSnapshot` model -- Compare snapshots between runs to detect drift (new packages, changed - configs, modified users/groups, open ports) -- Dashboard widget showing drift summary across hosts -- Alert rules: notify when drift exceeds threshold or specific facts change -- Compliance report export (PDF/CSV) showing baseline vs current state - -**Effort:** 3-4 weeks -**Dependencies:** Fact caching must be enabled on job templates - ---- - -### 1.4 AI Assistant (Ollama RAG) --- COMPLETED (v2026.04.0) - -**Problem:** Users need contextual help while using the platform. Error messages -from failed jobs are often cryptic. New users struggle with RBAC, credentials, -and inventory setup. - -**Solution:** (detailed in `docs/chat_plan.md`) - -- Ollama LLM (mistral:7b or llama3.1:8b) running as a Docker service -- ChromaDB vector store with indexed Forail/Ansible documentation -- Django API endpoint `/api/v2/assistant/` with SSE streaming -- Frontend chat panel integrated into Forail UI -- Context-aware: knows which page the user is on -- Error analysis: explain failed job output - -**Effort:** 4 weeks -**Dependencies:** GPU recommended (CPU fallback with smaller model) - ---- - -### 1.5 Improved Audit Trail --- COMPLETED (v2026.04.0) - -**Problem:** AWX activity stream records changes but lacks detail for compliance -auditing. No way to generate audit reports or track credential access. - -**Solution:** - -- Extend activity stream with: source IP, user agent, session ID -- Add credential access logging (who used which credential, when, on which host) -- Immutable audit log table (append-only, no updates or deletes) -- Audit report generator: filter by user/resource/date range, export PDF/CSV -- Retention policies: auto-archive old records to cold storage -- SIEM integration: structured JSON log export for Splunk/ELK/Datadog - -**Effort:** 2-3 weeks -**Dependencies:** None - ---- - -## Tier 2: Strategic Features (Q3-Q4 2026) - -Features that position Forail as a modern platform beyond basic AWX capabilities. - -### 2.1 Self-Service Portal --- COMPLETED (v2026.04.0) - -**Problem:** Non-technical users (helpdesk, operations, managers) cannot use -AWX without training. They need a simplified interface to run pre-approved -automation without understanding templates, inventories, or credentials. - -**Delivered:** - -- `ServiceCatalogItem` model wraps an existing JobTemplate or - WorkflowJobTemplate with portal metadata (icon, category, tags, - `requires_approval`, `approver_team`). -- `ServiceRequest` lifecycle (`pending_approval` → `approved` / - `rejected` → `running` → `successful` / `failed` / `canceled`) with - `submit/approve/reject` methods and a post_save signal that mirrors - terminal UnifiedJob status back onto the linked request. -- Approver permission model: superuser, `approver_team` membership, or - org admin fallback when no team is set. -- REST API mounted at `/api/v2/service_catalog_items/` and - `/api/v2/service_requests/` (CRUD + `launch_data`, `submit`, - `approve`, `reject`, `pending_approvals` inbox). -- Frontend: Service Portal (catalog grid), multi-step - `ServiceRequestDialog` (justification → workflow survey → per-node - surveys → confirm), My Requests, Approvals inbox, Catalog Admin CRUD. -- Reuses existing launch pipeline (`create_unified_job`) and the - `SurveyQuestionInput` extracted from `WorkflowLaunchDialog` — - no duplication. -- Tests: 22 standalone backend lifecycle tests + 10 frontend - type-shape tests. -- See `forail-backend/docs/17-self-service-portal.md` for full - architecture. - ---- - -### 2.2 Policy-as-Code --- COMPLETED (v2026.04.0) - -**Problem:** No governance framework to enforce rules like "production jobs -must have approval", "credentials must rotate every 90 days", or "only -signed playbooks can run on production inventories". - -**Delivered:** - -- `Policy` model storing Rego modules + metadata; pushed to a - `forail-opa` sidecar (OPA 0.69.0) on save via post_save signal. -- `PolicyDecision` audit row per evaluation hit; full launch context - preserved as JSON. -- `evaluator.evaluate_launch()` hooked into `JobTemplateLaunch.post`, - `WorkflowJobTemplateLaunch.post`, and `AdHocCommandList.create`, - strictly between `create_unified_job` and `signal_start`. -- Three-tier enforcement: global `OPA_ENABLED` kill switch + - per-organization `policy_enforcement` (`none/warn/enforce`) + - per-policy `enforcement` (`warn/enforce`). Org `warn` caps any - policy from blocking. All combinations covered by a unit-tested - resolver. -- Configurable fail mode: `OPA_FAIL_MODE=allow` (default, fail-open - with audit) or `deny` (fail-closed) when the OPA sidecar is - unreachable. -- REST API at `/api/v2/policies/` (CRUD + enable/disable + dry-run - test endpoint) and `/api/v2/policy_decisions/` (audit log). -- Frontend: Policies CRUD page with sync status badges, PolicyForm - with Rego editor and dry-run panel, PolicyDecisions audit log with - expandable context viewer. Compliance sidebar group extended with - Policies and Policy Decisions entries. -- `forail-opa` sidecar added to `forail-deploy/docker-compose.yml` - (image `openpolicyagent/opa:0.69.0-rootless`, healthcheck against - `/health`). -- 19 standalone backend tests + 6 frontend type-shape tests, 0 TS - errors. -- See `forail-backend/docs/19-policy-as-code.md` for the full - architecture. - ---- - -### 2.3 Modern Authentication (OIDC + WebAuthn) --- COMPLETED (v2026.04.0) - -**Problem:** AWX supports LDAP, SAML, and social auth but lacks native OIDC -and passwordless login. WebAuthn/passkeys are now the standard for -phishing-resistant authentication. - -**Delivered:** - -- OIDC client wired through the existing - `social_core.backends.open_id_connect.OpenIdConnectAuth` (no new - dependency — already vendored). Configuration via Settings → Generic - OIDC. New settings: button label, scope override, organization map, - team map. JIT user provisioning + org/team mapping reuses - `forail.sso.social_pipeline`. -- WebAuthn / FIDO2 via `py_webauthn==2.5.2`: - - `WebAuthnCredential` model + 5-minute challenge stores. - - REST API at `/api/v2/webauthn/credentials/`, - `register/{begin,complete}/`, `authenticate/{begin,complete}/`. - - Replay protection via monotonic sign-count guard. - - Origin / RP-ID derived from request — same image works on any host. -- Org-level MFA enforcement: `Organization.webauthn_required` - (`none`/`admins`/`all`) + `WebAuthnMfaEnforcementMiddleware` that - flips `session.mfa_pending` when policy applies. -- Frontend: `/me/security` credential management page, - `/auth/mfa` post-primary-auth interstitial, **Sign in with security - key** and **Sign in with OIDC** buttons on the login page, - TopBar dropdown "Security" entry. Browser side uses - `@simplewebauthn/browser` v13. -- 16 standalone backend tests (policy resolver, replay guard, TTL, - base64url helpers) + 5 frontend type-shape tests. -- See `forail-backend/docs/18-oidc-webauthn.md` for the full architecture. - ---- - -### 2.4 Workflow Survey Prompts per Node --- COMPLETED (v2026.04.0) - -**Problem:** Workflow job templates only support surveys at the workflow level. -Users cannot prompt for different variables at individual job template nodes -within a workflow. - -**Solution:** - -- Add `survey_spec` to `WorkflowJobTemplateNode` model -- At launch time, merge workflow-level and node-level survey prompts -- Frontend: multi-step launch dialog showing prompts grouped by node -- Support `ask_variables_on_launch` per node (not just per workflow) - -**Effort:** 2-3 weeks -**Dependencies:** None - ---- - -### 2.5 Automation Analytics Dashboard --- COMPLETED (v2026.04.0) - -**Problem:** AWX provides basic metrics but no insight into automation value, -trends, or efficiency. Managers cannot answer "how much time did automation -save this month?" - -**Solution:** - -- New analytics models tracking: job duration trends, success/failure rates - over time, most-used templates, busiest hosts, automation coverage -- Time savings calculator: estimated manual time vs automated time -- Dashboard with Recharts visualizations: job trends, host coverage map, - template usage heatmap, failure analysis -- Scheduled email reports: weekly/monthly automation summary -- API: `/api/v2/forail_analytics/` with date range filters - -**Effort:** 3 weeks -**Dependencies:** None (uses existing job data) - ---- - -## Tier 3: Platform Evolution (2027+) - -Long-term features for enterprise scale and ecosystem growth. - -### 3.1 Plugin Architecture - -- Microkernel design: core handles jobs, scheduling, inventory; everything - else is a plugin (credential backends, notification channels, inventory - sources, SCM providers) -- Plugin SDK with documented hooks: pre-job, post-job, credential resolution, - inventory sync, notification dispatch -- Plugin registry with install/update/remove via UI -- Sandboxed execution: plugins run in isolated containers - -**Effort:** 8-12 weeks - ---- - -### 3.2 Multi-Tenancy --- COMPLETED (v2026.04.0) - -**Problem:** Forail had Organizations and per-org RBAC, but running a single -install for multiple customers required trusting RBAC to be airtight, manually -provisioning every customer's org/admin/team, manually enforcing "fair use" -(there were no quotas — one noisy tenant could hog all Celery workers), and -manually skinning the UI per customer. - -**Delivered (v1 — soft multi-tenancy, no new compose service):** - -- `Organization` extended with 11 additive fields: `is_tenant_root`, - `tenant_max_concurrent_jobs`, `tenant_max_daily_launches`, - `tenant_max_hosts`, `tenant_max_storage_mb`, `tenant_isolation_strict`, - `tenant_logo_url`, `tenant_primary_color`, `tenant_secondary_color`, - `tenant_custom_domain` (indexed), `tenant_contact_email`. All default to - safe values — existing orgs are untouched (zero-downtime migration - `0204_multi_tenancy`). -- `TenantUsage` (OneToOne with Organization) — rolling counters for - concurrent jobs, launches-today, hosts_count, storage_mb_used, with a - `launches_today_window_start` for calendar-UTC day rollover. -- `TenantQuotaEvent` — one audit row per quota decision (allow or block), - mirrors the `PolicyDecision` shape; cached `organization_name` so the row - survives org delete. -- `TenantIsolationEvent` — v1 audit-only row for cross-tenant reads observed - when `tenant_isolation_strict=True` (blocking deferred to v2). -- `forail/main/tenancy/` package: pure helpers (`check_quota_value`, - `is_window_expired`, `reset_daily_window`, `format_quota_message`, - `normalize_branding_host`, `validate_hex_color`), `quota.py` with - `check_tenant_quota` + `on_job_finished`, `provisioning.py` with atomic - `provision_tenant` (Org + admin User + default Team + TenantUsage in one - transaction), `branding.py` with `get_branding_for_host`, `usage.py` with - `recalculate_tenant_usage` drift reconciliation, `isolation.py` - middleware. -- Launch hook inserted **before** Policy-as-Code and IaC Scanning in - `JobTemplateLaunch.post`, `WorkflowJobTemplateLaunch.post`, and - `AdHocCommandList.create`. Blocked launches return HTTP **429** with the - quota kind that tripped, inside the `forail.launch` OTel span - (adds `quota_blocked=` attribute for trace filtering). -- Job-finished signal decrements `concurrent_jobs_count`; Celery beat - `recalculate_tenant_usage_all` runs every - `TENANCY_QUOTA_RECALC_INTERVAL_S` seconds and also reconciles the - concurrent counter against actual running UnifiedJob count. -- Settings (System category): `TENANCY_ENABLED` (global kill switch, - default off), `TENANCY_DEFAULT_MAX_CONCURRENT_JOBS`, - `TENANCY_DEFAULT_MAX_DAILY_LAUNCHES`, `TENANCY_QUOTA_RECALC_INTERVAL_S` - (default 300s). -- REST API: `/api/v2/tenants/` (superuser CRUD + provision + recalculate), - `/api/v2/tenant_quota_events/` (audit log), and - `/api/v2/branding/?host=` — **PUBLIC** (no auth classes) so - the frontend can skin itself before login. Returns 404 on miss. -- Frontend: `/tenants`, `/tenants/new`, `/tenants/:id`, `/tenants/:id/edit`, - `/tenant_quota_events` pages with usage bars, branding preview, color - pickers, quota inputs (empty = unlimited), danger-zone delete. NEW - sidebar group **Tenancy** (Building2 + Activity icons) above Compliance. -- Boot-time branding: `src/branding/applyBranding.ts` called from - `src/main.tsx` **before** React mounts. Fetches - `/api/v2/branding/?host=window.location.hostname` with no credentials, - sets CSS variables `--forail-primary` / `--forail-secondary` on `:root`, - swaps the favicon and document title, caches in localStorage for 5 min. - Tailwind exposes the CSS vars as `colors.brand.primary`/`secondary`. -- No new compose service — tenancy piggybacks on the existing `forail-web` - and `forail-task` containers and is gated by `TENANCY_ENABLED`. -- Standalone tests in `tests_standalone/test_tenancy.py` cover all pure - helpers, QuotaResult aggregation, window rollover edge cases, branding - host normalization, and provisioning payload validation. -- See `forail-backend/docs/22-multi-tenancy.md` for the full architecture. - -**Deferred to v2:** - -- Postgres row-level security policies (real DB-level isolation). -- Strict-mode enforcement (cross-tenant reads blocked, not just audited). -- Per-tenant API rate limiting at the middleware layer (token bucket). -- Per-tenant Celery queues (single shared queue + quota is the v1 fairness - story). -- Custom-domain TLS provisioning (Let's Encrypt automation). -- Billing / metering hooks. -- Tenant-scoped LDAP/SAML/OIDC federation. - -**Effort:** 6-8 weeks - ---- - -### 3.3 Kubernetes Operator --- COMPLETED (v2026.05.0) - -**Delivered:** - -- 9 CRDs spanning the full Forail resource model: `Inventory`, - `Credential`, `JobTemplate`, `Schedule`, `Project`, `Organization`, - `Team`, `Workflow`, `ForailInstance` (in `forail-operator/api/v1alpha1/`). -- Reconciliation controllers for each CRD with finalizer-driven - cleanup, 60-second drift reconcile, and Secret watch for instant - Credential rotation. -- Helm chart (`forail-operator/helm/`) with full RBAC + all 9 CRD - manifests; chart bumped to 1.0.0. -- OLM bundle: `config/manifests/bases/forail-operator.clusterserviceversion.yaml` - with `alm-examples` for all 9 CRDs, `spec.icon`, `bundle.Dockerfile`, - Makefile targets (`bundle`, `bundle-build`, `bundle-validate`, - `catalog-build`). `operator-sdk bundle validate` clean. -- **Multi-cluster control plane** via `ForailInstance` CR + - `forailapi.ClientPool` (per-CR routing to different Forail backends - by name + tokenSecretRef; generation-gated cache invalidation). -- **Declarative Workflow DAG**: `spec.nodes[]` keyed by `identifier`, - three edge types (`successNodes`, `failureNodes`, `alwaysNodes`) - reconciled against `/workflow_job_template_nodes/`. -- e2e tested live against 3m+4w k3s 1.30 cluster - (see `forail-dev-cluster/` for the topology). - -**Effort spent:** 5 days (2026-05-17 → 2026-05-21). - ---- - -### 3.4 IaC Scanning and Supply Chain Security --- COMPLETED (v2026.04.0) - -**Problem:** Policy-as-Code (Tier 2.2) gates launches on metadata (who, -what, where) but never inspects the playbook body. Nothing prevents a -playbook from disabling SELinux, embedding a secret, using `shell:` -with unquoted user input, pulling an unpinned role, or importing a -Python package with a known CVE. - -**Delivered:** - -- `Scanner` model — one row per configured tool (ansible-lint, checkov, - pip-audit) with severity threshold + enforcement + `applies_to`. -- `ScanResult` audit row per scanner execution (status ok / warn / - blocked / error / timeout, duration, finding_count, highest severity, - truncated raw output, cached scanner_name so rows survive delete). -- `ScanFinding` child row per finding at or above threshold - (rule_id, severity, file_path, line, message). -- `forail/main/scanning/runner.py` — subprocess runner with per-scanner - timeout, project checkout path + playbook resolution, output parsing, - ScanResult + ScanFinding persistence, aggregate `ScanRunResult`. -- Tool adapters in `forail/main/scanning/tools/` — one module per CLI - (`ansible-lint -f json --strict`, `checkov -o json`, `pip-audit ---format json`) with severity normalization to info/low/medium/ - high/critical. -- Pure helpers: `severity_at_or_above`, `effective_enforcement`, - `aggregate_status`, `fail_mode_decision` — unit-tested standalone. -- Hook inserted **after** the Policy-as-Code hook in - `JobTemplateLaunch.post`, `WorkflowJobTemplateLaunch.post`, and - `AdHocCommandList.create`. Blocked launches return 403 with - `reasons`; warn launches append a one-liner to `job_explanation`. -- Settings: `SCANNER_ENABLED` (master switch), `SCANNER_TIMEOUT_S` - (per-scanner subprocess timeout), `SCANNER_FAIL_MODE` (allow / - deny on timeout/crash), `SCANNER_RAW_OUTPUT_MAX` — all in the - Security category. -- REST API at `/api/v2/scanners/` (CRUD + enable/disable) and - `/api/v2/scan_results/` (audit log with embedded findings). -- Frontend: Scanners CRUD page with tool badge, severity threshold, - enforcement badge, last-run status, enable toggle; ScannerForm with - tool dropdown, severity selector, JSON config editor, applies_to - checkboxes; ScanResults audit table with status filter and finding - drawer. Compliance sidebar group extended with Scanners and Scan - Results entries. -- Scanner CLIs bundled into the `forail-backend` image (installed into - `/var/lib/awx/venv/awx`), no new compose service — the existing - `forail_projects` volume is already mounted on every forail container. -- Standalone backend tests for helpers, adapter parsers, applies_to - matching, and fail-mode resolver. -- See `forail-backend/docs/20-iac-scanning.md` for the full - architecture. - -**Deferred to v2:** - -- Collection / role provenance verification (sigstore / checksums) — - needs a separate signing infrastructure conversation. -- Live CVE feed for non-Python EE packages (system OS packages). -- In-line annotations on the playbook source viewer. -- Custom rule authoring UI. - -**Effort:** 3-4 weeks - ---- - -### 3.5 Mobile Application - -Detailed plan in `docs/mobile_plan.md`: - -- Deployment approval with biometric verification -- Real-time server monitoring (containers, CPU, RAM, disk) -- Live log streaming -- Push notification alerts -- AI assistant chat - -**Effort:** 7 weeks - ---- - -### 3.6 Observability Integration - -- OpenTelemetry export for all automation runs (traces, metrics) -- Distributed tracing across multi-node receptor mesh -- Grafana dashboard templates for Forail metrics -- Closed-loop: observability alerts feed into EDA rules - -**Effort:** 3-4 weeks - ---- - -## Priority Matrix - -| # | Feature | Impact | Effort | Priority | -| --- | ----------------------- | ------ | ---------- | --------------------- | -| 1.1 | Dynamic Surveys | High | 2-3w | **DONE** | -| 1.2 | Event-Driven Automation | High | 4-6w | **DONE** | -| 1.3 | Drift Detection | High | 3-4w | **DONE** | -| 1.4 | AI Assistant (Ollama) | High | 4w | **DONE** | -| 1.5 | Audit Trail | Medium | 2-3w | **DONE** | -| 2.1 | Self-Service Portal | High | 3-4w | **DONE** | -| 2.2 | Policy-as-Code (OPA) | Medium | 4-5w | **DONE** | -| 2.3 | OIDC + WebAuthn | Medium | 3-4w | **DONE** | -| 2.4 | Workflow Node Surveys | Medium | 2-3w | **DONE** | -| 2.5 | Analytics Dashboard | Medium | 3w | **DONE** | -| 3.1 | Plugin Architecture | High | 8-12w | P2 | -| 3.2 | Multi-Tenancy | High | 6-8w | **DONE** | -| 3.3 | Kubernetes Operator | Medium | 5d (spent) | **DONE** (v2026.05.0) | -| 3.4 | IaC Scanning | Medium | 3-4w | **DONE** | -| 3.5 | Mobile App | Medium | 7w | P2 | -| 3.6 | Observability (OTel) | Medium | 3-4w | **DONE** | - ---- - -## Infrastructure & Test Environments - -- ~~**Provision Kubernetes test instance for Forail Platform**~~ — - **DONE** (2026-05-13..16). `forail-dev-cluster` switched from - 2-master + 2-worker kubeadm to **3-master + 4-worker k3s 1.30.4** - Vagrant cluster (`192.168.56.30-36`, 14 vCPU / 28 GB total) with - embedded etcd HA, bundled Traefik / local-path / klipper-lb / - metrics-server, and `--flannel-iface=eth1` fix for the long-standing - VirtualBox cross-node VXLAN issue. This unblocked Tier 3.3 - (Kubernetes Operator v1.0.0) and lets us validate 3.6 manifest - stubs end-to-end. - ---- - -## Competitive Landscape - -| Feature | Forail | AWX | AAP 2.5+ | Ascender | Semaphore | -| --------------------- | ------------------------------- | ------- | -------- | -------- | --------- | -| Docker Compose deploy | Yes | No | No | Yes | Yes | -| Dynamic surveys | Yes | No | No | No | No | -| Event-driven (EDA) | Yes | No | Yes | No | No | -| Drift detection | Yes | No | No | Yes | No | -| AI assistant | Yes | No | Yes | No | No | -| Self-service portal | **Yes** | No | Yes | No | No | -| Policy-as-Code | **Yes** | No | Planned | No | No | -| OIDC native | **Yes** | Partial | Yes | Partial | No | -| WebAuthn/passkeys | **Yes** | No | No | No | No | -| Modern UI (React 18) | Yes | Legacy | Yes | Legacy | Yes | -| Multi-tenancy | **Yes** | No | Yes | No | No | -| K8s operator | **Yes** (9 CRDs, multi-cluster) | Yes | Yes | Yes | No | -| Open source | Yes | Yes | No | Partial | Yes | - ---- - -## Research Sources - -- [AWX GitHub Issues — feature requests](https://github.com/ansible/awx/issues) -- [Ansible Forum — Is there a future for AWX?](https://forum.ansible.com/t/is-there-a-future-for-awx/44527) -- [Ansible Forum — Programmable survey feature](https://forum.ansible.com/t/programable-survey-feature-in-awx/5806) -- [Ascender Ledger — CIQ](https://github.com/ctrliq/ascender-ledger) -- [AAP 2.5 Release Notes — Red Hat](https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/html/release_notes/new-features) -- [What's New in AAP 2.6 — Red Hat](https://www.redhat.com/en/blog/whats-new-in-ansible-automation-platform-2.6) -- [Event-Driven Ansible — Red Hat](https://www.redhat.com/en/technologies/management/ansible/event-driven-ansible) -- [Terraform Cloud Alternatives 2026 — env0](https://www.env0.com/blog/terraform-cloud-tfc-alternatives-comprehensive-buyers-guide) -- [GitOps 2026 Complete Guide — Calmops](https://calmops.com/devops/gitops-2026-complete-guide/) -- [AI and AIOps in 2026 — Refonte Learning](https://www.refontelearning.com/blog/ai-and-aiops-in-2026-how-intelligent-automation-is-redefining-devops-engineering) -- [Policy-as-Code Tools 2026 — Spacelift](https://spacelift.io/blog/policy-as-code-tools) -- [OpenTelemetry in 2026 — The New Stack](https://thenewstack.io/can-opentelemetry-save-observability-in-2026/) -- [Multi-Tenant Architecture 2026 — QABash](https://www.qabash.com/saas-multi-tenancy-architecture-testing-2026/) -- [Authentication Trends 2026 — C# Corner](https://www.c-sharpcorner.com/article/authentication-trends-in-2026-passkeys-oauth3-and-webauthn/) -- [IaC Security 2026 — Fidelis](https://fidelissecurity.com/cybersecurity-101/cloud-security/infrastructure-as-code-iac-security-drives-cloud-confidence/) -- [Semaphore vs AWX](https://semaphoreui.com/vs/awx) - ---- - -## Wiki Documentation (Developer Onboarding) - -**Problem:** Novi developeri koji pristupe projektu nemaju pregled arhitekture, -ne znaju šta koji fajl radi, niti kako su komponente povezane. Bez kvalitetne -dokumentacije, onboarding traje predugo i razvoj se usporava. - -**Cilj:** Napraviti ultra detaljnu wiki dokumentaciju koja objašnjava svaki -fajl i modul u projektu, tako da budući kolege mogu samostalno da se snadju -i razvijaju platformu. - -**Sadržaj wiki dokumentacije:** - -1. **Architecture Overview** — dijagram sistema (backend, frontend, task engine, - Redis, PostgreSQL, Receptor), kako komponente komuniciraju -2. **Backend (Django)** — objašnjenje svakog modula: - - `forail/main/models/` — svaki model, relacije, migracije - - `forail/api/views/` — svaki API endpoint, šta radi, koji serializer koristi - - `forail/api/serializers/` — logika validacije i transformacije podataka - - `forail/main/tasks/` — Celery taskovi, job runner, callback pipeline - - `forail/main/signals/` — Django signali i side-effecti - - `forail/conf/` — settings, konfiguracija, environment varijable -3. **Frontend (React/TypeScript)** — struktura UI koda: - - Svaka stranica i komponenta - - Routing, state management, API pozivi - - Stilizacija i UI framework -4. **Task Engine** — kako se jobovi pokreću, lifecycle joba od launcha do - završetka, receptor mesh, izolacija izvršavanja -5. **Authentication & RBAC** — autentifikacija, permisije, organizacije, timovi, - kako RBAC radi od API-ja do baze -6. **Database Schema** — ER dijagram, ključne tabele, relacije -7. **Docker & Deployment** — objašnjenje svakog kontejnera, docker-compose - konfiguracija, environment varijable, volumeni -8. **CI/CD Pipeline** — `.github/workflows/ci.yml`, build proces, testovi -9. **Testing** — kako pokrenuti testove, struktura testova, šta koji test pokriva -10. **Contributing Guide** — coding standardi, git workflow, PR proces, - commit konvencije - -**Format:** GitHub Wiki (u okviru `forail-platform/forail-platform` repozitorijuma) -ili `docs/wiki/` direktorijum u samom projektu. - -**Effort:** 3-4 weeks -**Dependencies:** None — može se raditi paralelno sa razvojem feature-a - ---- - -## FreeBSD Support (Host & Jail) - -**Problem:** Forail trenutno radi isključivo na Linux-u (Ubuntu 24.04+) i u -Docker kontejnerima. FreeBSD korisnici, koji često koriste Ansible za -upravljanje serverima i mrežnom opremom, nemaju mogućnost da pokrenu Forail -nativno na svom sistemu. - -**Cilj:** Omogućiti pokretanje Forail platforme direktno na FreeBSD hostu -i unutar FreeBSD jail-a kao alternativu Docker deployment-u. - -**Potrebne izmene:** - -1. **Dependency kompatibilnost** — proveriti i prilagoditi sve Python zavisnosti - za FreeBSD (posebno: psycopg2, uwsgi/gunicorn, receptor, channels/daphne) -2. **Konfiguracija servisa** — rc.d skripte za pokretanje Forail komponenti - (web, task engine, daphne/websocket, beat scheduler) -3. **PostgreSQL & Redis** — dokumentovati instalaciju iz portova/pkg-a, - konfiguracija za Forail -4. **Jail deployment** — jail konfiguracija sa izolovanim Forail okruženjem, - networking (VNET ili alias IP), storage (ZFS dataset per jail) -5. **Port/package** — kreirati FreeBSD port (`sysutils/forail-platform`) za - jednostavnu instalaciju putem `pkg install` -6. **Receptor mesh** — proveriti da receptor radi na FreeBSD-u, prilagoditi - ako koristi Linux-specific sistemske pozive -7. **Testiranje** — pokrenuti test suite na FreeBSD 14.x, ispraviti sve - platform-specifične probleme (putanje, signali, korisnici/grupe) -8. **Dokumentacija** — instalacioni vodič za FreeBSD host i jail deployment - -**Effort:** 4-6 weeks -**Dependencies:** FreeBSD 14.x test okruženje, Python 3.12+ iz portova diff --git a/docs/img/architecture.dot b/docs/img/architecture.dot deleted file mode 100644 index 8951917..0000000 --- a/docs/img/architecture.dot +++ /dev/null @@ -1,129 +0,0 @@ -digraph ForailArchitecture { - rankdir=TB; - bgcolor="#1e1e2e"; - fontname="Helvetica Neue,Helvetica,Arial,sans-serif"; - node [fontname="Helvetica Neue,Helvetica,Arial,sans-serif", fontsize=11, style="filled", - shape="box", penwidth=1.5, margin="0.2,0.1", fontcolor="#cdd6f4"]; - edge [fontname="Helvetica Neue,Helvetica,Arial,sans-serif", fontsize=9, penwidth=1.2, - color="#6c7086", fontcolor="#a6adc8"]; - graph [fontname="Helvetica Neue,Helvetica,Arial,sans-serif", fontsize=12, fontcolor="#cdd6f4"]; - compound=true; - newrank=true; - splines=true; - nodesep=0.6; - ranksep=0.8; - pad=0.5; - - // ── Source Repositories ── - subgraph cluster_source { - label=" Source Repositories "; - labeljust="c"; - style="filled,rounded,bold"; - fillcolor="#313244"; - color="#89b4fa"; - fontcolor="#89b4fa"; - penwidth=2; - margin=20; - - backend [label="forail-backend\n(Django / Python)", - fillcolor="#1e1e2e", color="#89b4fa", fontcolor="#89b4fa", - shape="box", style="filled,rounded"]; - frontend [label="forail-frontend\n(React / TypeScript)", - fillcolor="#1e1e2e", color="#a6e3a1", fontcolor="#a6e3a1", - shape="box", style="filled,rounded"]; - } - - // ── Docker Registry ── - registry [label="Harbor Registry\n(ghcr.io)", - fillcolor="#313244", color="#f9e2af", fontcolor="#f9e2af", - shape="box", style="filled,rounded", penwidth=2]; - - // ── Deployment ── - subgraph cluster_deploy { - label=" forail-deploy (docker-compose) "; - labeljust="c"; - style="filled,rounded,bold"; - fillcolor="#313244"; - color="#cba6f7"; - fontcolor="#cba6f7"; - penwidth=2; - margin=20; - - web [label="forail-web\n(uwsgi + daphne)", - fillcolor="#1e1e2e", color="#89b4fa", fontcolor="#89b4fa", - shape="box", style="filled,rounded"]; - task [label="forail-task\n(dispatcher + receptor)", - fillcolor="#1e1e2e", color="#89b4fa", fontcolor="#89b4fa", - shape="box", style="filled,rounded"]; - fe [label="forail-frontend\n(nginx + React SPA)", - fillcolor="#1e1e2e", color="#a6e3a1", fontcolor="#a6e3a1", - shape="box", style="filled,rounded"]; - init [label="forail-init\n(migrations)", - fillcolor="#1e1e2e", color="#89b4fa", fontcolor="#89b4fa", - shape="box", style="filled,rounded"]; - } - - // ── Infrastructure ── - subgraph cluster_infra { - label=" Infrastructure "; - labeljust="c"; - style="filled,rounded,bold"; - fillcolor="#313244"; - color="#f38ba8"; - fontcolor="#f38ba8"; - penwidth=2; - margin=20; - - nginx [label="nginx\n(TLS + routing)", - fillcolor="#1e1e2e", color="#f9e2af", fontcolor="#f9e2af", - shape="box", style="filled,rounded"]; - postgres [label="PostgreSQL\n:5432", - fillcolor="#1e1e2e", color="#f38ba8", fontcolor="#f38ba8", - shape="cylinder"]; - redis [label="Redis\n:6379", - fillcolor="#1e1e2e", color="#f38ba8", fontcolor="#f38ba8", - shape="cylinder"]; - } - - // ── Users ── - users [label="Users\n(Browser / API)", - shape="ellipse", fillcolor="#313244", color="#6c7086", - fontcolor="#cdd6f4", fontsize=11]; - - // ── Connections ── - - // Build → Registry - backend -> registry [label=" docker push ", color="#89b4fa", fontcolor="#89b4fa"]; - frontend -> registry [label=" docker push ", color="#a6e3a1", fontcolor="#a6e3a1"]; - - // Registry → Deploy - registry -> web [label=" forail-platform/forail-backend ", color="#f9e2af", fontcolor="#f9e2af"]; - registry -> task [style="invis"]; - registry -> fe [label=" forail-platform/forail-frontend ", color="#a6e3a1", fontcolor="#a6e3a1"]; - registry -> init [style="invis"]; - - // Deploy → Infra - web -> postgres [color="#f38ba8"]; - web -> redis [color="#f38ba8"]; - task -> postgres [color="#f38ba8"]; - task -> redis [color="#f38ba8"]; - init -> postgres [label=" migrations ", color="#f38ba8", fontcolor="#f38ba8"]; - - // Nginx routing - nginx -> web [label=" /api /sso /ws ", color="#89b4fa", fontcolor="#89b4fa"]; - nginx -> fe [label=" /* ", color="#a6e3a1", fontcolor="#a6e3a1"]; - - // Users → Nginx - users -> nginx [label=" HTTPS :443 ", color="#f9e2af", fontcolor="#f9e2af", penwidth=2]; - - // Layout hints - {rank=same; backend; frontend} - {rank=same; web; task; fe; init} - {rank=same; nginx; postgres; redis} - - // Title - labelloc="t"; - label="\n Forail Platform — Architecture \n "; - fontsize=16; - fontcolor="#cdd6f4"; -} diff --git a/docs/img/architecture.png b/docs/img/architecture.png deleted file mode 100644 index d0cbb2c..0000000 Binary files a/docs/img/architecture.png and /dev/null differ diff --git a/docs/img/handbook/activity.png b/docs/img/handbook/activity.png deleted file mode 100644 index afe66ac..0000000 Binary files a/docs/img/handbook/activity.png and /dev/null differ diff --git a/docs/img/handbook/alert_rules.png b/docs/img/handbook/alert_rules.png deleted file mode 100644 index 94e197d..0000000 Binary files a/docs/img/handbook/alert_rules.png and /dev/null differ diff --git a/docs/img/handbook/alert_rules_new.png b/docs/img/handbook/alert_rules_new.png deleted file mode 100644 index 2794084..0000000 Binary files a/docs/img/handbook/alert_rules_new.png and /dev/null differ diff --git a/docs/img/handbook/analytics.png b/docs/img/handbook/analytics.png deleted file mode 100644 index f06b0fd..0000000 Binary files a/docs/img/handbook/analytics.png and /dev/null differ diff --git a/docs/img/handbook/approvals.png b/docs/img/handbook/approvals.png deleted file mode 100644 index 6023428..0000000 Binary files a/docs/img/handbook/approvals.png and /dev/null differ diff --git a/docs/img/handbook/audit_log.png b/docs/img/handbook/audit_log.png deleted file mode 100644 index ca85df0..0000000 Binary files a/docs/img/handbook/audit_log.png and /dev/null differ diff --git a/docs/img/handbook/catalog_admin.png b/docs/img/handbook/catalog_admin.png deleted file mode 100644 index bf80e2e..0000000 Binary files a/docs/img/handbook/catalog_admin.png and /dev/null differ diff --git a/docs/img/handbook/catalog_admin_new.png b/docs/img/handbook/catalog_admin_new.png deleted file mode 100644 index 1d39198..0000000 Binary files a/docs/img/handbook/catalog_admin_new.png and /dev/null differ diff --git a/docs/img/handbook/credentials.png b/docs/img/handbook/credentials.png deleted file mode 100644 index 5363434..0000000 Binary files a/docs/img/handbook/credentials.png and /dev/null differ diff --git a/docs/img/handbook/credentials_new.png b/docs/img/handbook/credentials_new.png deleted file mode 100644 index 24f7fb5..0000000 Binary files a/docs/img/handbook/credentials_new.png and /dev/null differ diff --git a/docs/img/handbook/dashboard.png b/docs/img/handbook/dashboard.png deleted file mode 100644 index b2ba4a8..0000000 Binary files a/docs/img/handbook/dashboard.png and /dev/null differ diff --git a/docs/img/handbook/drift_alerts.png b/docs/img/handbook/drift_alerts.png deleted file mode 100644 index a9b4c73..0000000 Binary files a/docs/img/handbook/drift_alerts.png and /dev/null differ diff --git a/docs/img/handbook/drift_detections.png b/docs/img/handbook/drift_detections.png deleted file mode 100644 index 29c4e65..0000000 Binary files a/docs/img/handbook/drift_detections.png and /dev/null differ diff --git a/docs/img/handbook/event_logs.png b/docs/img/handbook/event_logs.png deleted file mode 100644 index d5773bf..0000000 Binary files a/docs/img/handbook/event_logs.png and /dev/null differ diff --git a/docs/img/handbook/event_rules.png b/docs/img/handbook/event_rules.png deleted file mode 100644 index 09ac3a4..0000000 Binary files a/docs/img/handbook/event_rules.png and /dev/null differ diff --git a/docs/img/handbook/event_rules_new.png b/docs/img/handbook/event_rules_new.png deleted file mode 100644 index 301b5a0..0000000 Binary files a/docs/img/handbook/event_rules_new.png and /dev/null differ diff --git a/docs/img/handbook/execution_environments.png b/docs/img/handbook/execution_environments.png deleted file mode 100644 index 5aa4e29..0000000 Binary files a/docs/img/handbook/execution_environments.png and /dev/null differ diff --git a/docs/img/handbook/fact_snapshots.png b/docs/img/handbook/fact_snapshots.png deleted file mode 100644 index a5e3dab..0000000 Binary files a/docs/img/handbook/fact_snapshots.png and /dev/null differ diff --git a/docs/img/handbook/hosts.png b/docs/img/handbook/hosts.png deleted file mode 100644 index e51aa26..0000000 Binary files a/docs/img/handbook/hosts.png and /dev/null differ diff --git a/docs/img/handbook/instance_groups.png b/docs/img/handbook/instance_groups.png deleted file mode 100644 index 96882c7..0000000 Binary files a/docs/img/handbook/instance_groups.png and /dev/null differ diff --git a/docs/img/handbook/instances.png b/docs/img/handbook/instances.png deleted file mode 100644 index e5e803c..0000000 Binary files a/docs/img/handbook/instances.png and /dev/null differ diff --git a/docs/img/handbook/inventories.png b/docs/img/handbook/inventories.png deleted file mode 100644 index 0dc5259..0000000 Binary files a/docs/img/handbook/inventories.png and /dev/null differ diff --git a/docs/img/handbook/inventories_new.png b/docs/img/handbook/inventories_new.png deleted file mode 100644 index 10017d2..0000000 Binary files a/docs/img/handbook/inventories_new.png and /dev/null differ diff --git a/docs/img/handbook/jobs.png b/docs/img/handbook/jobs.png deleted file mode 100644 index d550050..0000000 Binary files a/docs/img/handbook/jobs.png and /dev/null differ diff --git a/docs/img/handbook/my_requests.png b/docs/img/handbook/my_requests.png deleted file mode 100644 index 1cfd71e..0000000 Binary files a/docs/img/handbook/my_requests.png and /dev/null differ diff --git a/docs/img/handbook/notifications.png b/docs/img/handbook/notifications.png deleted file mode 100644 index b63a0ca..0000000 Binary files a/docs/img/handbook/notifications.png and /dev/null differ diff --git a/docs/img/handbook/notifications_new.png b/docs/img/handbook/notifications_new.png deleted file mode 100644 index e8f655a..0000000 Binary files a/docs/img/handbook/notifications_new.png and /dev/null differ diff --git a/docs/img/handbook/observability.png b/docs/img/handbook/observability.png deleted file mode 100644 index c34b4d1..0000000 Binary files a/docs/img/handbook/observability.png and /dev/null differ diff --git a/docs/img/handbook/organizations.png b/docs/img/handbook/organizations.png deleted file mode 100644 index 32803c9..0000000 Binary files a/docs/img/handbook/organizations.png and /dev/null differ diff --git a/docs/img/handbook/organizations_new.png b/docs/img/handbook/organizations_new.png deleted file mode 100644 index 06c173d..0000000 Binary files a/docs/img/handbook/organizations_new.png and /dev/null differ diff --git a/docs/img/handbook/outbound_webhooks.png b/docs/img/handbook/outbound_webhooks.png deleted file mode 100644 index 65e08f8..0000000 Binary files a/docs/img/handbook/outbound_webhooks.png and /dev/null differ diff --git a/docs/img/handbook/outbound_webhooks_new.png b/docs/img/handbook/outbound_webhooks_new.png deleted file mode 100644 index 7784a84..0000000 Binary files a/docs/img/handbook/outbound_webhooks_new.png and /dev/null differ diff --git a/docs/img/handbook/policies.png b/docs/img/handbook/policies.png deleted file mode 100644 index 6e8dd00..0000000 Binary files a/docs/img/handbook/policies.png and /dev/null differ diff --git a/docs/img/handbook/policies_new.png b/docs/img/handbook/policies_new.png deleted file mode 100644 index 1bd2586..0000000 Binary files a/docs/img/handbook/policies_new.png and /dev/null differ diff --git a/docs/img/handbook/policy_decisions.png b/docs/img/handbook/policy_decisions.png deleted file mode 100644 index ad6f648..0000000 Binary files a/docs/img/handbook/policy_decisions.png and /dev/null differ diff --git a/docs/img/handbook/projects.png b/docs/img/handbook/projects.png deleted file mode 100644 index c056851..0000000 Binary files a/docs/img/handbook/projects.png and /dev/null differ diff --git a/docs/img/handbook/projects_new.png b/docs/img/handbook/projects_new.png deleted file mode 100644 index 4a4f3ae..0000000 Binary files a/docs/img/handbook/projects_new.png and /dev/null differ diff --git a/docs/img/handbook/quota_events.png b/docs/img/handbook/quota_events.png deleted file mode 100644 index 29faa74..0000000 Binary files a/docs/img/handbook/quota_events.png and /dev/null differ diff --git a/docs/img/handbook/scan_results.png b/docs/img/handbook/scan_results.png deleted file mode 100644 index 92e3ee1..0000000 Binary files a/docs/img/handbook/scan_results.png and /dev/null differ diff --git a/docs/img/handbook/scanners.png b/docs/img/handbook/scanners.png deleted file mode 100644 index 4a466a5..0000000 Binary files a/docs/img/handbook/scanners.png and /dev/null differ diff --git a/docs/img/handbook/scanners_new.png b/docs/img/handbook/scanners_new.png deleted file mode 100644 index 8579554..0000000 Binary files a/docs/img/handbook/scanners_new.png and /dev/null differ diff --git a/docs/img/handbook/schedules.png b/docs/img/handbook/schedules.png deleted file mode 100644 index 386960f..0000000 Binary files a/docs/img/handbook/schedules.png and /dev/null differ diff --git a/docs/img/handbook/schedules_new.png b/docs/img/handbook/schedules_new.png deleted file mode 100644 index 975c1d3..0000000 Binary files a/docs/img/handbook/schedules_new.png and /dev/null differ diff --git a/docs/img/handbook/service_portal.png b/docs/img/handbook/service_portal.png deleted file mode 100644 index 6ad0545..0000000 Binary files a/docs/img/handbook/service_portal.png and /dev/null differ diff --git a/docs/img/handbook/settings.png b/docs/img/handbook/settings.png deleted file mode 100644 index 6a19b4f..0000000 Binary files a/docs/img/handbook/settings.png and /dev/null differ diff --git a/docs/img/handbook/teams.png b/docs/img/handbook/teams.png deleted file mode 100644 index 41e4ec9..0000000 Binary files a/docs/img/handbook/teams.png and /dev/null differ diff --git a/docs/img/handbook/teams_new.png b/docs/img/handbook/teams_new.png deleted file mode 100644 index 1d06879..0000000 Binary files a/docs/img/handbook/teams_new.png and /dev/null differ diff --git a/docs/img/handbook/templates.png b/docs/img/handbook/templates.png deleted file mode 100644 index f45ca18..0000000 Binary files a/docs/img/handbook/templates.png and /dev/null differ diff --git a/docs/img/handbook/templates_new.png b/docs/img/handbook/templates_new.png deleted file mode 100644 index 1f4b2e5..0000000 Binary files a/docs/img/handbook/templates_new.png and /dev/null differ diff --git a/docs/img/handbook/tenants.png b/docs/img/handbook/tenants.png deleted file mode 100644 index 585a88f..0000000 Binary files a/docs/img/handbook/tenants.png and /dev/null differ diff --git a/docs/img/handbook/tenants_new.png b/docs/img/handbook/tenants_new.png deleted file mode 100644 index 42f126e..0000000 Binary files a/docs/img/handbook/tenants_new.png and /dev/null differ diff --git a/docs/img/handbook/topology.png b/docs/img/handbook/topology.png deleted file mode 100644 index eb9c0e2..0000000 Binary files a/docs/img/handbook/topology.png and /dev/null differ diff --git a/docs/img/handbook/users.png b/docs/img/handbook/users.png deleted file mode 100644 index 3fc6286..0000000 Binary files a/docs/img/handbook/users.png and /dev/null differ diff --git a/docs/img/handbook/users_new.png b/docs/img/handbook/users_new.png deleted file mode 100644 index d833eca..0000000 Binary files a/docs/img/handbook/users_new.png and /dev/null differ diff --git a/docs/mobile_plan.md b/docs/mobile_plan.md deleted file mode 100644 index 83e2b25..0000000 --- a/docs/mobile_plan.md +++ /dev/null @@ -1,815 +0,0 @@ -# Forail Mobile — Deployment Approval & Server Monitor - -Plan for an Android application that serves as a 2FA/biometric gateway for deployment operations and real-time server monitor. - ---- - -## Overview - -``` -┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐ -│ PC / CI/CD │────►│ Forail API │────►│ Android APK │ -│ │ │ (Backend) │ │ │ -│ • git push │ │ │ │ • Push notif │ -│ • deploy cmd │ │ • Auth │ │ • Fingerprint │ -│ • Claude Code │ │ • Queue │ │ • Approve/Deny │ -│ │◄────│ • WebSocket │◄────│ • Live monitor │ -└─────────────────┘ └──────────────┘ └─────────────────┘ -``` - -### Key Features - -| Feature | Description | -| ----------------------- | ----------------------------------------------------------- | -| **Deployment Approval** | Push notification → biometrics → approve/reject | -| **Server Monitor** | Real-time container status, CPU/RAM, health checks | -| **Log Viewer** | Live log streaming with filtering | -| **Alerts** | Notifications for service outages, high CPU, failed deploys | -| **Audit Trail** | Who approved what, when, from which device | - ---- - -## Architecture - -### Components - -``` -forail-mobile/ -├── backend/ # Go API service -│ ├── cmd/server/main.go -│ ├── internal/ -│ │ ├── auth/ # JWT, TOTP, WebAuthn -│ │ ├── approval/ # Deployment approval queue -│ │ ├── monitor/ # Server metrics collector -│ │ ├── notify/ # FCM push notifications -│ │ ├── ws/ # WebSocket hub -│ │ └── db/ # SQLite/PostgreSQL -│ ├── api/ # HTTP handlers -│ └── Dockerfile -│ -├── android/ # Kotlin Android app -│ ├── app/src/main/ -│ │ ├── java/.../forail/ -│ │ │ ├── ui/ # Jetpack Compose screens -│ │ │ ├── data/ # Repository, API client -│ │ │ ├── service/ # FCM, WebSocket, Biometric -│ │ │ └── model/ # Data classes -│ │ └── res/ -│ └── build.gradle.kts -│ -├── cli/ # CLI plugin for deploy approval -│ └── forail-deploy # Shell script / Go binary -│ -└── docker-compose.yml # Backend + Redis for deployment -``` - -### Tech Stack - -| Component | Technology | Reason | -| -------------- | -------------------------------- | ------------------------------------------------------- | -| **Backend** | Go 1.22+ | Fast, small binary, excellent for WebSocket/concurrency | -| **Database** | SQLite (dev) / PostgreSQL (prod) | Approval log, device registry, audit trail | -| **Push** | Firebase Cloud Messaging (FCM) | Free, reliable, Android native | -| **Real-time** | WebSocket | Log and status streaming | -| **Android** | Kotlin + Jetpack Compose | Modern Android UI, native biometrics | -| **Auth** | JWT + Biometrics + TOTP | Multi-layered security model | -| **CI/CD Hook** | GitHub Actions / Generic Webhook | Triggers approval flow | - ---- - -## Phase 1: Backend API (Week 1-2) - -### 1.1 Auth System - -``` -Device registration: -1. User logs in with Forail credentials (username/password) -2. Backend returns JWT access token + refresh token -3. Android registers FCM token and device fingerprint -4. Backend stores device in database (user_id, fcm_token, device_name, public_key) - -Approval flow: -1. CI/CD sends POST /api/v1/approvals/ with deployment info -2. Backend creates approval request (status: pending, ttl: 5min) -3. Backend sends FCM push to all registered devices of the user -4. Android displays notification → user opens app -5. Biometric verification (fingerprint/face) -6. POST /api/v1/approvals/{id}/respond with {action: "approve", biometric_proof: ...} -7. Backend changes status to approved/rejected -8. CI/CD polls or receives webhook callback -``` - -### 1.2 API Endpoints - -``` -POST /api/v1/auth/login # Username/password → JWT -POST /api/v1/auth/refresh # Refresh token -POST /api/v1/auth/devices # Register device (FCM token) -DELETE /api/v1/auth/devices/{id} # Remove device - -POST /api/v1/approvals/ # Create approval request (CI/CD) -GET /api/v1/approvals/ # List pending approvals -GET /api/v1/approvals/{id} # Approval details -POST /api/v1/approvals/{id}/respond # Approve/Reject with biometrics - -GET /api/v1/servers/ # List servers -GET /api/v1/servers/{id}/status # Container status, CPU, RAM, disk -GET /api/v1/servers/{id}/logs # HTTP endpoint for log history -WS /api/v1/ws/logs/{server_id} # WebSocket for live log streaming -WS /api/v1/ws/status # WebSocket for real-time server metrics - -GET /api/v1/deployments/ # Deployment history -GET /api/v1/audit/ # Audit trail (who, what, when) -``` - -### 1.3 Database Schema - -```sql --- Users (synced with Forail/AWX) -CREATE TABLE users ( - id INTEGER PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - password_hash TEXT NOT NULL, - is_admin BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Registered devices -CREATE TABLE devices ( - id TEXT PRIMARY KEY, -- UUID - user_id INTEGER REFERENCES users, - name TEXT NOT NULL, -- "Pixel 8 Pro" - fcm_token TEXT NOT NULL, - public_key TEXT, -- For WebAuthn - last_seen TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Approval requests -CREATE TABLE approvals ( - id TEXT PRIMARY KEY, -- UUID - deployment_id TEXT NOT NULL, -- External ID from CI/CD - server TEXT NOT NULL, -- Target server - action TEXT NOT NULL, -- "deploy", "rollback", "restart" - description TEXT, -- "Deploy forail v2.1.3 to production" - metadata JSONB, -- Commit hash, branch, image tag, etc. - status TEXT DEFAULT 'pending', -- pending, approved, rejected, expired - requested_by TEXT, -- Who initiated the deploy - responded_by INTEGER REFERENCES users, - responded_at TIMESTAMP, - biometric_verified BOOLEAN DEFAULT FALSE, - device_id TEXT REFERENCES devices, - expires_at TIMESTAMP NOT NULL, - callback_url TEXT, -- Webhook for CI/CD callback - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Servers for monitoring -CREATE TABLE servers ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, -- "forail-prod", "forail-staging" - host TEXT NOT NULL, -- SSH host or Docker API endpoint - ssh_user TEXT, - ssh_key_path TEXT, - monitoring_enabled BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Audit log -CREATE TABLE audit_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER REFERENCES users, - device_id TEXT REFERENCES devices, - action TEXT NOT NULL, -- "approve", "reject", "login", "register_device" - resource_type TEXT, -- "approval", "server", "deployment" - resource_id TEXT, - ip_address TEXT, - user_agent TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -### 1.4 Push Notification Payload - -```json -{ - "notification": { - "title": "Deployment Approval Required", - "body": "Deploy forail v2.1.3 to production (forail-prod)" - }, - "data": { - "type": "deployment_approval", - "approval_id": "abc-123", - "server": "forail-prod", - "action": "deploy", - "image": "ghcr.io/forail-platform/forail:2.1.3", - "commit": "a6705d9", - "branch": "modernization", - "requested_by": "krle", - "expires_at": "2026-03-04T17:00:00Z" - } -} -``` - ---- - -## Phase 2: Android Application (Week 2-4) - -### 2.1 Screens - -``` -┌─────────────────────────────────────┐ -│ LOGIN │ -│ │ -│ ┌───────────────────────────────┐ │ -│ │ Forail Server URL │ │ -│ │ https://forail.example.com │ │ -│ └───────────────────────────────┘ │ -│ ┌───────────────────────────────┐ │ -│ │ Username │ │ -│ └───────────────────────────────┘ │ -│ ┌───────────────────────────────┐ │ -│ │ Password │ │ -│ └───────────────────────────────┘ │ -│ │ -│ [ Enable Biometric Login ] │ -│ │ -│ ┌───────────────────────────────┐ │ -│ │ Sign In │ │ -│ └───────────────────────────────┘ │ -└─────────────────────────────────────┘ - -┌─────────────────────────────────────┐ -│ DASHBOARD ≡ │ -│ │ -│ ┌─ Pending Approvals ────────────┐ │ -│ │ 🔴 Deploy forail v2.1.3 │ │ -│ │ forail-prod • 3m ago │ │ -│ │ [Approve] [Reject] │ │ -│ └────────────────────────────────┘ │ -│ │ -│ ┌─ Servers ──────────────────────┐ │ -│ │ 🟢 forail-prod CPU 23% │ │ -│ │ 🟢 forail-staging CPU 8% │ │ -│ │ 🔴 forail-dev OFFLINE │ │ -│ └────────────────────────────────┘ │ -│ │ -│ ┌─ Recent Deployments ───────────┐ │ -│ │ ✅ v2.1.2 → prod 2h ago │ │ -│ │ ✅ v2.1.3 → staging 30m ago │ │ -│ │ ❌ v2.1.3 → prod PENDING │ │ -│ └────────────────────────────────┘ │ -│ │ -│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ -│ │Home│ │Srvr│ │Logs│ │Prof│ │ -│ └────┘ └────┘ └────┘ └────┘ │ -└─────────────────────────────────────┘ - -┌─────────────────────────────────────┐ -│ APPROVAL DETAIL ← │ -│ │ -│ Deploy forail v2.1.3 │ -│ ────────────────────────────────── │ -│ Server: forail-prod │ -│ Image: ghcr.io/forail-platform/forail:2.1.3 │ -│ Branch: modernization │ -│ Commit: a6705d9 │ -│ Requested: krle (3 min ago) │ -│ Expires: 2 min remaining │ -│ │ -│ Changes: │ -│ • fix auth flow │ -│ • add docker compose dev overlay │ -│ • update changelog │ -│ │ -│ ┌───────────────────────────────┐ │ -│ │ │ │ -│ │ 🔒 Scan fingerprint to │ │ -│ │ approve deployment │ │ -│ │ │ │ -│ └───────────────────────────────┘ │ -│ │ -│ ┌──────────┐ ┌──────────────┐ │ -│ │ Reject │ │ Approve │ │ -│ └──────────┘ └──────────────┘ │ -└─────────────────────────────────────┘ - -┌─────────────────────────────────────┐ -│ SERVER DETAIL ← 🔄 │ -│ │ -│ forail-prod │ -│ ────────────────────────────────── │ -│ │ -│ ┌─ Containers ───────────────────┐ │ -│ │ 🟢 forail-web Up 3d 120MB │ │ -│ │ 🟢 forail-task Up 3d 340MB │ │ -│ │ 🟢 postgres Up 3d 85MB │ │ -│ │ 🟢 redis Up 3d 12MB │ │ -│ │ 🟢 nginx Up 3d 8MB │ │ -│ └────────────────────────────────┘ │ -│ │ -│ CPU ▓▓▓▓░░░░░░░░░░░░░░░░ 23% │ -│ RAM ▓▓▓▓▓▓▓░░░░░░░░░░░░░ 38% │ -│ Disk ▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ 55% │ -│ │ -│ ┌─ Quick Actions ────────────────┐ │ -│ │ [Restart] [Stop] [Logs] │ │ -│ └────────────────────────────────┘ │ -│ │ -│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ -│ │Home│ │Srvr│ │Logs│ │Prof│ │ -│ └────┘ └────┘ └────┘ └────┘ │ -└─────────────────────────────────────┘ - -┌─────────────────────────────────────┐ -│ LIVE LOGS ← ⏸ 🔍 │ -│ │ -│ forail-prod > forail-web │ -│ ────────────────────────────────── │ -│ 16:45:01 GET /api/v2/me/ 200 12ms │ -│ 16:45:02 GET /api/v2/jobs/ 200 45ms│ -│ 16:45:03 POST /api/v2/job_templ.. │ -│ 16:45:03 GET /api/v2/config/ 200 │ -│ 16:45:05 WS connect user=admin │ -│ 16:45:06 GET /api/v2/dashboard/ │ -│ 16:45:08 POST /api/login/ 302 │ -│ 16:45:09 GET /api/v2/me/ 200 8ms │ -│ 16:45:10 GET /api/v2/projects/ │ -│ 16:45:11 GET /api/v2/inventories/ │ -│ 16:45:12 POST /api/v2/job_templ.. │ -│ 16:45:13 Job #42 launched by admin │ -│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ -│ Auto-scroll: ON │ -│ │ -│ Container: [forail-web ▼] Level: ALL │ -│ │ -│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ -│ │Home│ │Srvr│ │Logs│ │Prof│ │ -│ └────┘ └────┘ └────┘ └────┘ │ -└─────────────────────────────────────┘ -``` - -### 2.2 Android Libraries - -```kotlin -// build.gradle.kts (app) -dependencies { - // UI - implementation(platform("androidx.compose:compose-bom:2024.12.01")) - implementation("androidx.compose.material3:material3") - implementation("androidx.navigation:navigation-compose:2.8.5") - - // Biometrics - implementation("androidx.biometric:biometric:1.2.0-alpha05") - - // Networking - implementation("com.squareup.retrofit2:retrofit:2.11.0") - implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("org.java-websocket:Java-WebSocket:1.5.7") - - // Push - implementation("com.google.firebase:firebase-messaging:24.1.0") - - // Security - implementation("androidx.security:security-crypto:1.1.0-alpha06") // EncryptedSharedPreferences - - // DI - implementation("io.insert-koin:koin-androidx-compose:4.0.0") -} -``` - -### 2.3 Biometric Auth Flow - -```kotlin -// BiometricHelper.kt -class BiometricHelper(private val activity: FragmentActivity) { - - fun authenticate( - title: String = "Verify Identity", - subtitle: String = "Scan fingerprint to approve", - onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit, - onError: (String) -> Unit - ) { - val promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setSubtitle(subtitle) - .setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_STRONG - or BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) - .build() - - val biometricPrompt = BiometricPrompt( - activity, - ContextCompat.getMainExecutor(activity), - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: ...) { - onSuccess(result) - } - override fun onAuthenticationError(code: Int, msg: CharSequence) { - onError(msg.toString()) - } - } - ) - - biometricPrompt.authenticate(promptInfo) - } -} -``` - ---- - -## Phase 3: CI/CD Integration (Week 4-5) - -### 3.1 GitHub Actions Step - -```yaml -# .github/workflows/deploy.yml -name: Deploy to Production - -on: - push: - tags: ["v*"] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build Docker image - run: docker build -t ghcr.io/forail-platform/forail:${{ github.ref_name }} . - - - name: Push image - run: docker push ghcr.io/forail-platform/forail:${{ github.ref_name }} - - - name: Request deployment approval - id: approval - run: | - RESPONSE=$(curl -s -X POST \ - ${{ secrets.FORAIL_MOBILE_API }}/api/v1/approvals/ \ - -H "Authorization: Bearer ${{ secrets.FORAIL_MOBILE_TOKEN }}" \ - -H "Content-Type: application/json" \ - -d '{ - "server": "forail-prod", - "action": "deploy", - "description": "Deploy ${{ github.ref_name }}", - "metadata": { - "image": "ghcr.io/forail-platform/forail:${{ github.ref_name }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref_name }}", - "actor": "${{ github.actor }}" - }, - "ttl_seconds": 300 - }') - echo "approval_id=$(echo $RESPONSE | jq -r .id)" >> $GITHUB_OUTPUT - - - name: Wait for approval - run: | - for i in $(seq 1 60); do - STATUS=$(curl -s \ - ${{ secrets.FORAIL_MOBILE_API }}/api/v1/approvals/${{ steps.approval.outputs.approval_id }} \ - -H "Authorization: Bearer ${{ secrets.FORAIL_MOBILE_TOKEN }}" \ - | jq -r .status) - echo "Attempt $i: status=$STATUS" - if [ "$STATUS" = "approved" ]; then - echo "Deployment approved!" - exit 0 - elif [ "$STATUS" = "rejected" ]; then - echo "Deployment rejected." - exit 1 - elif [ "$STATUS" = "expired" ]; then - echo "Approval expired." - exit 1 - fi - sleep 5 - done - echo "Timeout waiting for approval." - exit 1 - - - name: Deploy - if: success() - run: | - ssh deploy@forail-prod "cd /opt/forail && \ - docker compose pull && \ - docker compose up -d" -``` - -### 3.2 CLI Command for Manual Deploy - -```bash -#!/bin/bash -# forail-deploy — requests approval before deployment - -set -e - -SERVER=${1:?Usage: forail-deploy [image_tag]} -TAG=${2:-latest} - -echo "Requesting approval for deploy $TAG to $SERVER..." - -APPROVAL=$(curl -s -X POST "$FORAIL_MOBILE_API/api/v1/approvals/" \ - -H "Authorization: Bearer $FORAIL_MOBILE_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{ - \"server\": \"$SERVER\", - \"action\": \"deploy\", - \"description\": \"Manual deploy $TAG to $SERVER\", - \"metadata\": {\"image_tag\": \"$TAG\"}, - \"ttl_seconds\": 300 - }") - -APPROVAL_ID=$(echo "$APPROVAL" | jq -r .id) -echo "Approval ID: $APPROVAL_ID" -echo "Waiting for mobile approval (5 min timeout)..." - -while true; do - STATUS=$(curl -s "$FORAIL_MOBILE_API/api/v1/approvals/$APPROVAL_ID" \ - -H "Authorization: Bearer $FORAIL_MOBILE_TOKEN" | jq -r .status) - - case "$STATUS" in - approved) echo "APPROVED. Deploying..."; break ;; - rejected) echo "REJECTED. Aborting."; exit 1 ;; - expired) echo "EXPIRED. Aborting."; exit 1 ;; - *) printf "."; sleep 3 ;; - esac -done - -# Start deploy -ssh "deploy@$SERVER" "cd /opt/forail && docker compose pull && docker compose up -d" -echo "Deploy complete." -``` - ---- - -## Phase 4: Server Monitoring Agent (Week 5-6) - -### 4.1 Lightweight Agent on the Server - -A small Go binary that runs on each server and sends metrics to the backend. - -```go -// Metrics collected by the agent -type ServerMetrics struct { - Hostname string `json:"hostname"` - CPUPercent float64 `json:"cpu_percent"` - MemTotal uint64 `json:"mem_total"` - MemUsed uint64 `json:"mem_used"` - DiskTotal uint64 `json:"disk_total"` - DiskUsed uint64 `json:"disk_used"` - Containers []ContainerStatus `json:"containers"` - Uptime int64 `json:"uptime_seconds"` - CollectedAt time.Time `json:"collected_at"` -} - -type ContainerStatus struct { - ID string `json:"id"` - Name string `json:"name"` - Image string `json:"image"` - Status string `json:"status"` // running, exited, restarting - Health string `json:"health"` // healthy, unhealthy, none - CPUPerc string `json:"cpu_perc"` - MemUsage string `json:"mem_usage"` - Uptime string `json:"uptime"` -} -``` - -### 4.2 Alert Rules - -```yaml -# alerts.yml — configured in the backend -rules: - - name: container_down - condition: "container.status != 'running'" - severity: critical - message: "Container {{.Name}} is {{.Status}} on {{.Server}}" - - - name: high_cpu - condition: "server.cpu_percent > 85" - duration: 5m - severity: warning - message: "High CPU ({{.Value}}%) on {{.Server}}" - - - name: high_memory - condition: "server.mem_used / server.mem_total > 0.90" - severity: warning - message: "Memory usage {{.Percent}}% on {{.Server}}" - - - name: disk_space - condition: "server.disk_used / server.disk_total > 0.85" - severity: warning - message: "Disk usage {{.Percent}}% on {{.Server}}" - - - name: deploy_failed - condition: "deployment.status == 'failed'" - severity: critical - message: "Deployment {{.ID}} failed on {{.Server}}" -``` - ---- - -## Phase 5: Security (Across All Phases) - -### 5.1 Security Model - -``` -Layer 1: HTTPS (TLS 1.3) for all communication -Layer 2: JWT access token (15min TTL) + refresh token (7d) -Layer 3: Device registration (bound to user) -Layer 4: Biometrics for approval actions (fingerprint/face) -Layer 5: Audit log for every operation -``` - -### 5.2 Key Security Measures - -| Measure | Implementation | -| ------------------- | ----------------------------------------------------------- | -| Token storage | Android EncryptedSharedPreferences (AES-256) | -| Biometric binding | Android Keystore hardware-backed keys | -| API rate limiting | 10 req/sec per device, 3 failed auth → 15min lockout | -| Approval expiry | Max 5 min TTL, cannot be extended | -| Device revocation | Admin can deactivate a device instantly | -| Audit trail | All actions logged with IP, device, timestamp | -| FCM security | Data-only messages (do not display content without the app) | -| Certificate pinning | OkHttp CertificatePinner for backend communication | - -### 5.3 Threat Model - -| Threat | Mitigation | -| -------------- | ----------------------------------------------- | -| Stolen phone | Biometrics required, token in encrypted storage | -| MITM attack | TLS + certificate pinning | -| Replay attack | JWT with short TTL, approval nonce | -| Brute force | Rate limiting + account lockout | -| Insider threat | Audit trail, device binding, biometric proof | - ---- - -## Timeline - -``` -Week 1-2: Backend API (Go) — auth, approvals, push notifications -Week 2-4: Android app (Kotlin) — login, dashboard, approval flow, biometrics -Week 4-5: CI/CD integration — GitHub Actions, CLI tool -Week 5-6: Monitoring agent — metrics, logs, alerts -Week 6-7: Testing + security audit + polish -Week 7: Release APK (sideload or F-Droid) -``` - -**Total: ~7 weeks** - ---- - -## MVP Priorities - -Minimum product that is already useful: - -1. **Backend:** Auth + Approval endpoints + FCM push -2. **Android:** Login + Approval screen + Fingerprint -3. **CLI:** `forail-deploy` command - -Everything else (monitoring, log viewer, alerts) comes in v2. - ---- - -## Phase 6: AI Assistant in the Mobile Application (Week 7-8) - -### 6.1 Overview - -Integrate Forail AI Assistant (Ollama RAG Chat) into the Android application. -Uses the same backend endpoint `/api/v2/assistant/` as the web UI. - -Detailed plan for AI Assistant backend: see `docs/chat_plan.md` - -### 6.2 Chat Screen - -``` -┌─────────────────────────────────────┐ -│ AI Assistant ← │ -│ │ -│ ┌────────────────────────────────┐ │ -│ │ 🤖 Hi! I'm your Forail │ │ -│ │ assistant. Ask me anything. │ │ -│ └────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────┐ │ -│ │ 👤 How do I restart a failed │ │ -│ │ job from my phone? │ │ -│ └────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────┐ │ -│ │ 🤖 To relaunch a failed job: │ │ -│ │ 1. Go to Jobs tab │ │ -│ │ 2. Tap the failed job │ │ -│ │ 3. Tap "Relaunch" │ │ -│ │ │ │ -│ │ 📎 user_guide/jobs.md │ │ -│ └────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────┐ ┌─────┐ │ -│ │ Ask anything... │ │ 🎤 │ │ -│ └──────────────────────┘ └─────┘ │ -│ │ -│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌───┐ │ -│ │Home│ │Srvr│ │Logs│ │ AI │ │Prf│ │ -│ └────┘ └────┘ └────┘ └────┘ └───┘ │ -└─────────────────────────────────────┘ -``` - -### 6.3 Android Implementation - -```kotlin -// ChatScreen.kt — Jetpack Compose -@Composable -fun ChatScreen(viewModel: ChatViewModel = koinViewModel()) { - val messages by viewModel.messages.collectAsState() - val isStreaming by viewModel.isStreaming.collectAsState() - - Column(modifier = Modifier.fillMaxSize()) { - // Chat messages list - LazyColumn( - modifier = Modifier.weight(1f), - reverseLayout = true - ) { - items(messages.reversed()) { msg -> - ChatBubble(msg) - } - } - - // Input field - ChatInput( - onSend = { viewModel.sendMessage(it) }, - onVoice = { viewModel.startVoiceInput() }, - enabled = !isStreaming - ) - } -} -``` - -```kotlin -// ChatViewModel.kt — SSE streaming -class ChatViewModel(private val api: ForailApi) : ViewModel() { - val messages = MutableStateFlow>(emptyList()) - val isStreaming = MutableStateFlow(false) - - fun sendMessage(text: String) { - viewModelScope.launch { - messages.update { it + ChatMessage("user", text) } - isStreaming.value = true - - // Stream response from backend - val botMsg = StringBuilder() - messages.update { it + ChatMessage("assistant", "") } - - api.streamAssistant(text).collect { token -> - botMsg.append(token) - messages.update { msgs -> - msgs.toMutableList().apply { - this[lastIndex] = ChatMessage("assistant", botMsg.toString()) - } - } - } - - isStreaming.value = false - } - } - - fun startVoiceInput() { - // Android SpeechRecognizer → text → sendMessage() - } -} -``` - -### 6.4 Mobile-Specific Features - -| Feature | Description | -| ----------------- | ----------------------------------------------------------------- | -| **Voice Input** | Android Speech-to-Text — ask by voice | -| **Quick Actions** | Bot suggests actions with buttons (Restart, View Logs) | -| **Offline FAQ** | Cached most common answers, works without internet | -| **Push + Chat** | From push notification (failed job) → directly into chat for help | -| **Contextual** | Knows which screen the user came from (Server, Job, Log) | - -### 6.5 Voice Input Flow - -``` -1. User holds the microphone button -2. Android SpeechRecognizer records voice -3. Speech-to-Text converts to text -4. Text is sent to /api/v2/assistant/ -5. Response is displayed in chat -6. (Optional) Text-to-Speech reads the response -``` - ---- - -## Future Development (Post-MVP) - -- **iOS version** — SwiftUI + Face ID -- **Telegram/Signal bot** — alternative for approval without APK -- **Multi-approver** — require 2/3 approvers for production -- **Rollback button** — instant rollback from phone -- **Grafana dashboards** — embed in app -- **Webhook integrations** — Slack, Discord, email notifications -- **Geo-fencing** — allow approval only from specific locations -- **AI Assistant Fine-tuning** — train the model on Forail-specific data diff --git a/docs/plan_detailed.md b/docs/plan_detailed.md deleted file mode 100644 index eb5761f..0000000 --- a/docs/plan_detailed.md +++ /dev/null @@ -1,1036 +0,0 @@ -# Forail - Detailed Development Plan (HISTORICAL — COMPLETED) - -> **Status: COMPLETED.** This detailed plan was fully executed as of v2026.04.0. This document is retained for historical reference. For the current state, see [Release Notes v2026.04.0](RELEASE_NOTES_v2026.04.0.md). - ---- - -## Codebase Overview - -### Backend (Python/Django) - -- 996 Python files, 512 source files -- 22 model files (13,402 lines) -- `serializers.py` - 6,303 lines (single file!) -- `access.py` - 3,019 lines (RBAC) -- 47 URL route modules -- 52 management commands -- 13 notification backends -- 10 credential plugins -- 4 Django applications (main, sso, conf, ui) - -### Frontend (React) - -- 1,597 JS files, 179,059 lines -- React 17.0.2, PatternFly 4.x, CRA (react-scripts 5.0.1) -- 73 components, 27 screens, 56 API models -- 545 test files, 21 custom hooks -- Styled Components + PatternFly CSS -- React Context (no Redux) -- React Router v5 (hash routing) -- Lingui for i18n (8 languages) - -### Infrastructure - -- Makefile with 60+ targets (809 lines) -- Dockerfile.j2 template (323 lines) -- Docker Compose generated by Ansible (14K template) -- Supervisor for process management -- Nginx reverse proxy -- Receptor for mesh networking - ---- - -## PHASE 1: Build Stabilization (Week 1-2) - -### 1.1 Current Build Verification - -**Goal:** Confirm that Forail can be built and launched with applied fixes. - -**Steps:** - -``` -1. vagrant up -2. vagrant ssh -3. cd /awx_devel -4. make docker-compose-build -5. make docker-compose COMPOSE_UP_OPTS=-d -6. docker exec -it tools_awx_1 awx-manage createsuperuser -7. Access https://192.168.56.20:8043 -``` - -**Possible errors and solutions:** - -| Error | File | Solution | -| --------------------------------- | ------------------------------- | --------------------- | -| `pip install` fails for cffi | `requirements/requirements.txt` | Update cffi version | -| `openssl` header mismatch | `Dockerfile.j2` | Install openssl-devel | -| `ansible-runner` devel build fail | `requirements_git.txt` | Pin to stable tag | -| `python3-saml` build fail | `requirements_git.txt` | Pin to stable tag | -| Node 16.13.1 download fail | `Dockerfile.j2` | Update to Node 18 | - -### 1.2 Pinning ALL git dependencies - COMPLETED (2026-02-06) - -**File:** `requirements/requirements_git.txt` - -**State before:** - -``` -git+...system-certifi.git@devel # PROBLEM: unstable -git+...ansible-runner.git@devel # PROBLEM: unstable -git+...python3-saml.git@devel # PROBLEM: unstable -django-ansible-base@...@2024.9.4 # OK: already pinned -``` - -**State after:** - -``` -git+...system-certifi.git@5aa52ab91f9d579bfe52b5acf30ca799f1a563d9 # commit hash -git+...ansible-runner.git@2.4.2 # stable tag -git+...python3-saml.git@f90824c4910e36c5a89dd295271be26691204ba3 # commit hash -django-ansible-base@...@2024.9.4 # already pinned -``` - -**Notes:** - -- `system-certifi` and `python3-saml` have no tags, commit hashes were used -- `ansible-runner` has tags, latest stable `2.4.2` was used -- Build tested and passes successfully - -### 1.3 Running unit tests - -```bash -make docker-compose-runtest -``` - -**Action:** Record ALL failing tests. Categorize them: - -- Failing due to our changes (Django 4.2.16, sqlparse) → fix immediately -- Failing on original AWX 24.6.1 as well → record, fix later -- Deprecated warnings → record for Phase 3 - -### 1.4 Phase 1 Result - -- [x] `make docker-compose-build` passes without errors (2026-02-06) -- [x] `make docker-compose` starts all services (2026-02-06) -- [x] Web UI is accessible and login works (2026-02-06) -- [x] Unit tests pass (or known failures are documented) (2026-02-06) - -**Notes from verification (2026-02-06):** - -- Build requires `COMPOSE_TAG=24.6.2` because rsync does not transfer `.git` folder -- Need to create dummy `.git/hooks` folder in VM: `mkdir -p .git/hooks && touch .git/hooks/pre-commit` -- rsyslog has permission issues but does not affect functionality -- Test results: 3406 passed, 7 failed, 8 errors, 7 skipped -- Failures are mostly in RBAC/dab_rbac modules - existing AWX bugs, not our changes - ---- - -## PHASE 2: Rebranding (Week 2-3) - COMPLETED (2026-02-06) - -### 2.1 Name Change in Configuration - COMPLETED - -**Modified files:** - -| File | Change | -| ----------- | ----------------------------------------------------- | -| `setup.cfg` | `name = awx` → `name = forail`, author → Forail Project | -| `NOTICE` | Already contains "Forail" (previously) | - -### 2.2 UI Branding - COMPLETED - -**Modified files:** - -| File | Change | -| ------------------------------------------------- | --------------------------------------------------------------- | -| `awx/ui/public/static/media/default.strings.json` | `BRAND_NAME: "Ansible AWX"` → `"Forail"` | -| `awx/ui/public/index.html` | meta description → "Forail - Infrastructure Automation Platform" | -| `awx/ui/src/components/About/About.js` | Copyright → "Forail Project" | -| `awx/ui/src/screens/Login/` | Login page branding | -| `awx/ui/src/components/About/` | About modal content | - -**Note:** Do not change Python package name (`awx`) in this phase as it requires refactoring all imports. That goes in Phase 4. - -### 2.3 Phase 2 Result - -- [x] UI displays "Forail" instead of "AWX" (2026-02-06) -- [x] Login page has Forail branding (2026-02-06) -- [x] About modal displays correct information (2026-02-06) - -**Note:** Python package name remains `awx` as changing it would require refactoring all imports. - ---- - -## PHASE 3: Dependency Modernization (Week 3-6) - COMPLETED (2026-03-06) - -**STATUS: COMPLETED** - All packages without UPGRADE BLOCKER updated to latest compatible versions. - -**Key strategy:** Removed `Cython<3` constraint (pyyaml 6.0.1+ supports Cython 3), which unlocked grpcio, msgpack and hiredis upgrade. Migration to channels 4.x/channels-redis 4.x removed aioredis dependency. - -### 3.1 Group 1: Python Core - COMPLETED (2026-03-06) - -| Package | Before | After | Note | -| ------------------- | ------ | ------ | ---------------------------------------- | -| django | 4.2.16 | 4.2.17 | LTS patch | -| djangorestframework | 3.15.1 | 3.15.2 | Patch | -| channels | 3.0.5 | 4.1.0 | Major upgrade, API compatible | -| channels-redis | 3.4.1 | 4.2.0 | Uses redis-py directly, aioredis removed | -| daphne | 3.0.2 | 4.1.2 | Major upgrade | -| asgiref | 3.7.2 | 3.8.1 | Minor | - -- [x] channels 4 migration: API (ProtocolTypeRouter, URLRouter, AsyncJsonWebsocketConsumer) unchanged -- [x] channels-redis 4 migration: CHANNEL_LAYERS config format unchanged -- [x] `awx/main/wsrelay.py`: `import aioredis` removed, `aioredis.errors.ConnectionClosedError` → `redis.exceptions.ConnectionError` -- [x] `aioredis` package removed from requirements.txt (no longer needed) -- [x] `async-timeout` package removed from requirements.txt (Python 3.12 has asyncio.timeout) - -### 3.2 Group 2: Database and Cache - COMPLETED (2026-03-06) - -| Package | Before | After | Note | -| -------- | ------ | ----- | ------------------------------------ | -| psycopg | 3.1.18 | 3.2.3 | Minor, stable | -| redis | 5.0.1 | 5.2.1 | Minor | -| hiredis | 2.0.0 | 3.0.0 | Major, unlocked by removing aioredis | -| sqlparse | 0.5.2 | 0.5.2 | Already on latest | - -### 3.3 Group 3: Cryptography - COMPLETED (2026-03-06) - -| Package | Before | After | Note | -| ------------ | ------ | ------ | ----------------------------------------- | -| cryptography | 41.0.7 | 42.0.8 | Limited to <44 (msal) and <43 (pyopenssl) | -| pyopenssl | 24.0.0 | 24.2.1 | Patch | -| pyjwt | 2.8.0 | 2.9.0 | Minor | -| cffi | 1.16.0 | 1.17.1 | Minor (cryptography dep) | - -### 3.4 Group 4: Async/Networking - COMPLETED (2026-03-06) - -| Package | Before | After | Note | -| ----------- | ------- | ------- | ----------------------------------- | -| aiohttp | 3.9.5 | 3.10.10 | Minor | -| twisted | 23.10.0 | 24.7.0 | Major, CVE-2024-41671 fix | -| autobahn | 23.6.2 | 24.4.2 | Major | -| incremental | 22.10.0 | 24.7.2 | For twisted 24.x | -| urllib3 | 1.26.18 | 1.26.20 | Patch only (botocore requires <2.0) | -| requests | 2.31.0 | 2.32.3 | Minor | -| yarl | 1.9.4 | 1.14.0 | For aiohttp 3.10.x | -| multidict | 6.0.5 | 6.1.0 | For aiohttp 3.10.x | -| frozenlist | 1.4.1 | 1.5.0 | For aiohttp 3.10.x | - -**Note:** urllib3 stays on 1.x because botocore requires `urllib3<2.0`. - -### 3.5 Group 5: Cloud Providers - COMPLETED (2026-03-06) - -| Package | Before | After | Note | -| --------------- | ------- | ------- | -------------------------------------- | -| boto3 | 1.34.47 | 1.35.36 | Minor | -| botocore | 1.34.47 | 1.35.36 | Minor | -| s3transfer | 0.10.0 | 0.10.3 | Patch | -| azure-identity | 1.15.0 | 1.19.0 | Minor | -| azure-core | 1.30.0 | 1.32.0 | Minor | -| msal | 1.26.0 | 1.31.0 | Minor | -| msal-extensions | 1.1.0 | 1.2.0 | Minor | -| kubernetes | 29.0.0 | 29.0.0 | No change (openshift~=29.0 constraint) | -| openshift | 0.13.2 | 0.13.2 | No change | - -### 3.6 Group 6: Observability - COMPLETED (2026-03-06) - -| Package | Before | After | Note | -| ----------------------------- | ------ | ------ | ------------------------------ | -| opentelemetry-api | 1.24.0 | 1.27.0 | Minor | -| opentelemetry-sdk | 1.24.0 | 1.27.0 | Minor | -| opentelemetry-exporter-otlp | 1.24.0 | 1.27.0 | Minor | -| opentelemetry-instrumentation | 0.45b0 | 0.48b0 | Minor | -| prometheus-client | 0.20.0 | 0.21.0 | Minor | -| grpcio | 1.62.2 | 1.67.1 | Unlocked by Cython 3 migration | -| protobuf | 4.25.3 | 4.25.5 | Patch (stays on 4.x) | - -### 3.7 Group 7: Node.js and Frontend - SKIPPED - -Node.js already updated to 20.x in Phase 6. Frontend (React 18, Vite 6) created from scratch in Phase 5 with modern stack. Old frontend (`awx/ui/`) remains unchanged as legacy fallback. - -### 3.8 Group 8: Makefile Bootstrap - COMPLETED (previously) - -Already updated in earlier phases. Now aligned with requirements.txt: - -- `cython==3.0.10` → `cython==3.0.11` (consistent with requirements.txt) - -### 3.9 Misc packages updated - -| Package | Before | After | -| -------------- | ------- | ------ | -| cython | 0.29.37 | 3.0.11 | -| msgpack | 1.0.5 | 1.1.0 | -| jinja2 | 3.1.3 | 3.1.4 | -| gitpython | 3.1.42 | 3.1.43 | -| pyyaml | 6.0.1 | 6.0.2 | -| psutil | 5.9.8 | 6.1.0 | -| jsonschema | 4.21.1 | 4.23.0 | -| slack-sdk | 3.27.0 | 3.33.0 | -| setuptools-scm | 8.0.4 | 8.1.0 | -| wheel | 0.42.0 | 0.43.0 | - -### 3.10 Packages with UPGRADE BLOCKER (remain unchanged) - -| Package | Version | Reason | -| ---------------------- | --------------- | -------------------------------------------------------------- | -| pip | 21.2.4 → 24.0 | UPGRADE BLOCKER removed: pip 21.x does not work on Python 3.12 | -| setuptools | 69.0.2 → 70.0.0 | Aligned with Makefile VENV_BOOTSTRAP | -| social-auth-core | 4.4.2 | UPGRADE BLOCKER | -| social-auth-app-django | 5.4.0 | UPGRADE BLOCKER | -| django-oauth-toolkit | 1.7.1 | Breaking changes in 2.0.0 | -| django-split-settings | 1.0.0 | Release process error | -| tacacs_plus | 1.0 | Auth does not work with newer versions | -| pyparsing | 2.4.6 | v3 breaks smart host filtering | -| pexpect | 4.7.0 | Library notes | -| ansiconv | 1.0.0 | From 2013, no upgrade available | -| django-guid | 3.2.1 | Pinned | - -### 3.11 Phase 3 Result - -- [x] All Python packages updated to latest compatible versions (2026-03-06) -- [x] Cython 3 migration unlocked grpcio, msgpack (2026-03-06) -- [x] Channels 4 migration removed aioredis dependency (2026-03-06) -- [x] Code fix: `awx/main/wsrelay.py` migrated from aioredis to redis exceptions (2026-03-06) -- [x] Node.js already on 20.x (Phase 6), Frontend already on React 18 (Phase 5) (previously) -- [x] VENV_BOOTSTRAP aligned (Makefile) (2026-03-06) -- [x] pip 21.2.4 → 24.0 (pip 21.x incompatible with Python 3.12) (2026-03-06) -- [x] setuptools 69.0.2 → 70.0.0 (aligned with Makefile) (2026-03-06) -- [x] Dockerfile.j2/ubuntu.j2: `hash -r` fix for npm after `n 20` (2026-03-06) -- [x] Docker build passes (2026-03-06) -- [x] Docker compose up works (postgres, redis, awx) (2026-03-06) -- [x] Django check --deploy passes (2026-03-06) -- [x] API works (/api/v2/ping/) (2026-03-06) -- [x] Unit tests: 1175 passed, 8 failed (pre-existing), 0 new regressions (2026-03-06) -- [ ] `pip-audit` reports no known CVEs (needs testing) - ---- - -## PHASE 4: Backend Refactoring (Week 6-12) - -### 4.1 Breaking Up serializers.py (Week 6-7) - COMPLETED (2026-02-07) - -**Status:** Fully completed. `_legacy.py` broken into 14 domain modules and deleted. - -**Done:** - -- [x] Created `awx/api/serializers/` directory -- [x] Created `base.py` with BaseSerializer, constants and helper functions (~600 lines) -- [x] Created `unified.py` with UnifiedJob/Template serializers (~400 lines) -- [x] Created `users.py` with UserSerializer, UserActivityStreamSerializer -- [x] Created `oauth.py` with OAuth2 serializers (6 classes) -- [x] Created `organizations.py` with Organization, Team, Role serializers (5 classes) -- [x] Created `projects.py` with Project serializers (9 classes) -- [x] Created `execution_environments.py` with ExecutionEnvironmentSerializer -- [x] Created `inventory.py` with Inventory, Host, Group serializers (24 classes) -- [x] Created `credentials.py` with Credential serializers (7 classes) -- [x] Created `jobs.py` with Job, AdHoc, SystemJob serializers (21 classes) -- [x] Created `workflows.py` with Workflow serializers (20 classes) -- [x] Created `events.py` with Event serializers (5 classes) -- [x] Created `notifications.py` with Notification and Label serializers (3 classes) -- [x] Created `schedules.py` with Schedule serializers (2 classes) -- [x] Created `instances.py` with Instance and HostMetric serializers (8 classes) -- [x] Created `activity_stream.py` with ActivityStreamSerializer -- [x] Updated `__init__.py` with re-exports from all domain modules -- [x] Deleted `_legacy.py` - no longer needed -- [x] All Python syntax checks pass -- [x] Backward compatibility preserved through `__init__.py` re-exports - -**Problem:** `awx/api/serializers.py` had 6,303 lines - impossible to maintain. - -**Action:** Break into modules by domain: - -``` -awx/api/serializers/ -├── __init__.py # Re-exports all serializers -├── base.py # BaseSerializer, base classes (~200 lines) -├── credentials.py # CredentialSerializer, CredentialTypeSerializer -├── inventory.py # InventorySerializer, GroupSerializer, HostSerializer -├── jobs.py # JobSerializer, JobTemplateSerializer -├── organizations.py # OrganizationSerializer, TeamSerializer, UserSerializer -├── projects.py # ProjectSerializer, ProjectUpdateSerializer -├── workflows.py # WorkflowJobTemplateSerializer, WorkflowNodeSerializer -├── notifications.py # NotificationTemplateSerializer -├── schedules.py # ScheduleSerializer -├── execution_environments.py -├── instances.py # InstanceSerializer, InstanceGroupSerializer -├── oauth.py # OAuth2ApplicationSerializer, TokenSerializer -├── activity_stream.py # ActivityStreamSerializer -└── settings.py # SettingSerializer -``` - -**Procedure for each module:** - -1. Identify all serializers belonging to the domain -2. Extract them into a new file with all imports -3. Add re-export to `__init__.py` -4. Run tests: `py.test awx/main/tests/unit/api/serializers/` -5. Run API tests: `py.test awx/main/tests/functional/api/` - -### 4.2 Breaking Up views/**init**.py (Week 7) - COMPLETED (2026-02-07) - -**Status:** Fully completed. `__init__.py` broken into 24 domain modules. - -**Done:** - -- [x] Shared utilities (mixins, helpers, exception classes) moved to `mixin.py` -- [x] Created `dashboard.py` - DashboardView, DashboardJobsGraphView (2 classes) -- [x] Created `instances.py` - InstanceList, InstanceDetail, etc. (9 classes) -- [x] Created `instance_groups.py` - InstanceGroupList, InstanceGroupDetail, etc. (6 classes) -- [x] Created `schedules.py` - ScheduleList, ScheduleDetail, etc. (9 classes) -- [x] Created `auth.py` - AuthView (1 class) -- [x] Created `oauth.py` - OAuth2ApplicationList, etc. (11 classes) -- [x] Created `users.py` - UserList, UserMeList, UserDetail, etc. (10 classes) -- [x] Created `teams.py` - TeamList, TeamDetail, etc. (8 classes) -- [x] Created `execution_environments.py` - ExecutionEnvironmentList, etc. (5 classes) -- [x] Created `projects.py` - ProjectList, ProjectDetail, ProjectUpdateStdout, etc. (21 classes) -- [x] Created `credentials.py` - CredentialTypeList, CredentialList, etc. (20 classes) -- [x] Created `hosts.py` - HostMetricList, HostList, HostDetail, etc. (12 classes) -- [x] Created `groups.py` - GroupList, GroupDetail, etc. (8 classes) -- [x] Created `inventory_sources.py` - InventoryGroupsList, InventorySourceList, etc. (26 classes) -- [x] Created `job_templates.py` - JobTemplateList, JobTemplateLaunch, etc. (17 classes) -- [x] Created `jobs.py` - JobList, JobDetail, JobRelaunch, etc. (22 classes) -- [x] Created `workflows.py` - WorkflowJobTemplateList, WorkflowJobList, etc. (35 classes) -- [x] Created `workflow_approvals.py` - WorkflowApprovalList, etc. (6 classes) -- [x] Created `ad_hoc_commands.py` - AdHocCommandList, etc. (14 classes) -- [x] Created `system_jobs.py` - SystemJobTemplateList, SystemJobList, etc. (11 classes) -- [x] Created `notifications.py` - NotificationTemplateList, etc. (7 classes) -- [x] Created `activity_stream.py` - ActivityStreamList, ActivityStreamDetail (2 classes) -- [x] Created `roles.py` - RoleList, RoleDetail, etc. (6 classes) -- [x] Created `unified.py` - UnifiedJobTemplateList, UnifiedJobList, UnifiedJobStdout (3 classes) -- [x] Updated `__init__.py` with re-exports from all domain modules (67 lines) -- [x] All Python syntax checks pass (py_compile) -- [x] Backward compatibility preserved through `__init__.py` re-exports -- [x] No circular dependencies between modules - -**Before:** `__init__.py` with 4,551 lines and ~296 view classes -**After:** `__init__.py` with 67 lines + 24 domain modules (4,806 lines total) - -### 4.3 Breaking Up access.py (Week 8) - COMPLETED (2026-02-10) - -**Status:** Fully completed. `access.py` broken into 15 domain modules. - -**Done:** - -- [x] Created `awx/main/access/` directory -- [x] Created `base.py` with BaseAccess, mixins, utility functions, check_superuser decorator (~529 lines) -- [x] Created `instances.py` with InstanceAccess, InstanceGroupAccess, ReceptorAddressAccess (3 classes) -- [x] Created `users.py` with UserAccess, OAuth2ApplicationAccess, OAuth2TokenAccess (3 classes) -- [x] Created `organizations.py` with OrganizationAccess, TeamAccess (2 classes) -- [x] Created `inventory.py` with InventoryAccess, HostAccess, GroupAccess, InventorySourceAccess, InventoryUpdateAccess (5 classes) -- [x] Created `credentials.py` with CredentialTypeAccess, CredentialAccess, CredentialInputSourceAccess (3 classes) -- [x] Created `projects.py` with ExecutionEnvironmentAccess, ProjectAccess, ProjectUpdateAccess (3 classes) -- [x] Created `jobs.py` with JobTemplateAccess, JobAccess, SystemJobTemplateAccess, SystemJobAccess, JobLaunchConfigAccess, AdHocCommandAccess, AdHocCommandEventAccess (7 classes) -- [x] Created `workflows.py` with WorkflowJobTemplateNodeAccess, WorkflowJobNodeAccess, WorkflowJobTemplateAccess, WorkflowJobAccess, WorkflowApprovalAccess, WorkflowApprovalTemplateAccess (6 classes) -- [x] Created `events.py` with JobHostSummaryAccess, JobEventAccess, UnpartitionedJobEventAccess, ProjectUpdateEventAccess, InventoryUpdateEventAccess, SystemJobEventAccess (6 classes) -- [x] Created `notifications.py` with NotificationTemplateAccess, NotificationAccess, LabelAccess (3 classes) -- [x] Created `unified.py` with UnifiedJobTemplateAccess, UnifiedJobAccess (2 classes) -- [x] Created `schedules.py` with ScheduleAccess (1 class) -- [x] Created `activity_stream.py` with ActivityStreamAccess (1 class) -- [x] Created `roles.py` with RoleAccess (1 class) -- [x] Updated `__init__.py` with re-exports, consumer_access, optimize_queryset, auto-registration (142 lines) -- [x] Deleted `access.py` - replaced with `access/` package -- [x] All Python syntax checks pass -- [x] Backward compatibility preserved through `__init__.py` re-exports -- [x] Lazy imports for circular dependencies (RoleAccess <-> UserAccess, NotificationAttachMixin -> NotificationTemplateAccess) - -**Before:** `access.py` with 3,020 lines and ~49 access classes -**After:** `__init__.py` with 142 lines + 15 domain modules (3,399 lines total) - -### 4.4 Django Model Cleanup (Week 8-9) - COMPLETED (2026-02-13) - -**Status:** Fully completed. 5 largest model files cleaned internally. - -**Result:** 9 query optimizations, 2 bug fixes, 6 dead code removals, 4 new database indexes. - -**Approach:** Changes grouped into 3 categories: - -- **A (No migration):** Query optimizations, bug fixes, dead code removal -- **B (With migration):** Adding database indexes (single migration 0196 for all) -- **C (Documentation only):** Deprecated fields marked for future phase - -**Done per file:** - -1. `inventory.py` - 3 query optimizations, 1 dead code cleanup, 2 indexes - - [x] A1: `update_computed_fields()` - cached `failed_hosts.count()` and `active_inventory_sources.count()` (5 queries instead of 7) - - [x] A2: `InventorySource.notification_templates` - Q objects instead of separate queries (3 instead of 6) - - [x] A3: `get_cloud_credential()`/`get_extra_credentials()` - sharing single `credentials.all()` call - - [x] A4: FIXME comments replaced with NOTE (3 methods) - - [x] C1: DEPRECATED block comment added above legacy computed fields - - [x] B1: `Inventory.kind` db_index=True, `InventorySource` Meta.indexes for `source` - -2. `unified_jobs.py` - 1 query optimization, 3 dead code removals - - [x] A5: `update_computed_fields()` - uses `new_next_schedule` instead of repeated `related_schedules.first()` - - [x] A6: Deleted commented-out `on_missed_schedule` field (4 lines) - - [x] A7: Deleted commented-out deadlock code and FIXME in `signal_start()` (9 lines) - - [x] A8: `result_stdout_text` setter - TODO replaced with DEPRECATED - -3. `jobs.py` - 2 query optimizations, 1 bug fix, 2 dead code removals, 1 index - - [x] A9: **Bug fix** `JobHostSummary.save()` - duplicate `failed` in update_fields + `host_name` was not propagated - - [x] A10: `Job.notification_data()` - uses `summaries` instead of third `self.job_host_summaries.all()` call - - [x] A11: `JobTemplate.notification_templates` - Q objects (3 queries instead of 6) - - [x] A12: Deleted commented-out `from django.core.cache import cache` - - [x] A13: Deleted orphaned docstring `'''RelatedJobsMixin'''` - - [x] B1: `Job` Meta.indexes for `(job_template, status)` - for `cache_timeout_blocked` - -4. `credential/__init__.py` - 2 query optimizations - - [x] A14: `Credential.save()` - `values_list('inputs', flat=True)` instead of loading full object - - [x] A15: `_cached_input_sources` cached_property - shares single `input_sources.all()` between `dynamic_input_fields` and `_get_dynamic_input` - -5. `workflow.py` - 1 query optimization, 1 bug fix, 1 index + migration - - [x] A16: `WorkflowJobTemplate.notification_templates` - Q objects (4 queries instead of 8) - - [x] A17: **Bug fix** `WorkflowApproval.save()` - modified `update_fields` was not propagated to `kwargs` - - [x] B1: `WorkflowJob` Meta.indexes for `(workflow_job_template, status)` - for `cache_timeout_blocked` - - [x] Created migration `0196_add_model_indexes.py` with all indexes - -**Deprecated fields (for future phase):** - -- `Inventory.has_active_failures`, `total_hosts`, `hosts_with_active_failures`, `total_groups`, `has_inventory_sources` - marked with DEPRECATED comment, removal in future major version - -### 4.5 Management Command Cleanup (Week 9) - COMPLETED (2026-02-17) - -**Status:** Fully completed. 4 deprecated commands deleted, 4 commands fixed/optimized, cleanup_jobs batch refactored. - -**Done:** - -- [x] Deleted 4 deprecated commands: `export_custom_venv.py`, `list_custom_venvs.py`, `custom_venv_associations.py`, `export_custom_scripts.py` (184 lines) -- [x] Deleted `get_custom_venv_choices()` and `get_custom_venv_pip_freeze()` from `awx/main/utils/common.py` -- [x] Cleaned up imports in `root.py` and `mixins.py`, backward compatibility of API preserved (`custom_virtualenvs: []`) -- [x] Fix `cleanup_tokens.py` and `cleanup_sessions.py`: `execute()` → `handle()` (Django standard) -- [x] Optimization `cleanup_activitystream.py`: DB-level filter instead of Python-side (massive performance improvement) -- [x] Refactored `cleanup_jobs.py`: `_batch_delete_by_pks()` helper, 6 methods use batch deletion instead of N+1 - -### 4.6 API Performance Optimization (Week 10) - COMPLETED (2026-02-20) - -**Status:** Fully completed. Audit of API listing endpoint performance. - -**Audit result:** - -1. **`select_related`/`prefetch_related`** — already globally applied - - `optimize_queryset()` in `awx/main/access/__init__.py` automatically applies `select_related`/`prefetch_related` from access classes to all querysets - - `GenericAPIView.get_queryset()` calls `optimize_queryset(qs)` for every request - - All 25+ access classes already define comprehensive `select_related` and `prefetch_related` tuples - - No views miss this optimization - -2. **`__str__` N+1 audit** — found and fixed 1 issue - - [x] `Schedule.__str__`: `self.unified_job_template.id` → `self.unified_job_template_id` (raw FK column, zero queries) - - `JobHostSummary.__str__`: already covered by `select_related('host')` in access class - - Other `__str__` methods: reviewed, no N+1 issues - -3. **Database indexes** — already complete - - Event models have multi-column composite indexes for all common query patterns - - `UnifiedJob.status`, `.created`, `.finished`, `.launch_type` have `db_index=True` - - `Job(job_template, status)` and `WorkflowJob(workflow_job_template, status)` composite indexes (added in 4.4) - - FK fields automatically indexed by Django - - `name` fields: B-tree indexes do not help for `ICONTAINS`/`LIKE '%query%'` patterns - -4. **Pagination** — already adequate - - `Pagination` class supports `count_disabled` query parameter for large datasets - - `UnifiedJobEventPagination` supports both page-based and limit-based modes - - Cursor-based pagination would be a **breaking API change** — not suitable for refactoring phase - -5. **SerializerMethodField** — 54 occurrences, no systematic N+1 issues - - Most are simple calculations (string formatting, boolean checks) - -6. **SUMMARIZABLE_FK_FIELDS** — 30+ FK mappings, covered by access class prefetch - -**Done:** - -- [x] Fix `Schedule.__str__` N+1: `unified_job_template.id` → `unified_job_template_id` - -### 4.7 Middleware Cleanup (Week 10) - COMPLETED (2026-02-20) - -**File:** `awx/main/middleware.py` (230 lines) -**File:** `awx/settings/defaults/base.py` - MIDDLEWARE list - -Review 17 middlewares: - -- [x] Are all of them needed? — **YES**, all 17 are needed -- [x] Is the order optimal? — **Fixed**: CorsMiddleware moved before MigrationRanCheckMiddleware (per django-cors-headers docs — must be before middlewares that generate responses, otherwise CORS headers are missing on redirect responses) -- [x] Should `TimingMiddleware` be in production? — **YES**, it is lightweight (just `time.time()` + X-API-Total-Time header); AWXProfiler activates only when `AWX_REQUEST_PROFILE=True` (default `False`) -- [x] Removed dead code: `dest = '/var/log/tower/profile'` attribute on TimingMiddleware (never used) -- [x] Performance: Added early return in URLModificationMiddleware for non-API paths (`/static/`, `/sso/`, UI) — skips PurePosixPath processing - -### 4.8 Settings Refactoring (Week 11) - COMPLETED (2026-02-10) - -**Status:** Fully completed. `defaults.py` broken into 10 domain modules. - -**Done:** - -- [x] Created `awx/settings/defaults/` directory (package replaces monolithic file) -- [x] Created `base.py` - Core Django settings: DEBUG, DATABASES, INSTALLED_APPS, MIDDLEWARE, TEMPLATES, BASE_DIR, i18n, static/media paths (~250 lines) -- [x] Created `auth.py` - Authentication, session, CSRF, OAuth2, LDAP, RADIUS, TACACS+ (~100 lines) -- [x] Created `rest_api.py` - REST_FRAMEWORK config, devserver settings (~40 lines) -- [x] Created `social_auth.py` - Social auth providers, pipeline, SAML, GitHub, Google, Azure (~100 lines) -- [x] Created `celery_conf.py` - BROKER_URL, CELERYBEAT_SCHEDULE, CACHES, cluster heartbeat (~50 lines) -- [x] Created `jobs.py` - Job execution, events, ansible config, task manager, ad hoc commands (~180 lines) -- [x] Created `inventory_plugins.py` - Cloud provider inventory settings: EC2, VMware, GCE, Azure RM, OpenStack, etc. (~115 lines) -- [x] Created `websockets.py` - ASGI, CHANNEL*LAYERS, BROADCAST_WEBSOCKET*\*, DJANGO_GUID (~45 lines) -- [x] Created `logging_conf.py` - LOGGING dict, handler*config, runtime handler loop, LOG_AGGREGATOR*\* (~120 lines) -- [x] Created `awx_settings.py` - Activity stream, metrics, runner, receptor, host metrics, execution queues (~130 lines) -- [x] Created `__init__.py` - Re-exports all modules + ANSIBLE*BASE*\* + dynamic_config include() (~65 lines) -- [x] Deleted `defaults.py` - replaced with `defaults/` package -- [x] BASE_DIR path fix (extra `os.path.dirname` for subdirectory) -- [x] Cross-module dependencies: `logging_conf.py` imports `LOG_ROOT` from `.base`, `websockets.py` imports `BROKER_URL` from `.celery_conf` -- [x] `production.py` and `development.py` continue to work without changes (`from .defaults import *`) -- [x] All Python syntax checks pass (py_compile) -- [x] 317 settings successfully exported and verified - -**Before:** `defaults.py` with 1,189 lines -**After:** `__init__.py` with 65 lines + 10 domain modules - -``` -awx/settings/defaults/ -├── __init__.py # Re-exports + ansible_base config + include() -├── base.py # Core Django settings -├── auth.py # Auth, session, CSRF, OAuth2, LDAP -├── rest_api.py # REST Framework config -├── social_auth.py # Social auth providers & pipeline -├── celery_conf.py # Broker, Celerybeat, caches, cluster -├── jobs.py # Job execution, events, ansible config -├── inventory_plugins.py # Cloud provider inventory settings -├── websockets.py # ASGI, channels, broadcast WS -├── logging_conf.py # Logging config + handler loop -└── awx_settings.py # AWX-specific misc settings -``` - -### 4.9 Signal Handler Cleanup (Week 11) - COMPLETED (2026-02-14) - -**File:** `awx/main/signals.py` (682 lines -> 722 lines) - -- [x] Module docstring + section header comments for 9 logical sections -- [x] Batch M2M in `migrate_children_from_deleted_group_to_parent_groups`: `add(*pks)` instead of loops — O(parents x (hosts+children)) -> O(parents x 2) queries -- [x] Batch fetch in `activity_stream_associate`: `in_bulk()` instead of exists()+filter() per PK — 2N -> 1 query -- [x] Batch session cleanup in `save_user_session_membership`: bulk `Session.delete()` instead of loop — 2N -> 1 query; removed redundant `membership.delete()` (CASCADE) -- [x] Fix thread-safety bug in OAuth2 handler: `disconnect/save/reconnect` → `queryset.update()` (does not trigger signals) -- [x] Fix exception safety in `disable_computed_fields()`: added `try/finally` around `yield` -- [x] Fix 2 typos: `Admnistrator` → `Administrator`, `funciton` → `function` -- [x] Removed unreachable `else: return` in `activity_stream_associate` - -### 4.10 Dispatch/Task System Review (Week 12) - COMPLETED (2026-02-20) - -**Files:** (1,846 lines total, already well structured) - -``` -awx/main/dispatch/ -├── __init__.py # PubSub, pg_bus_conn, create_listener_connection (142 lines) -├── pool.py # WorkerPool, AutoscalePool, PoolWorker (491 lines) -├── control.py # Control class for status/cancel/reload (78 lines) -├── periodic.py # Scheduler, ScheduledTask (142 lines) -├── publish.py # @task() decorator, PublisherMixin (130 lines) -├── reaper.py # startup_reaping, reap_job, reap, reap_waiting (90 lines) -└── worker/ - ├── __init__.py # (3 lines) - ├── base.py # AWXConsumerBase/PG/Redis, BaseWorker (331 lines) - ├── callback.py # CallbackBrokerWorker - event persistence (295 lines) - └── task.py # TaskWorker - task execution (144 lines) -``` - -**Review result:** - -- [x] Worker pool reviewed - AutoscalePool with memory-based scaling, orphan recovery, task manager timeout detection -- [x] Task flow documented (publish -> execute -> callback): - 1. `@task()` decorator adds `apply_async()`/`delay()` to functions/classes - 2. `apply_async()` sends JSON message through PostgreSQL `pg_notify(channel, payload)` - 3. `AWXConsumerPG` (run_dispatcher) listens on PG channels, receives notifications - 4. `process_task()` routes: control messages directly, task messages to worker pool - 5. `AutoscalePool.write()` puts message in multiprocessing queue (picks idle worker) - 6. `BaseWorker.work_loop()` reads from queue, calls `perform_work()` - 7. `TaskWorker.resolve_callable()` imports decorated function/class and executes - 8. For job execution: events are sent to Redis `CALLBACK_QUEUE` - 9. `AWXConsumerRedis` (run_callback_receiver) reads from Redis via `blpop` - 10. `CallbackBrokerWorker.flush()` buffers and saves events to database via `bulk_create()` -- [x] Bottlenecks identified: - - (LOW) PG NOTIFY listener is single-threaded but mitigated by worker pool - - (LOW) Redis `blpop` serialization with 4 workers, acceptable for typical workloads - - (MEDIUM) `bulk_create()` fallback to individual save is O(n) instead of O(1) - one bad event ruins entire batch - - (LOW) Jinja2 for debug stats instead of f-strings - - (LOW) `datetime.utcnow()` deprecated (pool.py) - - (COSMETIC) Typo `DISPATCHER_DB_DOWNTOWN_TOLLERANCE` (kept for backward compat) - - (LOW) `write_attempt_order.append(preferred_queue)` should be `queue_actual` (pool.py:296) -- [x] System assessment: **KEEP** - System is well designed for its purpose: - - PG NOTIFY as message bus eliminates external dependencies (no RabbitMQ/Celery) - - Autoscaling pool with memory-based limits is practical - - Transaction-safe publishing via `connection.on_commit()` - - Orphan recovery for dead worker processes - - Graceful degradation during short DB outages - - Clean separation: dispatcher (PG) for tasks, callback receiver (Redis) for events - - **Replacement is not needed** - -**Statistics:** - -- 45+ `@task()` decorated functions/classes in codebase -- 13 periodic tasks in CELERYBEAT_SCHEDULE (interval 20s - 4h) -- 3 queue types: dynamic task queue, broadcast (tower_broadcast_all), callback (callback_tasks) -- 20+ `apply_async()`/`delay()` calls from models, signals, views - -### 4.11 Phase 4 Result - -- [x] `serializers.py` broken into 14+ modules (2026-02-07) -- [x] `views/__init__.py` broken into 24 modules (2026-02-07) -- [x] `access.py` broken into 15 modules (2026-02-10) -- [x] `defaults.py` broken into 10 modules (2026-02-10) -- [x] `signals.py` cleaned and optimized (2026-02-14) -- [x] Django models optimized, 4 new indexes (2026-02-13) -- [x] Management commands cleaned, 4 deprecated deleted (2026-02-17) -- [x] Dispatch/Task system reviewed and documented (2026-02-20) -- [x] Middleware reviewed and optimized (2026-02-20) -- [ ] All tests still pass -- [ ] API response time improved by 20%+ - ---- - -## PHASE 5: Forail UI — Custom Frontend from Scratch (Week 12-24) - -**Decision:** Instead of incremental upgrade (Option A) or ansible-ui integration (Option B), **Option C: completely new custom frontend** was chosen. Reasons: - -- Old UI (`awx/ui/`) is React 17, PatternFly 4, CRA — too old for upgrade -- ansible-ui (Option B) was tried but looks the same as AWX — no unique Forail identity -- Custom frontend gives full control over design and tech stack - -**Tech Stack:** - -| Technology | Purpose | -| -------------------------- | -------------------------- | -| React 18 + TypeScript | Framework | -| Vite 6 | Build tool | -| Tailwind CSS 3 + shadcn/ui | Styling + components | -| TanStack Query v5 | API fetching, caching | -| TanStack Table v8 | Headless tables | -| React Router v7 | Client-side routing | -| React Hook Form + Zod | Forms and validation | -| Zustand | Global state (auth, theme) | -| Recharts | Dashboard charts | -| Lucide React | Icons | - -**Visual Style:** Enterprise/professional (Datadog, Grafana, Linear style). Indigo primary (#4F46E5), Inter font, dark mode first-class, collapsible sidebar layout. - -**Old UI remains** at `/ui_legacy/` as fallback. - -### 5.1 Scaffolding + Infrastructure - COMPLETED (2026-02-22) - -Created complete project foundation in `awx/ui_next/`: - -- [x] Config: `package.json`, `vite.config.ts`, `tsconfig.json`, `tsconfig.node.json`, `tailwind.config.ts`, `postcss.config.js`, `components.json`, `index.html` -- [x] Entry: `src/main.tsx` (React + QueryClient + BrowserRouter), `src/App.tsx` (auth-gated routing), `src/index.css` (CSS variables for light/dark theme) -- [x] API layer: `src/api/client.ts` (Axios with CSRF, session auth, 401 interceptor), `src/api/types.ts`, `src/api/hooks/useAuth.ts`, `src/api/hooks/useDashboard.ts` -- [x] Stores: `src/stores/auth.ts`, `src/stores/theme.ts` (persist to localStorage) -- [x] UI components (shadcn/ui style): Button, Card, Input, Label, Badge -- [x] Layout: AppLayout (sidebar + topbar + content), Sidebar (collapsible, grouped nav), TopBar (dark mode toggle, user menu) -- [x] Pages: Login, Dashboard (stats cards + recent jobs + Recharts bar chart), NotFound (404) -- [x] Makefile: `npm ci && npm run build` instead of git clone -- [x] urls.py: SPA catch-all (`r'^.*$'`) -- [x] Dockerfile.j2: ui-builder stage with Node 20 -- [x] .gitignore: Removed `awx/ui_next/src`, added tsc artifacts -- [x] **Build passes** — `tsc -b && vite build` → `build/awx/index_awx.html` - -### 5.2 Data Screens - COMPLETED (2026-02-23) - -- [x] Jobs — list with search/filter/pagination, detail with stdout output and host summary (2026-02-22) -- [x] Templates — Job Templates list with launch button, detail with config/credentials/labels (2026-02-23) -- [x] Inventories — list, detail with tabs (hosts, groups, details) + search/pagination (2026-02-23) -- [x] Projects — list with SCM info, detail with sync button (2026-02-23) -- [x] Credentials — list with type/kind badges, detail with owners (2026-02-23) -- [x] Hosts — global list of all hosts with health badges (2026-02-23) -- [x] Schedules — list with next run, enabled badge, template link (2026-02-23) -- [x] Activity Stream — timeline with operation badges and actor info (2026-02-23) - -### 5.3 Full CRUD + Admin - COMPLETED (2026-02-24) - -- [x] Organizations — list with resource counts, detail with stat cards (2026-02-23) -- [x] Users — list with role badges (Admin/Auditor/User) (2026-02-23) -- [x] Teams — list with organization info (2026-02-23) -- [x] Settings — categories card grid (2026-02-23) -- [x] CRUD forms (add/edit) for all resources — Job Templates, Projects, Inventories, Credentials, Organizations (2026-02-24) -- [x] Delete with ConfirmDialog on all detail pages (2026-02-24) -- [x] Launch prompt dialog for Job Templates with `ask_*_on_launch` support (2026-02-24) -- [x] Toast notifications (Sonner) for all mutations + global error handler (2026-02-24) -- [x] Instances page — list + detail with health check action (2026-02-24) -- [x] Instance Groups page — list + detail with capacity/policy info (2026-02-24) -- [x] Execution Environments page — list + detail with image/config info (2026-02-24) -- [x] Sidebar Admin section expanded (Instances, Instance Groups, Execution Env) (2026-02-24) -- [x] Detail pages for Users, Teams, Hosts, Schedules (2026-02-24) -- [x] WebSocket for real-time job status updates (`useWebSocket` hook in AppLayout) (2026-02-24) -- [x] xterm.js terminal for job stdout output with ANSI color support (2026-02-24) -- [ ] Schedules — RRule editor - -### 5.4 Advanced Features - -- [ ] Workflow visual editor (React Flow or D3) -- [ ] Topology View (mesh visualization) -- [ ] Monaco Editor for YAML/JSON editing - -### 5.5 Phase 5 Result - -- [x] Frontend build passes (2026-02-22) -- [x] Login page works with session auth (2026-02-22) -- [x] Dashboard displays data from `/api/v2/dashboard/` (2026-02-22) -- [x] Dark mode toggle works (2026-02-22) -- [x] Navigation works (sidebar, routing) (2026-02-22) -- [x] Jobs screen with stdout output and host summary (2026-02-22) -- [x] Templates list + detail + launch (2026-02-23) -- [x] Projects list + detail + sync (2026-02-23) -- [x] Inventories list + detail with hosts/groups tabs (2026-02-23) -- [x] Credentials list + detail (2026-02-23) -- [x] Organizations list + detail (2026-02-23) -- [x] Users list, Teams list (2026-02-23) -- [x] Hosts, Schedules, Activity Stream, Settings screens (2026-02-23) -- [x] CRUD forms (add/edit) for Job Templates, Projects, Inventories, Credentials, Organizations (2026-02-24) -- [x] Delete with confirmation (ConfirmDialog) on all detail pages (2026-02-24) -- [x] Launch prompt dialog with dynamic ask\_\*\_on_launch fields (2026-02-24) -- [x] Toast notifications (Sonner) for all mutations + global error handler (2026-02-24) -- [x] Instances, Instance Groups, Execution Environments — list + detail pages (2026-02-24) -- [x] Sidebar Admin section with new pages (2026-02-24) -- [x] Detail pages for Users, Teams, Hosts, Schedules (2026-02-24) -- [x] WebSocket real-time job status updates (2026-02-24) -- [x] xterm.js terminal for job stdout output (2026-02-24) -- [ ] Advanced features (workflow editor, Monaco editor) - ---- - -## PHASE 6: Dockerfile Modernization (Week 18-20) - -### 6.1 Python 3.12 Support - -**File:** `tools/ansible/roles/dockerfile/templates/Dockerfile.j2` - -Currently hardcodes Python 3.11 in 20+ places: - -```diff - # Builder stage -- python3.11 \ -- "python3.11-devel" \ -- "python3.11-pip" \ -- "python3.11-setuptools" \ -- "python3.11-packaging" \ -- "python3.11-psycopg2" \ -+ python3.12 \ -+ "python3.12-devel" \ -+ "python3.12-pip" \ -+ "python3.12-setuptools" \ -+ "python3.12-packaging" \ -+ "python3.12-psycopg2" \ - -- RUN pip3.11 install -vv build -+ RUN pip3.12 install -vv build -``` - -**Also update:** - -- All `python3.11` references in runtime stage -- venv paths: `/var/lib/awx/venv/awx/lib/python3.11/` → `python3.12/` -- egg-link paths -- site-packages paths - -**Total places to change:** ~20 in Dockerfile.j2 - -### 6.2 Alternative Ubuntu Base Image - -**New file:** `tools/ansible/roles/dockerfile/templates/Dockerfile.ubuntu.j2` - -```dockerfile -FROM ubuntu:24.04 AS builder -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y \ - python3.12 python3.12-venv python3.12-dev \ - python3-pip gcc g++ git make \ - libpq-dev libldap2-dev libsasl2-dev \ - libxml2-dev libxslt1-dev libffi-dev libssl-dev \ - libxmlsec1-dev libxmlsec1-openssl \ - pkg-config swig unzip nodejs npm - -# ... rest of the build -``` - -### 6.3 Makefile Support for Both Images - -**File:** `Makefile` - -```makefile -BASE_IMAGE ?= centos -PYTHON_VERSION ?= 3.12 - -ifeq ($(BASE_IMAGE),ubuntu) - DOCKERFILE_TEMPLATE = Dockerfile.ubuntu.j2 -else - DOCKERFILE_TEMPLATE = Dockerfile.j2 -endif -``` - -### 6.4 Multi-stage Build Optimization - -- Separate Python dependency install from source code -- Use Docker layer caching more efficiently -- Reduce final image size - -### 6.5 Phase 6 Result - -- [x] Python 3.12 as default -- [x] Ubuntu 24.04 Dockerfile works -- [x] CentOS Stream 9 Dockerfile still works -- [ ] Image size reduced by 15%+ - ---- - -## PHASE 7: Docker Compose Production (Week 20-22) - -### 7.1 Production Docker Compose - -**New file:** `tools/docker-compose-prod/docker-compose.yml` - -Services: - -1. **postgres** - PostgreSQL 15/16 -2. **redis** - Redis 7 -3. **forail-web** - Web server (nginx + uwsgi + daphne) -4. **forail-task** - Task dispatcher -5. **forail-receptor** - Receptor mesh -6. **nginx** - Reverse proxy with TLS - -### 7.2 Environment Configuration - -**New file:** `tools/docker-compose-prod/.env.example` - -```env -# Database -POSTGRES_USER=forail -POSTGRES_PASSWORD=changeme -POSTGRES_DB=forail - -# Forail -FORAIL_ADMIN_USER=admin -FORAIL_ADMIN_PASSWORD=changeme -FORAIL_SECRET_KEY=generate-random-key-here - -# TLS -FORAIL_HOSTNAME=forail.example.com -``` - -### 7.3 Backup/Restore - -**New files:** - -- `tools/docker-compose-prod/scripts/backup.sh` -- `tools/docker-compose-prod/scripts/restore.sh` - -### 7.4 Health Checks - -Add to docker-compose.yml: - -```yaml -healthcheck: - test: ["CMD", "awx-manage", "check", "--deploy"] - interval: 30s - timeout: 10s - retries: 3 -``` - -### 7.5 Phase 7 Result - -- [x] `docker compose up -d` starts Forail in production mode -- [x] TLS works with self-signed or Let's Encrypt certificate -- [x] Backup/restore scripts work -- [x] Health checks work - ---- - -## PHASE 8: Testing and QA (Week 22-24) - -### 8.1 Test Matrix - -| Test | Command | Expected Result | -| -------------------- | -------------------------------------- | ----------------------------------------------------------- | -| Unit (Python) | `make test_unit` | ~~100% pass~~ **DONE** (1237 passed, 0 failed) | -| Unit (Frontend) | `npm test` | ~~100% pass~~ **DONE** (42 passed, 0 failed — Vitest setup) | -| Functional (API) | `py.test awx/main/tests/functional/` | ~~95%+ pass~~ **DONE** (989 passed, 0 failed, 1 skipped) | -| Lint (Python) | `make api-lint` | ~~0 errors~~ **DONE** (flake8, 0 errors) | -| Lint (Frontend) | `npm run lint` | ~~0 errors~~ **DONE** (tsc --noEmit, 0 errors) | -| Security (Python) | `pip-audit` | ~~0 critical CVE~~ **DONE** (71→15 CVE, 0 critical runtime) | -| Security (Container) | `trivy image forail:latest` | ~~0 critical~~ **DONE** (0 CRITICAL) | -| Build (CentOS) | `make Dockerfile.dev && docker build` | ~~Success~~ **DONE** (forail:centos-dev, 882MB) | -| Build (Ubuntu) | `Dockerfile.ubuntu.j2 && docker build` | ~~Success~~ **DONE** (forail:ubuntu-dev, 932MB) | - -### 8.2 Performance Testing - -Establish baseline on 4 CPU / 8GB RAM: - -| Metric | Target | -| ------------------------------- | ------- | -| API /api/v2/jobs/ (100 items) | < 500ms | -| API /api/v2/hosts/ (1000 items) | < 1s | -| Job launch latency | < 3s | -| Login response | < 300ms | -| UI initial load | < 3s | - -### 8.3 Phase 8 Result - -- [x] Unit tests (Python) — 1237 passed, 0 failed -- [x] Unit tests (Frontend) — 42 passed, 0 failed -- [x] Functional API tests — 989 passed, 0 failed, 1 skipped -- [x] Lint (Python) — flake8, 0 errors (40 unused imports removed) -- [x] Lint (Frontend) — tsc --noEmit, 0 errors -- [x] Security (Python) — pip-audit, 71→15 CVE (0 critical runtime, 19 packages updated) -- [x] Security (Container) — trivy, 0 critical (0 CRITICAL, 28 HIGH OS-level) -- [x] Build (CentOS) — docker build Success (forail:centos-dev, 882MB) -- [x] Build (Ubuntu) — docker build Success (forail:ubuntu-dev, 932MB, 4 fixes) -- [ ] Performance targets achieved (optional, for Phase 9) - ---- - -## PHASE 9: Release (Week 24-25) - -### 9.1 Versioning - -Format: **CalVer** - `YYYY.MM.PATCH` -First release: `2026.XX.0` - -### 9.2 Release Checklist - -- [x] All tests pass (unit 1237, functional 989, frontend 42, lint 0 errors) -- [x] Changelog updated (CHANGELOG.md — all 9 phases documented) -- [x] VERSION file updated (24.6.2 → 2026.03.0, CalVer format) -- [x] CI/CD pipeline created (GitHub Actions: `.github/workflows/ci.yml`) -- [ ] Git tag created (v2026.03.0) -- [ ] Container image built and pushed (Harbor registry) -- [ ] Release notes written - -### 9.3 Container Registry - -```bash -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --tag registry.example.com/forail:2026.XX.0 \ - --tag registry.example.com/forail:latest \ - --push . -``` - ---- - -## Timeline (Summary) - -``` -Week 1-2: Phase 1 - Build stabilization -Week 2-3: Phase 2 - Rebranding (Forail) -Week 3-6: Phase 3 - Dependency modernization (8 groups) -Week 6-12: Phase 4 - Backend refactoring (11 sub-tasks) -Week 12-18: Phase 5 - Frontend refactoring (8 sub-tasks) -Week 18-20: Phase 6 - Dockerfile modernization -Week 20-22: Phase 7 - Docker Compose production -Week 22-24: Phase 8 - Testing and QA -Week 24-25: Phase 9 - Release -``` - -**Total: ~25 weeks for a single developer.** - ---- - -## Priority Order (What to Do First) - -If you don't have 25 weeks, here is the order by priority: - -1. **Phase 1** (MANDATORY) - Build must work -2. **Phase 3, Groups 1-3** (HIGH) - Critical security dependencies -3. **Phase 4.1-4.3** (HIGH) - Breaking up large files -4. **Phase 6.1** (MEDIUM) - Python 3.12 -5. **Phase 7** (MEDIUM) - Docker production -6. **Phase 2** (LOW) - Rebranding (can be done anytime) -7. **Phase 5** (LOW but LARGE) - Frontend (biggest effort) diff --git a/docs/plan_development.md b/docs/plan_development.md deleted file mode 100644 index 2d6c27e..0000000 --- a/docs/plan_development.md +++ /dev/null @@ -1,601 +0,0 @@ -# Forail - Development Plan (HISTORICAL — COMPLETED) - -> **Status: COMPLETED.** This plan was executed and all phases were delivered as of v2026.04.0. This document is retained for historical reference only. For the current state, see [Release Notes v2026.04.0](RELEASE_NOTES_v2026.04.0.md). - ---- - -## Current State Overview - -**Latest release:** AWX 24.6.1 (July 2, 2024) -**Status:** Releases are paused. Red Hat is refactoring AWX into a service-oriented architecture. -**Devel branch:** Active, already uses Python 3.12, Django 5.2.8, dispatcherd instead of Celery. -**Alternative:** Ascender (fork by CIQ/Rocky Linux team) - version 25.3.3 (Feb 2026). - -### Known Issues with AWX 24.6.1 - -| Problem | Description | -| -------------------- | -------------------------------------------------------------------------------------- | -| OpenSSL pin | Dockerfile pins `openssl-3.0.7` which no longer exists in CentOS Stream 9 repositories | -| Django conflict | `django-ansible-base` requires Django >=4.2.16, but AWX pins 4.2.10 | -| Python 3.12.8+ crash | `argparse._parse_known_args()` got a new `intermixed` parameter, breaks AWX CLI | -| Missing VERSION file | Build from source fails without a `VERSION` file in the root | -| Node.js 18 | Entering maintenance LTS, needs migration to Node.js 20+ | - ---- - -## Phase 0: Preparation (Week 1-2) - -### 0.1 Fork and Environment - -```bash -# Fork the AWX repository -git clone https://github.com/ansible/awx.git awx-fork -cd awx-fork -git checkout -b modernization 24.6.1 - -# Fork the AWX Operator repository -git clone https://github.com/ansible/awx-operator.git awx-operator-fork -``` - -### 0.2 Local Build and Test Infrastructure - -Before any changes, ensure the original build works: - -```bash -# Build development image -make docker-compose-build - -# Run tests -make docker-compose-test - -# Run unit tests -make test_unit -``` - -Document all errors that appear during the 24.6.1 build as these are the first bugs to fix. - -### 0.3 CI/CD Pipeline - -Set up the GitHub Actions pipeline with: - -- Build matrix: Python 3.11 / 3.12 / 3.13 -- OS matrix: CentOS Stream 9 / Ubuntu 24.04 -- Automatic unit tests on every push -- Container image build and scanning (Trivy/Grype) - ---- - -## Phase 1: Critical Fixes (Week 2-4) - -Goal: Make AWX 24.6.1 actually buildable and runnable. - -### 1.1 Fix OpenSSL Pinning - -**File:** `tools/ansible/roles/dockerfile/templates/Dockerfile.j2` - -```diff -- openssl-3.0.7 \ -+ openssl \ -``` - -Remove the hard-coded version and use the one from the repository. - -### 1.2 Fix Django Dependency Conflict - -**File:** `requirements/requirements.txt` - -```diff -- Django==4.2.10 -+ Django==4.2.16 -``` - -**File:** `requirements/requirements.txt` - -```diff -- sqlparse==0.5.1 -+ sqlparse==0.5.2 -``` - -Update `django-ansible-base` to a compatible version. Cherry-pick the fix from PR #15596. - -### 1.3 Fix Python 3.12.8+ / 3.13 Argparse Crash - -**File:** `awx/main/utils/common.py` (or wherever `HelpfulArgumentParser` is located) - -Cherry-pick the fix from PR #15692 - add the `intermixed` parameter to the override method: - -```python -# Before (broken on Python 3.12.8+): -def _parse_known_args(self, arg_strings, namespace): - -# After: -def _parse_known_args(self, arg_strings, namespace, intermixed=False): -``` - -### 1.4 Add VERSION File - -```bash -echo "24.6.2-custom" > VERSION -``` - -### 1.5 Validation - -```bash -make docker-compose-build # Must pass without errors -make docker-compose # Must start up -make test_unit # All tests must pass -``` - ---- - -## Phase 2: Base Image Modernization (Week 4-6) - -Goal: Move from CentOS Stream 9 to a more modern base image. - -### 2.1 Dual Base Image Support - -Create an alternative Dockerfile for Ubuntu 24.04: - -**File:** `tools/ansible/roles/dockerfile/templates/Dockerfile.ubuntu.j2` - -Key differences compared to CentOS: - -- `apt` instead of `dnf` -- Different package names (e.g., `libpq-dev` instead of `postgresql-devel`) -- Python 3.12 comes from the system (no need for `dnf module`) -- Node.js 20 from NodeSource repository - -```dockerfile -FROM ubuntu:24.04 AS base - -RUN apt-get update && apt-get install -y \ - python3.12 python3.12-venv python3.12-dev \ - python3-pip \ - libpq-dev libxml2-dev libxslt1-dev \ - libffi-dev libssl-dev \ - git curl wget \ - nginx \ - && rm -rf /var/lib/apt/lists/* -``` - -### 2.2 Makefile Support for Both Images - -```makefile -# Add to Makefile -BASE_IMAGE ?= centos # or ubuntu -ifeq ($(BASE_IMAGE),ubuntu) - DOCKERFILE_TEMPLATE = Dockerfile.ubuntu.j2 -else - DOCKERFILE_TEMPLATE = Dockerfile.j2 -endif -``` - -### 2.3 Multi-arch Build - -Add `docker buildx` support for AMD64 and ARM64: - -```bash -make docker-compose-buildx ARCH="linux/amd64,linux/arm64" -``` - ---- - -## Phase 3: Dependency Modernization (Week 6-10) - -Goal: Update all dependencies to the latest compatible versions. - -### 3.1 Strategy - -**Do NOT update everything at once.** Work in groups with testing after each group: - -| Priority | Group | Packages | -| -------- | ------------------ | -------------------------------------------- | -| 1 | Core Framework | Django 4.2.16 → 5.2.x, DRF, Channels, Daphne | -| 2 | Database and Cache | psycopg, redis, hiredis | -| 3 | Cryptography | cryptography, pyopenssl, pyjwt, pynacl | -| 4 | Async/Network | aiohttp, twisted, autobahn | -| 5 | Cloud Providers | boto3, azure-\*, kubernetes, openshift | -| 6 | Observability | opentelemetry-\*, prometheus-client | -| 7 | Other | all remaining ~130 packages | - -### 3.2 Django Upgrade Path - -This is the most critical change. I suggest a two-step approach: - -**Step A:** Django 4.2.10 → 4.2.16 (minor bump, minimal risk) - -- Fixes the dependency conflict with `django-ansible-base` -- Does not introduce breaking changes (LTS version) -- Test: `make test_unit && make test_coverage` - -**Step B:** Django 4.2.16 → 5.2.x (major bump, higher risk) - -- Needed only if Python 3.13 support is desired -- Breaking changes in Django 5.x: - - `DEFAULT_AUTO_FIELD` must be explicitly set - - Deprecated `django.utils.timezone.utc` (use `datetime.timezone.utc`) - - Changes in `Form` and `ModelForm` rendering - - `HttpResponse.headers` dictionary access instead of `__setitem__` -- Requires review of every AWX Django model and view - -### 3.3 Node.js Upgrade - -```diff -# In Dockerfile template: -- dnf module enable nodejs:18 -+ # For CentOS: -+ dnf module enable nodejs:20 -+ # For Ubuntu: -+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - -+ apt-get install -y nodejs -``` - -Run UI build and verify: - -```bash -make clean/ui ui -``` - -### 3.4 Dependency Tracking Tool - -Use `pip-audit` for security checks: - -```bash -pip install pip-audit -pip-audit -r requirements/requirements.txt -``` - -Use `pip-compile` (from `pip-tools`) for consistent resolution: - -```bash -pip install pip-tools -pip-compile requirements/requirements.in --output-file requirements/requirements.txt -``` - ---- - -## Phase 4: Docker Compose Production Setup (Week 10-13) - -Goal: Create a more official docker-compose for production since that is what the community demands. - -### 4.1 Architecture - -``` - ┌─────────┐ - │ Nginx │:443/:80 - └────┬────┘ - │ - ┌──────────┼──────────┐ - │ │ │ - ┌─────┴────┐ ┌──┴───┐ ┌───┴─────┐ - │ AWX Web │ │ AWX │ │ AWX │ - │ (API/UI) │ │ Task │ │Receptor │ - └─────┬────┘ └──┬───┘ └───┬─────┘ - │ │ │ - ┌─────────┼─────────┼─────────┘ - │ │ │ -┌───┴────┐ ┌─┴──┐ ┌───┴────────┐ -│Postgres│ │Redis│ │ Receptor │ -│ 15 │ │ 7 │ │ Worker(s) │ -└────────┘ └────┘ └────────────┘ -``` - -### 4.2 Key Files - -``` -awx-docker-prod/ -├── docker-compose.yml # Main compose file -├── docker-compose.override.yml # Local overrides -├── .env # Environment variables -├── nginx/ -│ ├── nginx.conf # Nginx configuration -│ └── ssl/ # TLS certificates -├── settings/ -│ ├── settings.py # AWX custom settings -│ ├── credentials.py # Credential configuration -│ └── receptor.conf # Receptor configuration -├── backup/ -│ └── backup.sh # Backup script for PostgreSQL -└── scripts/ - ├── init.sh # Initialization (migrations, admin user) - ├── healthcheck.sh # Health check script - └── upgrade.sh # Upgrade procedure -``` - -### 4.3 AWX Settings.py - -Create a custom `settings.py` that AWX loads: - -```python -# settings/settings.py -DATABASES = { - 'default': { - 'ATOMIC_REQUESTS': True, - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get('DATABASE_NAME', 'awx'), - 'USER': os.environ.get('DATABASE_USER', 'awx'), - 'PASSWORD': os.environ.get('DATABASE_PASSWORD', ''), - 'HOST': os.environ.get('DATABASE_HOST', 'postgres'), - 'PORT': os.environ.get('DATABASE_PORT', '5432'), - } -} - -CACHES = { - 'default': { - 'BACKEND': 'awx.main.cache.AWXRedisCache', - 'LOCATION': 'redis://redis:6379/1', - } -} - -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - 'hosts': [('redis', 6379)], - 'capacity': 10000, - }, - }, -} - -BROADCAST_WEBSOCKET_PORT = 8052 -BROADCAST_WEBSOCKET_PROTOCOL = 'http' -``` - -### 4.4 Backup Strategy - -```bash -#!/bin/bash -# backup/backup.sh -BACKUP_DIR="/backups/$(date +%Y%m%d_%H%M%S)" -mkdir -p "$BACKUP_DIR" - -# PostgreSQL dump -docker compose exec -T postgres pg_dump -U awx awx | gzip > "$BACKUP_DIR/awx_db.sql.gz" - -# AWX secret key -cp .env "$BACKUP_DIR/env.backup" - -# Projects -tar czf "$BACKUP_DIR/projects.tar.gz" -C volumes/ projects/ - -echo "Backup saved to: $BACKUP_DIR" -``` - ---- - -## Phase 5: Testing and QA (Week 13-16) - -### 5.1 Test Matrix - -| Test Type | Tool | What is Tested | -| ------------- | ------------------ | ------------------------------------------- | -| Unit | pytest | API, models, serializers, utility functions | -| Integration | pytest + Docker | Full stack with real database and Redis | -| UI | Cypress/Playwright | Frontend workflows | -| API | pytest + requests | REST API endpoints | -| Performance | locust/k6 | Concurrent users, job throughput | -| Security | pip-audit, Trivy | CVE scanning of dependencies and images | -| Compatibility | tox | Python 3.11, 3.12, 3.13 | - -### 5.2 Minimum Acceptance Criteria - -Before each release, the following MUST work: - -1. **Build:** `make docker-compose-build` without errors on both base images -2. **Start:** `docker compose up` starts all services without crashes -3. **Login:** Admin can log in to the Web UI -4. **Job:** An Ansible playbook can be created and run -5. **Inventory:** An inventory can be added and synced (static + dynamic) -6. **Credentials:** A credential can be created and used -7. **API:** All CRUD endpoints work (`/api/v2/`) -8. **WebSocket:** Real-time log streaming works during jobs -9. **Backup/Restore:** PostgreSQL dump and restore work -10. **Upgrade:** Migration from the previous version works without data loss - -### 5.3 Performance Baseline - -Establish a performance baseline on standard hardware (4 CPU, 8GB RAM): - -| Metric | Target | -| ----------------------- | ------------ | -| Startup time | < 60 seconds | -| Login response | < 500ms | -| API listing (100 items) | < 1s | -| Job launch latency | < 5s | -| Concurrent users | >= 20 | -| Concurrent jobs | >= 10 | - ---- - -## Phase 6: Versioning and Release (Week 16-17) - -### 6.1 Versioning Scheme - -I suggest CalVer (calendar-based) since Red Hat plans the same: - -``` -Format: YYYY.MM.PATCH -Example: 2026.03.0, 2026.03.1 (hotfix) -``` - -### 6.2 Release Process - -``` -1. Feature freeze (one week before release) -2. Create release branch: release/2026.03 -3. Run the full test matrix -4. Fix blocker bugs -5. Tag: git tag -a v2026.03.0 -6. Build production images -7. Push to container registry (ghcr.io or quay.io) -8. Update AWX Operator with the new version -9. Write release notes -10. Publish on GitHub Releases -``` - -### 6.3 Container Registry - -```bash -# Build and push -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --tag ghcr.io/USERNAME/awx:2026.03.0 \ - --tag ghcr.io/USERNAME/awx:latest \ - --push . -``` - ---- - -## Phase 7: Upstream Sync Strategy (Continuous) - -### 7.1 Tracking Upstream - -The AWX devel branch is still being developed. We need to sync regularly: - -```bash -# Add upstream remote -git remote add upstream https://github.com/ansible/awx.git - -# Weekly sync -git fetch upstream devel -git log --oneline upstream/devel..HEAD # See differences - -# Cherry-pick relevant commits -git cherry-pick -``` - -### 7.2 What to Track from Upstream - -- **Always:** Security fixes (CVE) -- **Always:** Bug fixes for existing functionality -- **Selectively:** New features from the service architecture -- **Carefully:** Large refactoring commits (can break stability) - -### 7.3 Contribute Back - -Send all generic fixes (not specific to our fork) as PRs upstream: - -```bash -git checkout -b fix/openssl-pinning upstream/devel -# Make the fix -git push origin fix/openssl-pinning -# Create a PR on github.com/ansible/awx -``` - ---- - -## Phase 8: Production Docker Deploy (Week 17-20) - -### 8.1 Server Requirements - -| Component | Minimum | Recommended | -| -------------- | ------------------------ | ---------------- | -| CPU | 4 cores | 8 cores | -| RAM | 8 GB | 16 GB | -| Disk | 40 GB SSD | 100 GB SSD | -| OS | Ubuntu 22.04+ / Rocky 9+ | Ubuntu 24.04 LTS | -| Docker | 24.0+ | 27.0+ | -| Docker Compose | v2.20+ | v2.30+ | - -### 8.2 Server Deploy Script - -```bash -#!/bin/bash -# deploy.sh - Single-command deploy on a bare server - -# 1. Install Docker -curl -fsSL https://get.docker.com | sh -systemctl enable docker --now - -# 2. Clone our configuration -git clone https://github.com/USERNAME/awx-docker-prod.git /opt/awx -cd /opt/awx - -# 3. Configuration -cp .env.example .env -# Edit .env with actual values - -# 4. Start -docker compose up -d - -# 5. Initialization -docker compose exec forail-web awx-manage migrate --noinput -docker compose exec forail-web awx-manage createsuperuser \ - --username admin --email admin@example.com --noinput -docker compose exec forail-web awx-manage update_password \ - --username admin --password "$ADMIN_PASSWORD" -``` - -### 8.3 Monitoring - -Add Prometheus metrics and Grafana dashboard: - -```yaml -# docker-compose.override.yml -services: - prometheus: - image: prom/prometheus:latest - volumes: - - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml - ports: - - "9090:9090" - - grafana: - image: grafana/grafana:latest - ports: - - "3000:3000" - environment: - - GF_SECURITY_ADMIN_PASSWORD=admin -``` - -AWX already has built-in OpenTelemetry and Prometheus support - it just needs scraping configured. - ---- - -## Alternative: Use Ascender Instead of a Fork - -Before starting with our own fork, it is worth considering **Ascender**: - -| Aspect | Our Fork | Ascender | -| ------------- | ---------------------- | ---------------------------------- | -| Version | Based on 24.6.1 | 25.3.3 (Feb 2026) | -| Maintenance | By ourselves | CIQ team (Rocky Linux) | -| Base Image | CentOS/Ubuntu | Rocky Linux 9 | -| Deploy | Docker + K8s | K8s (multiple distros) + Single VM | -| Support | Community | Commercial option | -| Upstream sync | Manual | Regular | -| Risk | High (self-maintained) | Low (professional team) | - -**Recommendation:** If the goal is just to run AWX on a new server, Ascender is a more pragmatic choice. If the goal is learning and full control, a custom fork makes sense. - ---- - -## Timeline - -``` -Week 1-2: Phase 1 - Build stabilization ✓ COMPLETED -Week 2-3: Phase 2 - Rebranding (Forail) ✓ COMPLETED -Week 3-6: Phase 3 - Dependency modernization ✓ COMPLETED -Week 6-12: Phase 4 - Backend refactoring ✓ COMPLETED -Week 12-18: Phase 5 - Frontend refactoring ✓ COMPLETED -Week 18-20: Phase 6 - Dockerfile modernization ✓ COMPLETED -Week 20-22: Phase 7 - Docker Compose production ✓ COMPLETED -Week 22-24: Phase 8 - Testing and QA ✓ COMPLETED -Week 24-25: Phase 9 - Release (2026.03.0) → IN PROGRESS -``` - -**Total: ~20 weeks (5 months) for a single developer.** -With a team of 2-3 people, it can be reduced to 2-3 months. - ---- - -## Resources and References - -- [AWX GitHub](https://github.com/ansible/awx) -- [AWX Operator GitHub](https://github.com/ansible/awx-operator) -- [AWX on K3s (kurokobo)](https://github.com/kurokobo/awx-on-k3s) -- [Ascender (CIQ fork)](https://github.com/ctrliq/ascender) -- [AWX Refactoring Plan - Ansible Forum](https://forum.ansible.com/t/refactoring-awx-into-a-pluggable-service-oriented-architecture/7404) -- [AWX Future - Ansible Forum](https://forum.ansible.com/t/is-there-a-future-for-awx/44527) -- [Migration to AAP 2.5 - Red Hat](https://developers.redhat.com/articles/2025/08/13/migrating-configurations-awx-24-aap-25) -- [AWX without Kubernetes - Hetzner](https://community.hetzner.com/tutorials/awx-without-kubernetes/) diff --git a/docs/plan_separation.md b/docs/plan_separation.md deleted file mode 100644 index 073c8a5..0000000 --- a/docs/plan_separation.md +++ /dev/null @@ -1,345 +0,0 @@ -# Project Separation Plan — Separate Repositories (HISTORICAL — COMPLETED) - -> **Status: COMPLETED.** The monorepo was successfully separated into independent repositories. This document is retained for historical reference. - ---- - -## Overview - -The Forail platform currently resides in a single monorepo. This plan defines the separation into **5 independent repositories** connected through CI/CD pipelines. - -``` -forail-platform/ -├── forail-backend ← Django API + Task Engine + Celery -├── forail-frontend ← React UI (Vite + Tailwind) -├── forail-devops ← Docker, Compose, Nginx, CI/CD, infra -├── forail-assistant ← Ollama + ChromaDB RAG (future) -└── forail-mobile ← Android/iOS app (future) -``` - ---- - -## Phase 1: forail-backend - -**Repo:** `forail-platform/forail-backend` - -### What goes in - -| Source (current monorepo) | Destination in new repo | -| -------------------------------------------------------- | ----------------------- | -| `forail/` (Python package) | `forail/` | -| `forail/main/`, `forail/api/`, `forail/conf/`, `forail/sso/` | Same | -| `forail/settings/` | `forail/settings/` | -| `manage.py` | `manage.py` | -| `requirements/` | `requirements/` | -| `tools/` (management scripts) | `tools/` | -| `setup.cfg`, `setup.py`, `pyproject.toml` | Root | - -### Documentation included with backend - -- `docs/wiki/02-backend-django.md` -- `docs/wiki/04-task-engine.md` -- `docs/wiki/05-authentication-rbac.md` -- `docs/wiki/06-database-schema.md` -- `docs/wiki/09-testing-guide.md` (Python section) -- `docs/wiki/11-api-reference.md` -- `docs/wiki/12-configuration-reference.md` - -### CI/CD for backend repo - -```yaml -# .gitlab-ci.yml -stages: - - lint # flake8 - - test # pytest (unit + functional) - - build # Docker image (forail-backend:tag) - - security # pip-audit, trivy - - publish # Push image to registry -``` - -### Artifact - -- Docker image: `ghcr.io/forail-platform/forail-backend:` -- API documentation (auto-generated) - ---- - -## Phase 2: forail-frontend - -**Repo:** `forail-platform/forail-frontend` - -### What goes in - -| Source (current monorepo) | Destination in new repo | -| ----------------------------------- | ----------------------- | -| `src/` (React application) | `src/` | -| `public/` | `public/` | -| `index.html` | `index.html` | -| `package.json`, `package-lock.json` | Root | -| `vite.config.ts` | Root | -| `tailwind.config.ts` | Root | -| `tsconfig.json`, `tsconfig.*.json` | Root | -| `postcss.config.js` | Root | -| `.eslintrc.*` | Root | - -### Documentation included with frontend - -- `docs/wiki/03-frontend-react.md` -- `docs/wiki/09-testing-guide.md` (Frontend section) - -### CI/CD for frontend repo - -```yaml -# .gitlab-ci.yml -stages: - - lint # tsc --noEmit, eslint - - test # vitest - - build # vite build → static bundle - - publish # Upload artifact or Docker image with nginx -``` - -### Artifact - -- Build folder (`dist/`) — static files -- Optional Docker image: `ghcr.io/forail-platform/forail-frontend:` (nginx + static files) - -### Configuration - -- API URL is configured via environment variable (`VITE_API_URL`) -- Frontend builds independently from the backend -- Proxy configuration in `vite.config.ts` for development - ---- - -## Phase 3: forail-devops - -**Repo:** `forail-platform/forail-devops` - -### What goes in - -| Source (current monorepo) | Destination in new repo | -| ---------------------------- | ----------------------- | -| `Dockerfile`, `Dockerfile.*` | `docker/` | -| `docker-compose.yml` | Root | -| `nginx/` configuration | `nginx/` | -| `Vagrantfile` | `vagrant/` | -| Deployment scripts | `scripts/` | -| SSL/TLS configuration | `ssl/` | - -### Documentation included with devops - -- `docs/wiki/01-architecture-overview.md` -- `docs/wiki/07-docker-deployment.md` -- `docs/wiki/08-ci-cd-pipeline.md` -- `docs/wiki/10-contributing-guide.md` -- `docs/ci-pipeline-reference.md` -- `docs/startrun.md` -- `docs/RELEASE_NOTES_*.md` -- `docs/future_development_plan.md` - -### Structure - -``` -forail-devops/ -├── docker/ -│ ├── Dockerfile.backend # Multi-stage for backend -│ ├── Dockerfile.frontend # Multi-stage for frontend (nginx) -│ └── Dockerfile.assistant # Ollama + RAG (future) -├── docker-compose.yml # Production stack -├── docker-compose.dev.yml # Development stack -├── nginx/ -│ ├── nginx.conf -│ └── forail.conf -├── ssl/ -│ └── letsencrypt.sh -├── scripts/ -│ ├── backup.sh -│ ├── restore.sh -│ ├── health-check.sh -│ └── init.sh -├── vagrant/ -│ └── Vagrantfile -├── docs/ -│ └── (all deployment documentation) -├── .env.example -└── README.md -``` - -### Docker Compose (production) - -```yaml -services: - postgres: - image: postgres:15 - redis: - image: redis:7 - forail-backend: - image: ghcr.io/forail-platform/forail-backend:${VERSION} - forail-frontend: - image: ghcr.io/forail-platform/forail-frontend:${VERSION} - forail-task: - image: ghcr.io/forail-platform/forail-backend:${VERSION} # same image, different entrypoint - nginx: - # reverse proxy → frontend + backend API -``` - -### CI/CD orchestration - -``` -The forail-devops repo is the "glue" that: -1. Pulls backend and frontend image versions -2. Defines how to deploy to the server -3. Contains docker-compose for production -4. Contains backup/restore scripts -5. Contains health check and monitoring configuration -``` - ---- - -## Phase 4: forail-assistant (future) - -**Repo:** `forail-platform/forail-assistant` - -### Planned structure - -``` -forail-assistant/ -├── app/ -│ ├── main.py # FastAPI/Django app -│ ├── ollama_client.py # Ollama LLM integration -│ ├── rag/ -│ │ ├── indexer.py # ChromaDB document indexing -│ │ └── retriever.py # RAG retrieval -│ └── api/ -│ └── assistant.py # /api/v2/assistant/ endpoint -├── documents/ # Documents for RAG indexing -├── Dockerfile -├── requirements.txt -├── docker-compose.yml # Ollama + ChromaDB + Assistant -└── docs/ - └── chat_plan.md -``` - -### Integration - -- Exposes an API consumed by the frontend (`/api/v2/assistant/`) -- SSE streaming for real-time responses -- ChromaDB for vector search over documentation -- Ollama for LLM inference (local, no cloud dependency) - ---- - -## Phase 5: forail-mobile (future) - -**Repo:** `forail-platform/forail-mobile` - -### Planned structure - -``` -forail-mobile/ -├── android/ -│ ├── app/src/main/kotlin/ # Kotlin + Jetpack Compose -│ └── build.gradle.kts -├── backend/ # Go API for mobile-specific features -│ ├── cmd/server/main.go -│ ├── internal/ -│ │ ├── auth/ # JWT + biometric verification -│ │ ├── push/ # FCM push notifications -│ │ └── approval/ # Deployment approval flow -│ └── go.mod -├── docs/ -│ └── mobile_plan.md -└── .github/workflows/ # Android build + Go build -``` - ---- - -## How repositories connect (CI/CD integration) - -### Versioning - -- All repos use **CalVer**: `YYYY.MM.PATCH` (e.g., `2026.03.1`) -- Git tags trigger the release pipeline -- `forail-devops` references versions from other repos - -### Release flow - -``` -1. Developer pushes code to forail-backend or forail-frontend -2. That repo's CI: - - lint → test → build → security → publish Docker image -3. forail-devops is updated with the new version: - - Manual: update VERSION in .env or docker-compose.yml - - Automatic: webhook/trigger that updates the version -4. Deploy to server: - - git pull forail-devops - - docker compose pull - - docker compose up -d -``` - -### Connection diagram - -``` -┌──────────────┐ ┌───────────────┐ ┌──────────────┐ -│ forail-backend│ │ forail-frontend│ │forail-assistant│ -│ (Django) │ │ (React) │ │ (Ollama) │ -└──────┬───────┘ └──────┬────────┘ └──────┬───────┘ - │ publish │ publish │ publish - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────┐ -│ Harbor Registry (ghcr.io) │ -│ ghcr.io/forail-platform/forail-backend ghcr.io/forail-platform/forail-frontend forail-platform/... │ -└─────────────────────────┬───────────────────────────────┘ - │ pull - ▼ - ┌───────────────────────┐ - │ forail-devops │ - │ docker-compose.yml │ - │ nginx, ssl, scripts │ - └───────────┬───────────┘ - │ deploy - ▼ - ┌───────────────────────┐ - │ Production Server │ - └───────────────────────┘ -``` - ---- - -## Execution Order - -| Step | Action | Priority | -| ---- | ------------------------------------------------- | -------- | -| 1 | Create `forail-frontend` repo, extract React code | High | -| 2 | Create `forail-backend` repo, extract Django code | High | -| 3 | Create `forail-devops` repo, define Docker Compose | High | -| 4 | Set up CI/CD for each repo | High | -| 5 | Test end-to-end with separate images | High | -| 6 | Create `forail-assistant` repo | Medium | -| 7 | Create `forail-mobile` repo | Low | - -### Steps 1-3: Separation (estimate: 1-2 weeks) - -- Use `git filter-branch` or `git subtree split` to preserve history -- Update all references and paths -- Verify that each repo independently passes CI - -### Steps 4-5: CI/CD integration (estimate: 1 week) - -- GitLab CI for each repo -- Harbor registry publish for each repo -- `forail-devops` orchestration - -### Steps 6-7: Future components - -- Per `chat_plan.md` and `mobile_plan.md` timelines - ---- - -## Notes - -- **Monorepo remains as archive** — the current `awx` repo is kept in read-only mode as a reference -- **Documentation is split** — each repo gets its relevant documentation -- **Shared wiki** — `forail-devops` contains the architectural overview and links to all repositories -- **Docker images are the only artifact** — repos do not depend on each other directly, only via Docker images -- **Environment variables** — all inter-service configuration goes through env variables (12-factor app principle) diff --git a/docs/screenshots/.gitignore b/docs/screenshots/.gitignore deleted file mode 100644 index c2658d7..0000000 --- a/docs/screenshots/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md deleted file mode 100644 index 2e5d8fb..0000000 --- a/docs/screenshots/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# Forail Handbook Screenshot Generator - -A small Playwright tool that captures annotated screenshots of every page in -the Forail UI and saves them to `../img/handbook/`. The output is consumed by -[`HANDBOOK.md`](../HANDBOOK.md). - -This tool is **documentation tooling only** — it is intentionally outside -`forail-frontend` and `forail-backend` because it has no role in the -application's build, test, or runtime. - -## How it annotates - -Each shot is captured with two CSS injections: - -1. A red outline + glow on the targeted element (`data-forail-highlight`). -2. A numbered red badge (1, 2, 3 …) anchored to its top-left corner - (`data-forail-callout`). - -The numbers correspond to a list under the image in `HANDBOOK.md` that -explains what each highlighted element does. - -## One-time setup - -```bash -cd forail-deploy/docs/screenshots -npm install -npx playwright install chromium -``` - -## Usage - -Make sure the Forail stack is up and reachable at `FORAIL_URL` -(default `https://localhost`). Then: - -```bash -# All shots -npm run screenshots - -# A single shot id (see SHOTS object in screenshots.mjs) -node screenshots.mjs dashboard -``` - -## Configuration - -Override via environment variables: - -| Variable | Default | Purpose | -|---|---|---| -| `FORAIL_URL` | `https://localhost` | Base URL of the running Forail stack | -| `FORAIL_USER` | `admin` | Login username | -| `FORAIL_PASS` | `ForailAdmin2026!` | Login password | - -## Adding a new shot - -1. Open `screenshots.mjs`. -2. Add a new entry to the `SHOTS` object: - ```js - my_new_page: (p) => listPage(p, '/my_new_page', [ - { selector: 'main h1', n: 1 }, - { selector: 'main button.bg-primary', n: 2 }, - ]), - ``` -3. `node screenshots.mjs my_new_page` to test it. -4. Add `![My New Page](img/handbook/my_new_page.png)` plus the numbered legend - under the matching section in `HANDBOOK.md`. diff --git a/docs/screenshots/package-lock.json b/docs/screenshots/package-lock.json deleted file mode 100644 index b383c09..0000000 --- a/docs/screenshots/package-lock.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "forail-handbook-screenshots", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "forail-handbook-screenshots", - "version": "1.0.0", - "devDependencies": { - "playwright": "^1.59.0" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.59.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/docs/screenshots/package.json b/docs/screenshots/package.json deleted file mode 100644 index 38c0696..0000000 --- a/docs/screenshots/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "forail-handbook-screenshots", - "version": "1.0.0", - "private": true, - "description": "Playwright-based screenshot generator for the Forail user handbook. Not part of the application — documentation tooling only.", - "type": "module", - "scripts": { - "screenshots": "node screenshots.mjs" - }, - "devDependencies": { - "playwright": "^1.59.0" - } -} diff --git a/docs/screenshots/probe-forms.mjs b/docs/screenshots/probe-forms.mjs deleted file mode 100644 index a510ad3..0000000 --- a/docs/screenshots/probe-forms.mjs +++ /dev/null @@ -1,82 +0,0 @@ -// Probe form pages to discover all labeled inputs and their structure. -// Run: node probe-forms.mjs -import { chromium } from 'playwright' - -const BASE = 'https://localhost' -const PAGES = [ - '/inventories/new', - '/credentials/new', - '/projects/new', - '/templates/job_template/new', - '/users/new', - '/teams/new', - '/organizations/new', - '/event_rules/new', - '/outbound_webhooks/new', - '/policies/new', - '/scanners/new', - '/tenants/new', - '/service_catalog/new', - '/drift_alert_rules/new', - '/schedules/new', - '/notification_templates/new', -] - -const browser = await chromium.launch({ headless: true }) -const ctx = await browser.newContext({ - ignoreHTTPSErrors: true, - viewport: { width: 1440, height: 900 }, -}) -const page = await ctx.newPage() - -// Login -await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }) -await page.evaluate(() => localStorage.setItem('forail_password_changed_1', 'true')) -await page.locator('input').first().fill('admin') -await page.locator('input[type="password"]').first().fill('ForailAdmin2026!') -await Promise.all([ - page.waitForResponse((r) => r.url().includes('/api/login/') && r.request().method() === 'POST'), - page.click('button:has-text("Sign in")'), -]) -await page.waitForTimeout(500) - -for (const path of PAGES) { - console.log(`\n=== ${path} ===`) - try { - await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' }) - await page.waitForTimeout(400) - - // Get all labels with their associated input types - const fields = await page.evaluate(() => { - const out = [] - const labels = document.querySelectorAll('label') - labels.forEach((lbl) => { - const text = lbl.textContent.trim() - if (!text) return - const id = lbl.getAttribute('for') - let inputType = '?' - let inputTag = '?' - if (id) { - const inp = document.getElementById(id) - if (inp) { - inputTag = inp.tagName.toLowerCase() - inputType = inp.getAttribute('type') || inputTag - } - } - out.push(` ${text} → ${inputTag}/${inputType}`) - }) - // Buttons - const buttons = [] - document.querySelectorAll('main button').forEach((b) => { - const t = b.textContent.trim() - if (t) buttons.push(` [btn] ${t}`) - }) - return [...out, ...buttons] - }) - fields.forEach((f) => console.log(f)) - } catch (e) { - console.log(` ERROR: ${e.message}`) - } -} - -await browser.close() diff --git a/docs/screenshots/screenshots.mjs b/docs/screenshots/screenshots.mjs deleted file mode 100644 index f8291e4..0000000 --- a/docs/screenshots/screenshots.mjs +++ /dev/null @@ -1,489 +0,0 @@ -// Annotated screenshot generator for the Forail Platform user handbook. -// -// This is a documentation tool only. It is intentionally separate from the -// frontend application — it does not ship with the product, it is not part -// of the build, and it has no runtime dependency on forail-frontend code. -// All it needs is a running Forail stack reachable at FORAIL_URL. -// -// Run from forail-deploy/docs/screenshots/: -// npm install # one-time setup -// npx playwright install chromium # one-time setup -// npm run screenshots # take all screenshots -// node screenshots.mjs dashboard # only one shot id -// -// Output: ../img/handbook/.png -// -// Each shot may highlight elements three ways: -// { kind: 'label', text: 'Name', n: 1 } # form field by Label text -// { kind: 'role', role: 'button', name: 'Create Inventory', n: 2 } # by ARIA role -// { kind: 'css', selector: 'main h1', n: 3 } # raw CSS selector - -import { chromium } from 'playwright' -import { mkdir } from 'node:fs/promises' -import { fileURLToPath } from 'node:url' -import { dirname, resolve } from 'node:path' - -const __dirname = dirname(fileURLToPath(import.meta.url)) -const OUT_DIR = resolve(__dirname, '../img/handbook') - -const BASE = process.env.FORAIL_URL || 'https://localhost' -const USER = process.env.FORAIL_USER || 'admin' -const PASS = process.env.FORAIL_PASS || 'ForailAdmin2026!' - -const ANNOTATE_CSS = ` -[data-forail-highlight] { - outline: 4px solid #ff3b30 !important; - outline-offset: 3px !important; - box-shadow: 0 0 0 8px rgba(255, 59, 48, 0.18) !important; - border-radius: 6px !important; -} -.forail-callout-badge { - position: absolute !important; - z-index: 999999; - background: #ff3b30; - color: white; - font: 700 15px/1 system-ui, -apple-system, sans-serif; - width: 30px; - height: 30px; - border-radius: 999px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45); - pointer-events: none; - border: 2px solid white; -} -` - -/** - * Annotate elements. All matching is done inside the page (page.evaluate), - * which works even when shadcn/Radix labels are not associated via htmlFor. - * - * Each item: { kind: 'label'|'role'|'css', n: , ... } - */ -async function annotate(page, items) { - const failed = await page.evaluate((items) => { - const failures = [] - - function normText(s) { - return (s || '').trim().replace(/\s+/g, ' ').replace(/\s*\*\s*$/, '') - } - - function findByLabelText(text) { - const want = normText(text) - const labels = Array.from(document.querySelectorAll('label')) - const lbl = labels.find((l) => normText(l.textContent) === want) - if (!lbl) return null - - // (a)