Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
name: CI

on: [push, pull_request]
on:
push:
pull_request:

permissions:
contents: read

jobs:
test-and-build:
runs-on: ubuntu-latest
timeout-minutes: 20

services:
postgres:
Expand All @@ -28,7 +34,11 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: 'stable'
go-version-file: go.mod
cache: true

- name: Download modules
run: go mod download

- name: Apply migrations
env:
Expand All @@ -38,13 +48,19 @@ jobs:
psql -h localhost -U testuser -d controlplane_test -f "$f"
done

- name: Run Go tests
- name: Check formatting
run: test -z "$(gofmt -l .)"

- name: Vet
run: go vet ./...

- name: Test with race detector
env:
TEST_DATABASE_URL: postgres://testuser:testpassword@localhost:5432/controlplane_test?sslmode=disable
run: go test -v -count=1 ./...
run: go test -race -count=1 ./...

- name: Build application
run: go build -v ./...
run: go build ./...

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
Expand Down
66 changes: 66 additions & 0 deletions ARCHITECTURE_STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ControlPlane Architecture Status

This document describes the repository as it exists today. It is intentionally narrower than the project name: ControlPlane is currently a **server-rendered Go application for internal knowledge and governance workflows**, not a Kubernetes-style controller or a separate distributed control-plane/data-plane system.

## Current Shape

The application is one Go process built around `net/http` and `chi`.

Request flow:

```text
HTTP request
-> chi middleware
-> session load/save
-> CSRF protection
-> authentication / role checks
-> handler
-> UserStore interface
-> PostgreSQL
-> server-rendered HTML response
```

PostgreSQL is the primary durable store. The same database also backs server-side sessions through `scs/pgxstore`.

## Implemented

- Email/password authentication with bcrypt verification.
- PostgreSQL-backed sessions and secure-cookie behavior in production mode.
- CSRF protection on the web application.
- Role checks for authenticated, reviewer, and admin routes.
- Notebook creation, revision history, tags, search, submission, and deletion flows.
- Reviewer approval/rejection workflows.
- Transactional approval/rejection persistence with approval records and audit events.
- Moderation flags and admin audit/team workflows.
- Graceful HTTP shutdown on `SIGINT`/`SIGTERM`, followed by PostgreSQL pool cleanup.
- Handler tests using fake stores and `httptest`.
- PostgreSQL integration tests using `TEST_DATABASE_URL`.
- GitHub Actions verification with migrations, formatting, vet, race-enabled tests, build, and golangci-lint.

## Important Runtime Semantics

Approval and rejection use explicit PostgreSQL transactions. Their revision state change, approval record, and audit event are committed together.

Normal handler-side audit calls outside those transaction methods are synchronous database writes. They are not an external event bus and do not provide cross-service delivery guarantees.

The application currently runs as a single process. Session state is centralized in PostgreSQL, but there is no replica coordination layer for rate limiting or background work.

## Not Implemented Yet

The following are future work and should not be inferred from the repository name or architecture diagrams:

- Prometheus metrics endpoint.
- OpenTelemetry tracing.
- Production Docker image and Kubernetes manifests.
- `/healthz` and `/readyz` probe endpoints.
- Redis-backed distributed rate limiting.
- gRPC or external service-to-service APIs.
- A separate control-plane service and data-plane service.
- Kubernetes reconciliation/controller behavior.
- Distributed consensus, leader election, or multi-region coordination.

## Verification Boundary

CI proves that the checked-in migrations apply to PostgreSQL and that the Go code passes formatting, vet, race-enabled tests, build, and lint checks.

The repository does **not** currently claim production deployment validation, load-test SLOs, multi-node failover testing, or Kubernetes operational readiness.
97 changes: 57 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ This is not a blog, not a todo app, and not a public wiki. It is an operational

## Current Status

> **Phase 5 — Production Hardening** (Complete)
> **Phase 5 — Repository Hardening** (CI-verified)

