Go module, makefile (build, test, docker, e2e targets), version embedding, initialize script, vars template, ignores and the project plan.
46 KiB
dpsapi — implementation plan
1. Project definition
Repository: git.bjphoster.com/source/dpsapi
Application: dpsapi
Purpose: a small Go HTTP service that exposes one authenticated REST endpoint, GET /api/v1/containers, and returns the currently running Docker containers obtained through /var/run/docker.sock.
The same binary also provides token-management and audit-inspection CLI subcommands. With no command-line arguments, the binary starts the HTTP server in the foreground. With token ... arguments, it performs the requested token operation and exits. With audit ... arguments, it prints the requested audit rows and exits. With the single argument help, it prints the usage text to stdout and exits.
The project follows the structure and conventions of go-template-container-webserver, including a split between main.go, HTTP/server setup, routes, Makefile-driven build/test commands, version/build metadata, and a two-stage container build. The YAML configuration layer from the template is not used; runtime configuration is entirely environment-based.
Reference repository: https://git.bjphoster.com/source/go-template-container-webserver
2. Fixed runtime configuration
Read configuration from environment variables:
| Variable | Required | Default | Meaning |
|---|---|---|---|
DB_PATH |
yes | none | SQLite database path |
HTTP_ADDR |
no | 0.0.0.0 |
HTTP listen address |
HTTP_PORT |
no | 80 |
HTTP listen port |
LOG_LEVEL |
no | INFO |
Minimum emitted log level |
Accepted LOG_LEVEL values, case-insensitive: ERROR, WARN, INFO, DEBUG, TRACE.
Configuration loading must validate the values before the server or CLI operation starts. Missing DB_PATH, invalid HTTP_PORT, invalid HTTP_ADDR, or invalid LOG_LEVEL must produce a clear error and a non-zero exit code.
No YAML configuration file, YAML dependency, or YAML configuration path is introduced.
3. Process and command behavior
Server mode
/path/to/dpsapi with no arguments:
- Load environment configuration.
- Initialize the logger.
- Open/create the SQLite database at
DB_PATH. - Enable SQLite foreign-key enforcement for the connection.
- Create the required schema if it does not already exist.
- Connect to Docker Engine through
unix:///var/run/docker.sock. - Register the HTTP route.
- Start listening on
${HTTP_ADDR}:${HTTP_PORT}in the foreground. - Handle
SIGINTandSIGTERMwith graceful HTTP shutdown and database cleanup.
A successful graceful shutdown exits with code 0. Startup failures and unrecoverable server failures exit non-zero.
CLI mode
token list and audit print their rows as a padded table: a header line, then one line per row, rendered with Go's text/tabwriter as tabwriter.NewWriter(out, 0, 0, 2, ' ', 0), each line written as its cells joined by \t and terminated by \n (no trailing tab), with a single Flush at the end. Header and rows form one block with the same number of cells on every line. The resulting format, stated independently of Go:
- For every column except the last, width = 2 + the maximum, over all printed lines (header included), of the cell length in Unicode code points (runes; not bytes, not display width).
- Each non-last cell is written followed by (width − its rune length) spaces. The last cell is written as-is, never padded. No line has trailing whitespace; every line ends with
\n. - Widths are computed per invocation over the printed lines only. With no rows only the header line is printed, each header cell padded by its own length plus 2 (
TOKEN DESCRIPTION ACTIVE CREATED_AT DELETED_AT,TIME TOKEN ORIGIN OPERATION).
/path/to/dpsapi token list [all]
Open the database, initialize the schema if necessary, and print the token rows as a padded table with the cells TOKEN, DESCRIPTION (strconv.Quoted), ACTIVE (true/false), CREATED_AT, DELETED_AT (- when NULL), ordered by creation time then token. Without an argument only the active tokens are listed (filtered in SQL with WHERE active = 1); with the argument all (exact, case-sensitive) both active and inactive/deleted tokens are listed. Tokens are always 43 characters, so the TOKEN column is 45 wide whenever at least one row is printed. Any other single argument (including ALL, an empty string, --all) is the usage error unknown token list argument; two or more arguments (even all all) are the usage error token list takes at most one argument. Both forms log cli dispatch command="token list" at DEBUG.
/path/to/dpsapi token new [description]
Generate a cryptographically secure token, store it in the database, and print the generated token to stdout. The optional description is stored verbatim. If omitted, the description is an empty string. More than one argument after new is a usage error; callers can quote a description containing spaces.
/path/to/dpsapi token delete <token>
Find the specified token, set active to false, and set deleted_at to the current UTC timestamp. The row is retained so existing audit rows continue to satisfy the foreign key. Deleting a nonexistent token or an already inactive token is an error and must not create or modify an audit row.
/path/to/dpsapi audit [all | n | n<unit>]
Open the database, initialize the schema if necessary, read the operations rows and print the selected ones as a padded table (see above) with the header cells TIME, TOKEN, ORIGIN, OPERATION, then one line per row with the time exactly as stored, the full token value exactly as stored (the same value token list shows, so an administrator can correlate the two), the origin, and the operation. OPERATION is the last column, so GET containers is never followed by padding. The command only reads; it never writes to operations or tokens.
- With no argument, select the latest 10 rows.
- With one argument consisting only of ASCII decimal digits (
n, at least1), select the latestnrows; if fewer exist, select all of them. - With one argument consisting of ASCII decimal digits followed by one lowercase unit letter (
n<unit>,nat least1), select every row whose time is at or after the current time minusnunits (inclusive boundary). Units:sseconds,mminutes,hhours,ddays of 24 hours,wweeks of 7 days. The current time comes from the application's injectable clock. - With the single argument
all(exact, case-sensitive), select every row (OperationQuery{}: no limit, no window); the current time plays no part. The parsed selection carries an explicitallflag, so its zero value never means "every row".ALL,All,--all,alls,all1,1allandallsurrounded by whitespace are the usage errorinvalid audit argument;audit all <anything>is the usage erroraudit takes at most one argument. - Leading zeros are accepted (
010is10,01his1h), consistently withHTTP_PORT. - Rows are printed in chronological order, oldest first and newest last, for every form. Ordering compares the parsed timestamps, with ties broken by insertion order (
rowid). "Latestn" means thenrows with the greatest (time, insertion order), printed ascending. - With no selected rows, only the header line is printed and the exit code is
0. - Origin and operation are written by the server from the TCP peer address and a literal, so they contain no tabs or newlines and are printed raw (a tab would split a cell of the padded table).
- A stored time that cannot be parsed is a persistence error (exit
1); the command must not print rows in an order it cannot establish. - At
DEBUGthe dispatch record carries the parsed selection:cli dispatch command=audit limit=<n>,window=<duration>orall=true. - More than one argument,
0in any form, a sign, whitespace, any other number syntax, an unknown or uppercase unit, a unit without digits, any word other than lowercaseall, an empty argument, a count that does not fit the platform integer, or a window that does not fit Go'stime.Durationis a usage error. Usage error messages are fixed strings and never echo the argument.
/path/to/dpsapi help
Print the usage text (exactly usageText, the same bytes printed to stderr after a usage error) to stdout and exit 0. Nothing else is printed (no dpsapi: line); stderr only carries DEBUG/TRACE log records when LOG_LEVEL asks for them (dispatch mode=cli, then cli dispatch command=help). The database is never opened or created. Configuration is still loaded and validated before dispatch, so a configuration error wins over help (exit 2, dpsapi: configuration error: ..., no usage text). help is exact and case-sensitive: Help, -h and --help remain unknown commands, token help remains an unknown token subcommand and audit help an invalid audit argument. help <anything> is the usage error help takes no arguments.
Any unknown command, missing command argument, or extra argument is a usage error and exits 2. The usage error messages are exactly:
dpsapi: usage error: unknown command
dpsapi: usage error: help takes no arguments
dpsapi: usage error: missing token subcommand
dpsapi: usage error: unknown token subcommand
dpsapi: usage error: token list takes at most one argument
dpsapi: usage error: unknown token list argument
dpsapi: usage error: token new takes at most one argument
dpsapi: usage error: token delete requires exactly one argument
dpsapi: usage error: audit takes at most one argument
dpsapi: usage error: invalid audit argument
Each is followed on stderr by the usage text (with a trailing newline):
usage:
dpsapi start the HTTP server
dpsapi help print this usage text
dpsapi token list [all] list the active tokens, or every token with "all"
dpsapi token new [description] create a token and print it
dpsapi token delete <token> deactivate a token
dpsapi audit print the latest 10 audit rows
dpsapi audit <n> print the latest n audit rows
dpsapi audit <n><unit> print the audit rows of the last n units (s, m, h, d, w)
dpsapi audit all print every audit row
4. SQLite schema
The database must self-initialize using idempotent DDL at startup for both server and CLI modes.
tokens
Columns, with token as the primary key:
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY NOT NULL,
description TEXT NOT NULL DEFAULT '',
active INTEGER NOT NULL CHECK (active IN (0, 1)),
created_at TEXT NOT NULL,
deleted_at TEXT NULL
);
Go code represents active as bool; SQLite stores the value as 0/1.
created_at and deleted_at use UTC RFC3339Nano text values. deleted_at is NULL while the token is active.
operations
The audit table contains exactly the requested columns:
CREATE TABLE IF NOT EXISTS operations (
token TEXT NOT NULL,
origin TEXT NOT NULL,
operation TEXT NOT NULL,
time TEXT NOT NULL,
FOREIGN KEY (token) REFERENCES tokens(token)
);
For this implementation operation is always the literal string GET containers.
Foreign-key enforcement must be enabled with PRAGMA foreign_keys = ON for the application database connection(s).
Audit rows are created only for authenticated requests to GET /api/v1/containers. The originating IP address is stored in origin. The audit insert must occur after successful token authentication and before the Docker Engine request, so an authenticated request is recorded even when the Docker operation itself fails.
CLI actions, both token management and audit inspection, are not written to operations, because the requested operation value is specifically GET containers. The audit table is readable through the audit CLI command, which never writes to operations or tokens.
5. Token generation and authentication
Generate tokens from crypto/rand; use a fixed high-entropy byte length and encode using URL-safe encoding without padding. The generated value must be suitable for use in an HTTP Authorization header.
Store the generated token value in tokens.token exactly as generated.
HTTP authentication uses:
Authorization: Bearer <token>
Authentication flow for GET /api/v1/containers:
- Require an
Authorizationheader. - Require the
Bearerscheme. - Extract the token and reject an empty value.
- Look up the token in
tokens. - Reject an unknown token.
- Reject a token whose
activevalue is false. - On success, continue with the audit insert and Docker query.
Authentication failures return HTTP 401 Unauthorized with a small JSON error body. Tokens must never be emitted in normal application logs.
The token row used for authentication remains the row referenced by the audit record through the foreign key.
6. Docker integration
Do not invoke a docker executable inside the application container. The final image is scratch, so the implementation must talk directly to Docker Engine through /var/run/docker.sock.
Use the Docker Engine Go client and configure it to use:
unix:///var/run/docker.sock
The endpoint must reproduce the semantics of docker ps:
- list containers that are currently running;
- do not include stopped containers by default;
- return an empty JSON array when no running containers exist;
- propagate Docker/client errors as an internal server error.
The HTTP handler serializes the running-container summaries as JSON and sets Content-Type: application/json.
Do not add container create/delete/start/stop endpoints or any other Docker control operation.
7. HTTP API
The sole application endpoint is:
GET /api/v1/containers
Authorization: Bearer <token>
Success:
- HTTP
200 OK. Content-Type: application/json.- JSON array containing the Docker Engine container-summary objects corresponding to running containers.
Expected failure classes:
401 Unauthorized: missing, malformed, unknown, or inactive token.405 Method Not Allowed: path is valid but the HTTP method is notGET.404 Not Found: unknown route.500 Internal Server Error: authenticated request could not be audited, the Docker socket/client failed, or response serialization failed.
Error responses use a consistent JSON object shape such as:
{
"error": "message"
}
Do not expose SQL errors, Docker socket details, stack traces, or internal implementation details in HTTP error bodies. Detailed diagnostics remain in application logs.
The client IP stored in operations.origin is derived from the request's network source address. The implementation must avoid blindly trusting arbitrary client-supplied forwarding headers.
8. Logging
Use the Go standard-library structured logging package (log/slog) with a text handler writing to the console.
Log filtering semantics:
ERROR: errors only.WARN: warnings and errors.INFO: info, warnings, and errors.DEBUG: debug, info, warnings, and errors.TRACE: trace, debug, info, warnings, and errors.
Define a custom trace level below debug.
At INFO, every authenticated or unauthenticated HTTP request reaching the application logs the exact human-readable message:
this request has been made
The log record may include structured fields such as method, path, status, duration, and origin.
At DEBUG, log meaningful lifecycle and state transitions: configuration resolution, DB initialization, route initialization, authentication result class, audit insertion result, Docker request start/end, CLI dispatch (including the parsed audit selection and command=help), audit listing result counts, and shutdown.
At TRACE, log function entry and exit for application functions and row-level database activity encountered by the execution path. For database rows, log the fields needed to identify the row and state transition, but redact the actual token secret value. Trace logging must therefore be exhaustive about the execution path without turning the logs into a copy of the credential store.
Every log-level parsing and filtering behavior must have automated tests.
9. Suggested package/file structure
Keep the repository layout close to the referenced Go template while separating concerns enough to make the requested tests straightforward:
dpsapi/
├── .dockerignore
├── .gitignore
├── .vars
├── vars.example
├── dockerfile
├── makefile
├── initialize.sh
├── version.go
├── version.sh
├── go.mod
├── go.sum
├── README.md
├── main.go
├── config.go
├── logger.go
├── server.go
├── routes.go
├── handlers.go
├── auth.go
├── token_store.go
├── audit_store.go
├── docker_client.go
├── cli.go
├── schema.sql
└── *_test.go
The exact split can remain simple; the important boundary is that configuration, logging, token persistence, audit persistence (insertion and listing), Docker access, HTTP handling, and CLI dispatch are independently testable.
schema.sql contains the authoritative table definitions. Initialization code executes the statements idempotently.
main.go performs argument dispatch and top-level lifecycle handling. It must not contain the implementation of token operations, audit listing, or HTTP handlers.
10. Main control flow
main.go
Implement a single top-level dispatch:
args == 0 -> runServer()
args[0] == help -> runHelpCommand(args[1:]) (usage text to stdout; never opens the database)
args[0] == token -> runTokenCommand(args[1:])
args[0] == audit -> runAuditCommand(args[1:])
anything else -> usage error
Configuration is loaded and validated before dispatch for every mode (including help), so configuration errors take precedence over usage errors and over help. CLI commands validate their arguments before opening the database, so usage errors never create or open it; help never opens it at all.
Return errors from lower layers instead of calling os.Exit deep inside application functions. Only the top-level process entry point decides the final exit status.
Configuration
config.go exposes a testable loader that reads all environment variables and produces a typed configuration structure.
Logging
logger.go exposes a testable logger constructor and log-level parser, including the custom TRACE level.
Token store
token_store.go provides operations equivalent to:
InitializeSchema
CreateToken
ListTokens
GetToken
DeactivateToken
Each function returns errors to the caller and is independently unit tested.
Audit store
audit_store.go provides operations equivalent to:
RecordOperation
ListOperations
RecordOperation inserts one operations row with token, origin, operation, and time. ListOperations takes a small query value (an optional limit selecting the latest rows and an optional inclusive lower time bound) and returns the matching rows in chronological order, ordered by parsed time and then insertion order. Both return errors to the caller and are independently unit tested.
Docker client
docker_client.go wraps the Docker SDK behind a narrow interface such as:
ListRunningContainers(ctx)
The HTTP layer depends on the interface, while unit tests can use a fake implementation.
HTTP handler
handlers.go performs authentication, audit logging, Docker lookup, and JSON response generation in that order.
CLI
cli.go handles exact argument parsing and formatting for token list [all], token new [description], token delete <token>, and audit [all | n | n<unit>], including the shared padded-table writer used by token list and audit. help is dispatched in main.go, next to usageText.
11. Error handling rules
Errors must be categorized into usage/configuration, authentication, persistence, Docker-client, and HTTP/server errors.
CLI errors go to stderr. Successful command output goes to stdout. Exit code checking must be explicit in tests.
The API never returns the underlying database or Docker error string directly to clients.
SQLite statements must use parameterized SQL. No token, description, origin, audit argument, or other user-controlled value is interpolated into SQL strings.
Stored timestamps are compared and ordered as parsed time values, never as text: RFC3339Nano strings with a variable-length fraction do not sort lexically. A stored timestamp that cannot be parsed is a persistence error.
Token creation and deletion must use transactions where a multi-step state change is required. In particular, deactivation must atomically set both active = 0 and deleted_at.
12. Testing strategy
Testing is a first-class part of the implementation. Every function and every meaningful branch/logic path must have automated coverage.
Unit tests with go test
Test configuration parsing:
- all variables absent except
DB_PATHwhere applicable; - default
HTTP_ADDR=0.0.0.0; - default
HTTP_PORT=80; - default
LOG_LEVEL=INFO; - every valid log level;
- invalid log levels;
- missing
DB_PATH; - empty values;
- invalid ports including zero, negative, non-numeric, and out-of-range values;
- valid edge-case ports;
- invalid/empty addresses as applicable.
Test logging:
- exact level ordering;
INFOemits the request message;- lower-priority records are filtered correctly;
TRACEemits function and row-level records;- token values are redacted in trace output;
- logger never panics on empty or unusual fields.
Test SQLite schema initialization:
- database file creation;
- repeated initialization is idempotent;
- tables exist with the requested columns;
- primary/foreign-key behavior;
activeaccepts only 0/1;deleted_atstarts NULL and becomes populated on deletion;- foreign-key enforcement is actually enabled.
Test token generation:
- generated tokens are non-empty;
- generated tokens have the expected fixed length/encoding;
- consecutive generated tokens differ;
- generated token contains only the permitted encoding alphabet.
Test token persistence:
- create with empty description;
- create with normal description;
- create with spaces/special characters;
- list with no rows;
- list with one row;
- list with many rows;
- retrieval of existing token;
- retrieval of unknown token;
- retrieval of inactive token;
- deletion of existing active token;
- deletion of unknown token;
- deletion of already inactive token;
- timestamp population and UTC formatting;
- duplicate-token collision handling if the generator is forced to collide in a test seam.
Test audit persistence:
- insert a valid operation row;
- verify exact
token,origin,operation, andtimevalues; - reject invalid foreign-key token references;
- verify UTC timestamp formatting;
- verify multiple audit rows are retained;
- list with no rows, with and without a limit or lower time bound;
- list ordering with variable-length fractional timestamps, non-UTC offsets, rows inserted out of chronological order, and equal instants (ties keep insertion order);
- list with limits of one, fewer than, equal to, and more than the number of rows;
- list with a lower time bound exactly at, just before, and just after row times, alone and combined with a limit;
- unparseable stored times, scan errors, query errors, and iteration errors;
- listing never writes to
operationsortokens; - row-level trace records redact the token.
Test HTTP authentication:
- missing
Authorization; - empty
Authorization; - wrong scheme;
- empty bearer token;
- malformed bearer value;
- unknown token;
- inactive token;
- valid active token.
Test HTTP endpoint behavior:
- valid
GETwith a fake Docker client and no containers; - valid
GETwith one container; - valid
GETwith many containers; - Docker client returning an error;
- audit store returning an error;
- JSON serialization failure via an injected test dependency where applicable;
- unsupported HTTP methods;
- unknown paths;
- response content type;
- response status codes;
- exact error-body shape;
- audit row created only after authentication succeeds;
- audit row created before Docker lookup so Docker failures remain auditable;
- origin extraction behavior;
- no token secret appears in logs.
Test CLI parsing and commands:
token listandtoken list allwith no data (exact padded header);token listwith one row (exact padded output);token listwith many rows, including multi-byte (non-ASCII, CJK, emoji) and quote-escaped descriptions, asserting the exact padded output and that widths count runes, not bytes;token listhides inactive tokens andtoken list allshows them, ordering preserved, with widths computed over the printed rows only;token list ALL,token list "",token list --all→unknown token list argument;token list all all→token list takes at most one argument;helpprints exactly the usage text on stdout with empty stderr at the default log level, exits0, and creates no database;help <x>is a usage error;Help,-h,--helpremain unknown commands; configuration errors win overhelp; theDEBUGrecords forhelpcontain no token;- the usage text matches the documented bytes exactly;
token newwithout description;token newwith a description;token newwith quoted spaces/special characters;token newwith too many arguments;token deletewith a valid active token;token deletewith an unknown token;token deletewith an already inactive token;- missing delete argument;
- unknown top-level command;
- unknown token subcommand;
- extra arguments to commands;
- correct stdout/stderr routing;
- exact exit-code behavior.
Test audit CLI:
auditwith no argument prints the latest 10 rows;audit <n>with explicit counts, including leading zeros and the largest count that fits an int;audit <n>withnlarger than the number of rows prints all rows;audit <n><unit>for each units,m,h,d,w, including leading zeros and the largest window per unit that fits atime.Duration;audit allwith zero rows (header only), one row, and more than 10 rows (every row printed, not the latest 10), independent of the injected clock (rows far in the past and in the future are printed);- window boundary inclusivity: a row exactly at now minus the window is printed, one nanosecond earlier is not, and rows later than now are printed;
- empty table, and a fresh database, print only the padded header for every form;
- exact padded output with one and many rows (variable-length times, long tokens and IPv6 origins), with widths following the selected rows;
- chronological output order for every form (
audit allincluded), including variable-length fractional timestamps, non-UTC offsets and ties broken by insertion order; - the window is computed from the injected clock;
- every usage-error class: more than one argument,
0and0<unit>, negative and signed numbers, whitespace, empty argument, unknown and uppercase units, unit without digits, other number syntaxes, case and spelling variants ofall(ALL,All,all1,1all,all,alls,--all),audit all <anything>, count overflowing int, window overflowingtime.Duration; the exact message, the usage text, exit code2, no echo of the argument, and no database created; - configuration errors take precedence over audit usage errors;
- persistence failures (unusable
DB_PATH, unparseable stored time) and stdout write failures exit1with nothing on stdout; - correct stdout/stderr routing and exact exit codes;
- read-only guarantee: the command succeeds with triggers rejecting every write to
operationsandtokens, and the table contents are unchanged; - full tokens appear on stdout while DEBUG and TRACE logs carry only
token_id; the DEBUG dispatch record carrieslimit=,window=orall=true; - rows written by the real server (authenticated requests) are listed while the server is running.
Test graceful shutdown and server error propagation as separate lifecycle cases.
Run go test ./... and a race-enabled test pass where the environment supports it.
End-to-end tests
E2E testing must be Playwright-based. Playwright is the top-level E2E orchestration framework even for non-browser interactions: use Playwright Test for test lifecycle, fixtures, retries/configuration, and its API request facilities for HTTP; use Node child-process execution from the Playwright test suite for direct compiled-binary CLI/server invocations; use curl from the Playwright tests where explicitly required to validate the real command-line HTTP client path; and use shell commands/processes from the Playwright suite only where they are necessary to create or inspect the real Docker/SQLite test environment. The objective is an exhaustive simulated-user and adversarial E2E exercise of the implemented system, not merely a happy-path smoke test.
The Playwright suite must create isolated temporary test state per test or test group, including a dedicated SQLite database, server process, log capture, and uniquely named Docker test resources. Tests must clean up all resources even after failures. Parallel tests must not accidentally share mutable state unless the scenario specifically tests concurrency or contention.
The E2E suite must exercise the compiled binary, not package internals. Unit tests remain responsible for direct function-level coverage; Playwright E2E tests are responsible for proving that the complete process behaves correctly when driven like a real user or hostile client.
Required core E2E flow:
- Build the production binary used by the E2E suite.
- Start the binary with
DB_PATH, explicitHTTP_ADDR,HTTP_PORT, and each supported log level. - Wait for actual server readiness through the HTTP interface rather than assuming startup timing.
- Create tokens only through the compiled binary:
token newwith no description, normal descriptions, whitespace, punctuation, Unicode, very long descriptions within practical test limits, and descriptions requiring shell/argument quoting. - Run
token listandtoken list allthrough the compiled binary and verify the exact padded-table output (column widths in runes, unpadded last column, no trailing whitespace), thattoken listshows only active tokens whiletoken list allshows active and inactive ones, timestamps, and descriptions. - Start known temporary Docker containers and verify
GET /api/v1/containersthrough Playwright HTTP requests and separately through realcurlinvocations. - Stop and remove those containers and verify that the API reflects the Docker Engine state.
- Exercise missing, malformed, unknown, inactive, empty, duplicated, and unusual authorization headers and verify the expected HTTP status and JSON error shape.
- Exercise wrong HTTP methods, unknown routes, trailing-slash variations, query strings, fragments where applicable, repeated headers, unusual but syntactically valid headers, and empty request bodies.
- Verify audit rows directly in SQLite after successful and unsuccessful request scenarios and confirm that only authenticated
GET /api/v1/containersrequests create the requested operation rows. - Delete tokens through the compiled binary and verify that
deleted_atis populated,activebecomes false, historical rows remain visible, and the inactive token cannot authenticate. - Exercise the no-running-containers case and a large running-container result set.
- Exercise a large token population and verify listing, deletion, authentication, and audit behavior against that population.
- Run with
ERROR,WARN,INFO,DEBUG, andTRACEand verify actual emitted log visibility and content at each level. - Send
SIGTERMand verify graceful shutdown and the actual process exit code. - Deliberately trigger startup/configuration failures and command-line usage failures (including
token list <not all>,token list all allandhelp <x>) and verify both stderr content and non-zero exit codes. - Run
audit,audit <n>,audit <n><unit>andaudit allthrough the compiled binary, including while the server is running, and verify the padded header, the exact padded rows and their chronological order against theoperationsrows in SQLite, the default of 10 rows, counts larger than the row count, the window boundary, thataudit allprints every row (more than 10) while its case variants and extra arguments are usage errors, and that the command leaves both tables unchanged. - Run
helpthrough the compiled binary and verify that stdout is exactly the usage text, stderr is empty at the default level, the exit code is0, no database is created, and a configuration error still wins over it.
The Playwright suite must include explicit simulated-user personas and test missions:
Power user: perform realistic administrative workflows at high volume and with deliberate state transitions: create many tokens, use several simultaneously, repeatedly list and delete tokens, rotate credentials, issue many authenticated requests, run multiple Docker containers with varied names/labels/images/commands, repeat operations, run concurrent API requests, restart the server against the same database, and verify state persistence and consistency after every transition. The power-user flow must also exercise boundary values, large result sets, long-lived database state, and repeated server restarts.
Stupid user: intentionally make common human mistakes and operate the CLI/API incorrectly: omit arguments, add extra arguments, use misspelled commands, copy/paste whitespace around tokens, pass 0, uppercase or unknown units, units without a number, decimals, a separate count and unit, ALL or --all to audit, use empty descriptions, pass absurdly large descriptions, use malformed Authorization headers, use the wrong HTTP method, call wrong paths, repeat deletions, delete nonexistent tokens, start the server with missing or malformed environment variables, use invalid ports/log levels, and send requests with unexpected content types or irrelevant bodies. Each scenario must verify that the program fails safely, returns the expected status/exit code, and does not corrupt database state.
Malicious user: actively attempt to bypass authentication, obtain information, corrupt state, and cause unsafe behavior using the interfaces that actually exist. Include token enumeration attempts, near-match tokens, inactive tokens, case changes, whitespace mutations, delimiter/header injection attempts, duplicate and conflicting headers, very large headers, very large request counts, path normalization tricks, URL-encoding variations, malformed HTTP syntax where the chosen client permits it, SQL-injection payloads in every user-controlled CLI/database field (including the audit argument), numeric-overflow audit counts and windows, attempts to make audit write or reveal anything other than the stored rows, shell-metacharacter descriptions, newline/control-character descriptions, Unicode confusables, NUL-containing input where the platform permits it, attempts to manipulate forwarding headers to spoof origin, concurrent token deletion versus authenticated requests, database locking/contention scenarios, Docker socket unavailability, and deliberate backend failures. Verify that secrets are not disclosed, SQL remains parameterized, internal errors are not reflected into HTTP responses, and state remains structurally valid.
Fuzzer user: generate high-cardinality and malformed input across every externally controllable value: CLI arguments (including the audit count/window argument), descriptions, tokens, authorization headers, paths, query strings, request headers, method values, environment variables, port/address/log-level values, token populations, container populations, and concurrent request patterns. Use deterministic seeds for reproducibility and record failing seeds/cases. Include empty strings, one-byte values, maximum practical values, repeated delimiters, random bytes transformed into transport-safe representations, Unicode edge cases, control characters, long repeated strings, numeric extremes, invalid UTF-8 where the underlying interface allows it, and combinations of malformed fields. The fuzzer-facing assertions are process survival, correct bounded error behavior, correct HTTP/exit status classes, no credential leakage, no database corruption, and continued ability to process subsequent valid operations.
The suite must also execute cross-persona abuse sequences in which a power-user state is attacked by malicious and fuzzer traffic, followed by normal administrative operations. This validates recovery and state integrity rather than testing each scenario only against a pristine database.
The E2E test matrix must include, at minimum, all of the following categories:
- zero tokens; one token; many tokens; zero containers; one container; many containers;
- active, inactive, unknown, and near-match credentials;
- empty, normal, Unicode, whitespace, control-character, long, and special-character descriptions;
- exact command syntax, missing arguments, extra arguments, unknown commands, and command sequencing errors;
- zero audit rows, fewer audit rows than requested, more audit rows than requested, rows exactly at the window boundary, every
auditargument form and unit (includingall), and every class of invalidauditargument (including case variants ofall); - missing, empty, malformed, repeated, oversized, and adversarial HTTP headers;
- valid and invalid environment configuration for every application environment variable;
- fresh database, existing database, repeated schema initialization, persistent database across restarts, and database contention/lock conditions;
- Docker socket available, inaccessible, temporarily failing, and returning empty/non-empty container sets;
- sequential requests, bursts, concurrent requests, repeated requests, and requests racing with token deletion;
- clean startup, clean shutdown, startup failure, runtime failure, and forced dependency failure;
- every supported HTTP status path and every CLI exit-status path;
- every log level, with assertions on which records must and must not appear;
- binary invocation from the host test environment and invocation through
docker execin the container-image E2E suite.
For every CLI action, Playwright tests must capture stdout, stderr, and exit code separately. For every HTTP action, tests must capture status, headers, body, and timing/error behavior. For every database verification, tests must inspect the resulting rows directly and verify exact values, nullability, timestamps, foreign-key relationships, and row counts.
At least one Playwright test must act as a long-running simulated operator session that executes a large sequence of valid, mistaken, adversarial, and randomized actions against the same live instance, periodically checking that all previously established invariants still hold. The session must continue after expected failures so that one bad request or bad CLI command cannot mask later corruption.
At least one Playwright test must deliberately mix concurrent authenticated requests, invalid authentication attempts, token creation/deletion, database reads, and Docker container lifecycle operations, then verify the final database and API state against the operations actually performed.
At least one Playwright test must exercise the same scenario through multiple transport/client styles where practical: Playwright HTTP requests, curl, direct binary CLI execution, and docker exec against the final image.
The E2E harness must never treat a test-process crash, timeout, unexpected signal, hung subprocess, malformed output, or leaked temporary resource as a successful negative test. All such conditions are test failures. The harness must explicitly distinguish an expected application error from a test-infrastructure error.
All E2E tests must assert exit codes explicitly. Expected successful process termination must be 0; expected usage/configuration/application command failures must be non-zero; unexpected signal termination or timeout must fail the test.
The Playwright suite must produce artifacts useful for diagnosing failures, including captured server stdout/stderr, the exact CLI arguments used where safe, HTTP request/response metadata, reproducible fuzzer seeds/cases, and a database snapshot or equivalent diagnostic state when a test fails. Secrets must be redacted from test reports and retained artifacts.
The E2E suite must be runnable non-interactively from the repository's build workflow and must fail the build on any failed assertion, unexpected process exit, timeout, resource leak, or unhandled test error.
Container-image E2E tests
Build the final image and run it with:
/var/run/docker.sockmounted;- a writable persistent directory mounted for
DB_PATH; - the requested environment variables set.
Verify:
- the default entrypoint with no arguments starts the HTTP server;
docker exec <container> /app token new ...works;docker exec <container> /app token listandtoken list allwork and print the padded table;docker exec <container> /app helpprints the usage text on stdout and exits0;docker exec <container> /app token delete ...works;docker exec <container> /app audit ...works, includingaudit all;curlfrom outside the container reaches/api/v1/containers;- the final image contains the application binary and required runtime files only;
- no Docker CLI is required inside the image;
- the application continues to function from the
scratchfinal stage.
13. Dockerfile
Use the template's two-stage pattern:
Stage 1 — builder
- Go builder image selected by build argument, following the template's
.vars/Makefile convention. - Copy the source tree.
- Build a Linux binary with
CGO_ENABLED=0. - Include version and commit metadata through linker flags, following the template's existing
APP_VERSION/COMMIT_IDpattern if retained. - Produce one executable binary at a deterministic path such as
/app.
The SQLite implementation must remain compatible with CGO_ENABLED=0 so the builder can produce a fully static binary.
Stage 2 — final
FROM scratch
Copy only the compiled binary into the final image and make it the entrypoint:
ENTRYPOINT ["/app"]
Do not install a shell, Docker CLI, package manager, or other runtime utilities in the final image.
No certificate bundle is required by the application's intended Docker-socket-only runtime behavior.
14. Makefile/build workflow
Follow the referenced repository's Makefile style and keep the workflow explicit. Include targets for at least:
default
clean
build
docker
test
test-race
test-e2e
docker-e2e
version
test must run go test ./....
Build arguments should cover the Go builder/version, target OS/architecture, repository metadata, image name, and application version in the same general style as the template.
The repository's .vars/vars.example mechanism may be retained for build/deployment variables. It must not be used as an application runtime configuration file and must not introduce YAML.
15. Docker runtime contract
The intended runtime invocation provides:
-v /var/run/docker.sock:/var/run/docker.sock
-v <persistent-dir>:/data
-e DB_PATH=/data/dpsapi.db
Optional runtime variables:
HTTP_ADDR=0.0.0.0
HTTP_PORT=80
LOG_LEVEL=INFO
The application does not attempt to create or manage the Docker socket. It assumes the socket is mounted and accessible.
The SQLite file must live on persistent storage in the deployed container configuration so token state and audit history survive container replacement.
16. README requirements
Document only the implemented behavior:
- project purpose;
- build/test commands;
- environment variables and defaults;
- database schema;
- server invocation;
- CLI invocation for
help,token list [all],token new [description],token delete <token>, andaudit [all | n | n<unit>], including the acceptedauditargument forms, units, the padded-table output format and ordering, the usage errors and the usage text; - HTTP endpoint and Bearer-token authentication;
- Docker socket mount requirement;
- example
curlinvocation; - example
docker exectoken-management and audit invocations.
Do not add sections describing future features, enhancements, TODOs, or upgrade plans.
17. Definition of done
The implementation is complete only when all of the following are true:
dpsapiwith no arguments runs the foreground HTTP server.token list [all],token new [description], andtoken delete <token>all work against the same SQLite database selected byDB_PATH;token listshows the active tokens andtoken list allevery token, both as padded tables.helpprints the usage text to stdout and exits0without opening the database.- Tokens have the requested fields and deletion is implemented as inactive state plus
deleted_at. - Every authenticated
GET /api/v1/containersrequest creates oneoperationsaudit row with the exactGET containersoperation value. audit,audit <n>,audit <n><unit>, andaudit allprint the latest 10 rows, the latestnrows, the rows of the lastnunits, or every row in chronological order, against the same SQLite database, without writing to it.- Docker container data comes from
/var/run/docker.sockusing Docker Engine API semantics equivalent todocker ps. - Default bind address is
0.0.0.0:80. LOG_LEVELdefaults toINFOand supports exactlyERROR,WARN,INFO,DEBUG, andTRACE.- INFO logging includes the exact phrase
this request has been madefor HTTP requests. - TRACE logging includes function-path and row-level diagnostics with token values redacted.
- The final Docker image is built in two stages and uses
scratchfor the final stage. - No YAML configuration is required or used.
- Unit tests cover every function and meaningful logic/error path.
- E2E tests exercise the compiled binary,
curl, real Docker Engine access, CLI behavior, error cases, large data sets, logging levels, and process exit codes. - The complete test suite passes before the repository is considered finished.