Phase 5 test files, integration tests, rate limiting, and CI pipelines are now complete. The backend is fully hardened for production use.
Phase 5 covers application-level hardening: tests, CI, lifecycle handling, and documentation/implementation parity. This is **not** a claim of production deployment readiness; metrics/tracing, container/Kubernetes deployment, health/readiness probes, and shared rate limiting remain future work. See [ARCHITECTURE_STATUS.md](./ARCHITECTURE_STATUS.md) for the as-built boundary.

| Phase | Description | Status |
|-------|-------------|--------|
Expand All @@ -93,7 +93,7 @@ Phase 5 test files, integration tests, rate limiting, and CI pipelines are now c
| Phase 2 | Forms, validation, sessions, auth, context | ✅ Complete |
| Phase 3 | Notebooks, revisions, tags, search, DB-backed pages | ✅ Complete |
| Phase 4 | Approvals, audit, moderation, governance | ✅ Complete |
| Phase 5 | Tests, CI, production hardening | ✅ Complete |
| Phase 5 | Tests, CI, graceful lifecycle, repository hardening | ✅ Complete |
| Phase 6 | Advanced: Metrics, Tracing, Docker/K8s, Shared Rate Limiting | 🚧 Planned |

---
Expand All @@ -103,7 +103,7 @@ Phase 5 test files, integration tests, rate limiting, and CI pipelines are now c
The architecture is intentionally conservative and explicit:

- `main.go` builds the long-lived dependencies once
- an `Application` struct carries shared dependencies (config, DB pool, session manager, template cache)
- an `Application` struct carries config, logger, `UserStore`, session manager, and template cache; `run()` owns the PostgreSQL pool lifecycle
- `app.routes()` builds the router and middleware graph
- handlers are methods on `app`
- request-scoped data lives in `*http.Request` and `Context`, not on global variables
Expand Down Expand Up @@ -147,7 +147,7 @@ Browser

| Concept | Scope | Storage | Example |
|---------|-------|---------|---------|
| Application | server lifetime | process memory | DB pool, session manager, template cache |
| Process dependencies | server lifetime | `run()` + `Application` | PostgreSQL pool, store, session manager, template cache |
| Session | many requests | DB/cookie-backed | `userID`, flash message |
| Context | one request | memory | current loaded user pointer |

Expand Down Expand Up @@ -207,7 +207,8 @@ ControlPlane/
│ └── audit.sql.go
├── migrations/
│ ├── 0001_initial_schema.sql # Core tables, indexes, constraints
│ └── 0002_create_audit_logs.sql # Audit log table
│ ├── 0002_create_audit_logs.sql # Audit log table
│ └── 0003_create_sessions_table.sql # scs PostgreSQL session table
├── ui/templates/ # Server-rendered HTML templates
├── docs/ # Architecture and study documentation
│ ├── Control Plane Notebook Architecture.md
Expand All @@ -222,7 +223,7 @@ ControlPlane/
├── sqlc.yaml # sqlc configuration
├── go.mod
├── go.sum
├── .env
├── ARCHITECTURE_STATUS.md
└── .gitignore
```

Expand All @@ -237,7 +238,6 @@ The `Application` struct is dependency injection in plain Go. It avoids globals,
```go
type Application struct {
config Config
conn *pgxpool.Pool
logger *slog.Logger
store data.UserStore
sessionManager *scs.SessionManager
Expand All @@ -260,7 +260,8 @@ type UserStore interface {
ListNotebookRevisions(ctx context.Context, notebookID int) ([]NotebookRevision, error)
ListNotebookTags(ctx context.Context, notebookID int) ([]Tag, error)
InsertAuditLog(ctx context.Context, params InsertAuditLogParams) error
ApproveRevisionTx(ctx context.Context, revisionID, notebookID, reviewerID int) error
ApproveRevisionTx(ctx context.Context, revisionID, notebookID, reviewerID int, note string) error
RejectRevisionTx(ctx context.Context, revisionID, reviewerID int, note string) error
ListSubmittedRevisions(ctx context.Context) ([]NotebookRevision, error)
UpdateRevisionStatus(ctx context.Context, params UpdateRevisionStatusParams) error
// ... and more
Expand Down Expand Up @@ -417,6 +418,7 @@ Without this split, drafts and approvals become fragile because you keep overwri
| `GET` | `/notebooks/{id}/edit` | `notebookEditForm` | Edit draft form |
| `POST` | `/notebooks/{id}/edit` | `notebookEditSubmit` | Submit draft edit |
| `POST` | `/notebooks/{id}/submit` | `notebookSubmitForApproval` | Submit latest draft revision for review |
| `POST` | `/notebooks/{id}/delete` | `notebookDeleteSubmit` | Delete notebook |
| `POST` | `/notebooks/{id}/flag` | `notebookFlagSubmit` | Create a moderation flag |
| `GET` | `/notebooks/search` | `notebooksSearch` | Search notebooks |

Expand All @@ -425,6 +427,7 @@ Without this split, drafts and approvals become fragile because you keep overwri
| Method | Path | Handler | Description |
|--------|------|---------|-------------|
| `GET` | `/approvals` | `approvalQueue` | Submitted revisions queue |
| `GET` | `/approvals/{id}` | `approvalReviewView` | Review one submitted revision |
| `POST` | `/approvals/approve` | `approveRevisionSubmit` | Approve and publish revision |
| `POST` | `/approvals/reject` | `rejectRevisionSubmit` | Reject revision |
| `GET` | `/moderation` | `moderationQueue` | Moderation flag queue |
Expand All @@ -434,6 +437,9 @@ Without this split, drafts and approvals become fragile because you keep overwri

| Method | Path | Handler | Description |
|--------|------|---------|-------------|
| `GET` | `/admin/teams` | `adminTeams` | Team administration page |
| `POST` | `/admin/teams` | `adminTeamOnboardSubmit` | Onboard team/membership data |
| `POST` | `/admin/notebooks/{id}/delete` | `adminNotebookDeleteSubmit` | Administrative notebook deletion |
| `GET` | `/admin/audit` | `adminAudit` | Audit event log |

---
Expand Down Expand Up @@ -465,8 +471,12 @@ serveWithSession(app, handler, request)
| `middleware_test.go` | Auth redirect, role enforcement | 3 |
| `auth_handlers_test.go` | Login form, credentials, logout | 4 |
| `notebook_handlers_test.go` | Create draft, view, validation | 4 |
| `approval_handlers_test.go` | Approve/reject transactions | 2 |
| `handlers_test.go` | Full handler integration tests | 10+ |
| `approval_handlers_test.go` | Approve/reject handler behavior | focused |
| `csrf_test.go` | CSRF enforcement and form behavior | focused |
| `ui_smoke_test.go` | Representative rendered routes | smoke |
| `main_test.go` | Graceful server lifecycle | 3 |
| `handlers_test.go` | Broader handler coverage | legacy/focused |
| `internal/data/store_test.go` | Real PostgreSQL data-path checks | integration |

### Running Tests

Expand All @@ -478,31 +488,16 @@ go test -v ./...

## CI/CD Pipeline

GitHub Actions runs on every push to `main` and on pull requests:
GitHub Actions runs on all pushes and pull requests. The repository default branch is `master`, and the workflow is not restricted to a stale `main` branch filter:

```yaml
name: CI Pipeline

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25.0'
- name: Run Go tests
run: go test -v ./...
- name: Build application
run: go build -v ./...
name: CI

on: [push, pull_request]

# PostgreSQL service + checked-in migrations
# Go version is read from go.mod
# Gates: gofmt, go vet, race-enabled tests, build, golangci-lint
```

---
Expand Down Expand Up @@ -598,10 +593,14 @@ gantt

section Phase 5
Tests & CI :done, p5, after p4, 14d
Production hardening :done, p5b, after p5, 14d
Repository hardening :done, p5b, after p5, 14d

section Phase 5.5
Domain & correctness hardening : p55a, after p5b, 14d
Concurrency & invariant tests : p55b, after p55a, 14d

section Phase 6 (Advanced)
Metrics & Tracing : p6a, after p5b, 14d
Metrics & Tracing : p6a, after p55b, 14d
K8s & Shared Limiting : p6b, after p6a, 14d
```

Expand Down Expand Up @@ -645,7 +644,7 @@ gantt
- Moderation flags table
- Admin audit page

### Phase 5 — Production Hardening ✅
### Phase 5 — Repository Hardening ✅

- [x] Test infrastructure: `fakeStore`, `newTestApplication`, `serveWithSession`
- [x] Context helper tests
Expand All @@ -657,8 +656,25 @@ gantt
- [x] GitHub Actions CI pipeline
- [x] CI pipeline refinement and edge cases
- [x] Structured error handling improvements
- [x] Request logging and observability
- [x] Graceful shutdown
- [x] Request logging via chi middleware
- [x] Graceful SIGINT/SIGTERM HTTP shutdown with PostgreSQL pool cleanup

### Phase 5.5 — Domain & Correctness Hardening 🚧 (Next)

The goal of this phase is to move correctness guarantees out of optimistic handler logic and into explicit authorization policy, transactional store operations, and database-enforced invariants.

- [ ] Centralize authorization policy for notebook ownership, team membership, visibility, editing, submission, review, moderation, and deletion.
- [ ] Make notebook creation atomic: notebook row, initial revision, tags, and audit event succeed or fail together.
- [ ] Make draft editing transactional so content, revision history, tags, and audit state cannot diverge after partial failures.
- [ ] Make submission transactional so the state transition and audit record commit together.
- [ ] Guard lifecycle transitions in the data layer (for example, only `draft -> submitted` and `submitted -> approved/rejected`).
- [ ] Reject stale, repeated, or invalid transitions by checking affected rows and returning explicit domain errors.
- [ ] Prevent double approval/rejection under concurrent reviewer actions using row locking or optimistic concurrency.
- [ ] Add database constraints/indexes for workflow invariants, including preventing conflicting decisions for one revision.
- [ ] Add PostgreSQL integration tests for unauthorized access, invalid transitions, duplicate decisions, rollback behavior, and concurrent approval attempts.
- [ ] Add race/failure-path tests around the workflow boundary rather than testing only happy-path handlers.
- [ ] Align README/demo claims with the actual authorization and transactional guarantees.
- [ ] Merge the hardening branch once these invariants are verified so the default branch reflects the portfolio-ready state.

### Phase 6 — Advanced Production Ops 🚧 (Future)

Expand Down Expand Up @@ -691,7 +707,8 @@ Project documentation lives in [`docs/`](./docs):

| Document | Description |
|----------|-------------|
| [Architecture Guide](./docs/Control%20Plane%20Notebook%20Architecture.md) | System design and component relationships |
| [As-Built Architecture Status](./ARCHITECTURE_STATUS.md) | Current implementation boundary, runtime semantics, and explicit non-goals |
| [Architecture Guide](./docs/Control%20Plane%20Notebook%20Architecture.md) | Historical/system design and component relationships |
| [Reading Map](./docs/Control%20Plane%20Notebook%20Reading%20Map.md) | Recommended study order |
| [Notebook vs Ledger API](./docs/Control%20Plane%20Notebook%20vs%20Ledger%20API.md) | Design comparison |
| [Phase 0-1 Wiring Recipe](./docs/Phase%200-1%20Wiring%20Recipe.md) | Foundation build guide |
Expand Down
Loading
Loading