From a02eef13a75a13c30f8572775c6645314a6b5eff Mon Sep 17 00:00:00 2001 From: Bryan Joshua Pedini Date: Tue, 6 Oct 2026 10:52:25 +0200 Subject: [PATCH] chore: add project scaffolding and build tooling Go module, makefile (build, test, docker, e2e targets), version embedding, initialize script, vars template, ignores and the project plan. --- .gitignore | 11 + PLAN.md | 756 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 39 +++ go.sum | 109 ++++++++ initialize.sh | 14 + makefile | 98 +++++++ vars.example | 13 + version.go | 8 + version.sh | 33 +++ 9 files changed, 1081 insertions(+) create mode 100644 .gitignore create mode 100644 PLAN.md create mode 100644 go.mod create mode 100644 go.sum create mode 100755 initialize.sh create mode 100644 makefile create mode 100644 vars.example create mode 100644 version.go create mode 100755 version.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a84366 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.env +.vars +data +/dpsapi +*.db* +coverage.out +*.cov +e2e/node_modules +e2e/test-results +e2e/playwright-report +e2e/reports diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..0f0e6c2 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,756 @@ +# 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: + +1. Load environment configuration. +2. Initialize the logger. +3. Open/create the SQLite database at `DB_PATH`. +4. Enable SQLite foreign-key enforcement for the connection. +5. Create the required schema if it does not already exist. +6. Connect to Docker Engine through `unix:///var/run/docker.sock`. +7. Register the HTTP route. +8. Start listening on `${HTTP_ADDR}:${HTTP_PORT}` in the foreground. +9. Handle `SIGINT` and `SIGTERM` with 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.Quote`d), `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 ` + +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]` + +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 least `1`), select the latest `n` rows; if fewer exist, select all of them. +- With one argument consisting of ASCII decimal digits followed by one lowercase unit letter (`n`, `n` at least `1`), select every row whose time is at or after the current time minus `n` units (inclusive boundary). Units: `s` seconds, `m` minutes, `h` hours, `d` days of 24 hours, `w` weeks 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 explicit `all` flag, so its zero value never means "every row". `ALL`, `All`, `--all`, `alls`, `all1`, `1all` and `all` surrounded by whitespace are the usage error `invalid audit argument`; `audit all ` is the usage error `audit takes at most one argument`. +- Leading zeros are accepted (`010` is `10`, `01h` is `1h`), consistently with `HTTP_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`). "Latest `n`" means the `n` rows 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 `DEBUG` the dispatch record carries the parsed selection: `cli dispatch command=audit limit=`, `window=` or `all=true`. +- More than one argument, `0` in any form, a sign, whitespace, any other number syntax, an unknown or uppercase unit, a unit without digits, any word other than lowercase `all`, an empty argument, a count that does not fit the platform integer, or a window that does not fit Go's `time.Duration` is 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 ` 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: + +```text +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): + +```text +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 deactivate a token + dpsapi audit print the latest 10 audit rows + dpsapi audit print the latest n audit rows + dpsapi audit 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: + +```sql +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: + +```sql +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: + +```http +Authorization: Bearer +``` + +Authentication flow for `GET /api/v1/containers`: + +1. Require an `Authorization` header. +2. Require the `Bearer` scheme. +3. Extract the token and reject an empty value. +4. Look up the token in `tokens`. +5. Reject an unknown token. +6. Reject a token whose `active` value is false. +7. 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: + +```text +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: + +```http +GET /api/v1/containers +Authorization: Bearer +``` + +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 not `GET`. +- `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: + +```json +{ + "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: + +```text +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: + +```text +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: + +```text +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: + +```text +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: + +```text +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: + +```text +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 `, and `audit [all | n | n]`, 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_PATH` where 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; +- `INFO` emits the request message; +- lower-priority records are filtered correctly; +- `TRACE` emits 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; +- `active` accepts only 0/1; +- `deleted_at` starts 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`, and `time` values; +- 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 `operations` or `tokens`; +- 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 `GET` with a fake Docker client and no containers; +- valid `GET` with one container; +- valid `GET` with 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 list` and `token list all` with no data (exact padded header); +- `token list` with one row (exact padded output); +- `token list` with 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 list` hides inactive tokens and `token list all` shows 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`; +- `help` prints exactly the usage text on stdout with empty stderr at the default log level, exits `0`, and creates no database; `help ` is a usage error; `Help`, `-h`, `--help` remain unknown commands; configuration errors win over `help`; the `DEBUG` records for `help` contain no token; +- the usage text matches the documented bytes exactly; +- `token new` without description; +- `token new` with a description; +- `token new` with quoted spaces/special characters; +- `token new` with too many arguments; +- `token delete` with a valid active token; +- `token delete` with an unknown token; +- `token delete` with 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: + +- `audit` with no argument prints the latest 10 rows; +- `audit ` with explicit counts, including leading zeros and the largest count that fits an int; +- `audit ` with `n` larger than the number of rows prints all rows; +- `audit ` for each unit `s`, `m`, `h`, `d`, `w`, including leading zeros and the largest window per unit that fits a `time.Duration`; +- `audit all` with 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 all` included), 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, `0` and `0`, negative and signed numbers, whitespace, empty argument, unknown and uppercase units, unit without digits, other number syntaxes, case and spelling variants of `all` (`ALL`, `All`, `all1`, `1all`, ` all`, `alls`, `--all`), `audit all `, count overflowing int, window overflowing `time.Duration`; the exact message, the usage text, exit code `2`, 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 exit `1` with nothing on stdout; +- correct stdout/stderr routing and exact exit codes; +- read-only guarantee: the command succeeds with triggers rejecting every write to `operations` and `tokens`, and the table contents are unchanged; +- full tokens appear on stdout while DEBUG and TRACE logs carry only `token_id`; the DEBUG dispatch record carries `limit=`, `window=` or `all=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: + +1. Build the production binary used by the E2E suite. +2. Start the binary with `DB_PATH`, explicit `HTTP_ADDR`, `HTTP_PORT`, and each supported log level. +3. Wait for actual server readiness through the HTTP interface rather than assuming startup timing. +4. Create tokens only through the compiled binary: `token new` with no description, normal descriptions, whitespace, punctuation, Unicode, very long descriptions within practical test limits, and descriptions requiring shell/argument quoting. +5. Run `token list` and `token list all` through the compiled binary and verify the exact padded-table output (column widths in runes, unpadded last column, no trailing whitespace), that `token list` shows only active tokens while `token list all` shows active and inactive ones, timestamps, and descriptions. +6. Start known temporary Docker containers and verify `GET /api/v1/containers` through Playwright HTTP requests and separately through real `curl` invocations. +7. Stop and remove those containers and verify that the API reflects the Docker Engine state. +8. Exercise missing, malformed, unknown, inactive, empty, duplicated, and unusual authorization headers and verify the expected HTTP status and JSON error shape. +9. 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. +10. Verify audit rows directly in SQLite after successful and unsuccessful request scenarios and confirm that only authenticated `GET /api/v1/containers` requests create the requested operation rows. +11. Delete tokens through the compiled binary and verify that `deleted_at` is populated, `active` becomes false, historical rows remain visible, and the inactive token cannot authenticate. +12. Exercise the no-running-containers case and a large running-container result set. +13. Exercise a large token population and verify listing, deletion, authentication, and audit behavior against that population. +14. Run with `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE` and verify actual emitted log visibility and content at each level. +15. Send `SIGTERM` and verify graceful shutdown and the actual process exit code. +16. Deliberately trigger startup/configuration failures and command-line usage failures (including `token list `, `token list all all` and `help `) and verify both stderr content and non-zero exit codes. +17. Run `audit`, `audit `, `audit ` and `audit all` through the compiled binary, including while the server is running, and verify the padded header, the exact padded rows and their chronological order against the `operations` rows in SQLite, the default of 10 rows, counts larger than the row count, the window boundary, that `audit all` prints every row (more than 10) while its case variants and extra arguments are usage errors, and that the command leaves both tables unchanged. +18. Run `help` through the compiled binary and verify that stdout is exactly the usage text, stderr is empty at the default level, the exit code is `0`, 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 `audit` argument form and unit (including `all`), and every class of invalid `audit` argument (including case variants of `all`); +- 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 exec` in 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.sock` mounted; +- 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 /app token new ...` works; +- `docker exec /app token list` and `token list all` work and print the padded table; +- `docker exec /app help` prints the usage text on stdout and exits `0`; +- `docker exec /app token delete ...` works; +- `docker exec /app audit ...` works, including `audit all`; +- `curl` from 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 `scratch` final 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_ID` pattern 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 + +```dockerfile +FROM scratch +``` + +Copy only the compiled binary into the final image and make it the entrypoint: + +```dockerfile +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: + +```text +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: + +```text +-v /var/run/docker.sock:/var/run/docker.sock +-v :/data +-e DB_PATH=/data/dpsapi.db +``` + +Optional runtime variables: + +```text +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 `, and `audit [all | n | n]`, including the accepted `audit` argument 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 `curl` invocation; +- example `docker exec` token-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: + +- `dpsapi` with no arguments runs the foreground HTTP server. +- `token list [all]`, `token new [description]`, and `token delete ` all work against the same SQLite database selected by `DB_PATH`; `token list` shows the active tokens and `token list all` every token, both as padded tables. +- `help` prints the usage text to stdout and exits `0` without opening the database. +- Tokens have the requested fields and deletion is implemented as inactive state plus `deleted_at`. +- Every authenticated `GET /api/v1/containers` request creates one `operations` audit row with the exact `GET containers` operation value. +- `audit`, `audit `, `audit `, and `audit all` print the latest 10 rows, the latest `n` rows, the rows of the last `n` units, or every row in chronological order, against the same SQLite database, without writing to it. +- Docker container data comes from `/var/run/docker.sock` using Docker Engine API semantics equivalent to `docker ps`. +- Default bind address is `0.0.0.0:80`. +- `LOG_LEVEL` defaults to `INFO` and supports exactly `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE`. +- INFO logging includes the exact phrase `this request has been made` for 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 `scratch` for 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. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5498506 --- /dev/null +++ b/go.mod @@ -0,0 +1,39 @@ +module git.bjphoster.com/source/dpsapi + +go 1.27.1 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/moby/moby/api v1.56.1 + github.com/moby/moby/client v0.6.1 + modernc.org/sqlite v1.60.1 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.8.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/sys v0.48.0 // indirect + modernc.org/libc v1.77.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.12.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2352f33 --- /dev/null +++ b/go.sum @@ -0,0 +1,109 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= +github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c= +github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= +github.com/moby/moby/client v0.6.1 h1:gvfKmdcWw+9MzXYP0vAUKQtBTGu1L8475K2nsmL7O98= +github.com/moby/moby/client v0.6.1/go.mod h1:XHgTFqz9NCgS/VuoxXMmEDVmVgXQ4vFEc15wsCIwb24= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg= +modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE= +modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc= +modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs= +modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= +modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw= +modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/initialize.sh b/initialize.sh new file mode 100755 index 0000000..bb84189 --- /dev/null +++ b/initialize.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +set -euo pipefail + +if [ ! -f ".vars" ]; then + cp vars.example .vars +fi + +if [ ! -f "go.mod" ]; then + module_name=$(git remote get-url origin | sed 's|^https://||') + go mod init "$module_name" +fi diff --git a/makefile b/makefile new file mode 100644 index 0000000..02a26e3 --- /dev/null +++ b/makefile @@ -0,0 +1,98 @@ +#!make +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +include .vars + +# Image tag / embedded version (override from the environment: APP_VERSION=1.2.3 make docker) +APP_VERSION ?= latest +export APP_VERSION +# Commit embedded in the binary, computed on the host ("undefined" when there are no commits) +COMMIT_ID := $(shell git rev-parse --verify --quiet --short HEAD 2>/dev/null || echo undefined) +export COMMIT_ID +# Label identifying this project's images, so cleanup never touches unrelated images +IMAGE_LABEL = dpsapi.project=$${REPO_ORG}/$${REPO_NAME} +# Local build output +BINARY = dpsapi + +.PHONY: default clean build docker dockerpush test test-race test-e2e docker-e2e version run dockerrun + +default: version + +clean: + rm -f ./$(BINARY) ./coverage.out + if [ "$$(docker images "$${CONTAINER_ORG}/$${CONTAINER_IMAGE}" --format "{{.Repository}}:{{.Tag}}")" != "" ]; then \ + docker image rm $$(docker images "$${CONTAINER_ORG}/$${CONTAINER_IMAGE}" --format "{{.Repository}}:{{.Tag}}"); \ + fi + docker image prune --force --filter "label=$(IMAGE_LABEL)" + +build: + CGO_ENABLED=0 go build \ + -trimpath \ + -ldflags="-w -s -X 'main.APP_VERSION=$${APP_VERSION}' -X 'main.COMMIT_ID=$${COMMIT_ID}'" \ + -o ./$(BINARY) . + +docker: + docker build \ + --build-arg GO_BUILDER=$${GO_BUILDER} \ + --build-arg GO_VERSION=$${GO_VERSION} \ + --build-arg GO_OS=$${GO_OS} \ + --build-arg GO_ARCH=$${GO_ARCH} \ + --build-arg GIT_HOST=$${GIT_HOST} \ + --build-arg REPO_ORG=$${REPO_ORG} \ + --build-arg REPO_NAME=$${REPO_NAME} \ + --build-arg APP_VERSION=$${APP_VERSION} \ + --build-arg COMMIT_ID=$${COMMIT_ID} \ + --label "$(IMAGE_LABEL)" \ + -t $${CONTAINER_ORG}/$${CONTAINER_IMAGE}:$${APP_VERSION} . +# Remove only this project's images left dangling by the re-tag (prune without --all only removes dangling images) + docker image prune --force --filter "label=$(IMAGE_LABEL)" + +dockerpush: + docker push \ + $${CONTAINER_ORG}/$${CONTAINER_IMAGE}:$${APP_VERSION} + +test: + go test ./... + +test-race: + CGO_ENABLED=1 go test -race ./... + +# The binary is built by the build prerequisite, so the suite's own build step is skipped +test-e2e: build + cd e2e && npm ci && DPSAPI_E2E_SKIP_BUILD=1 npx playwright test --project=host + +docker-e2e: docker + cd e2e && npm ci && \ + DPSAPI_IMAGE=$${CONTAINER_ORG}/$${CONTAINER_IMAGE}:$${APP_VERSION} npx playwright test --project=image + +version: + bash -c "./version.sh" + +run: + mkdir -p ./data + docker run \ + --rm \ + --tty \ + --interactive \ + --publish $${CONTAINER_IP}:$${CONTAINER_PORT}:80 \ + --workdir /go/src/$${GIT_HOST}/$${REPO_ORG}/$${REPO_NAME} \ + --volume $(shell pwd):/go/src/$${GIT_HOST}/$${REPO_ORG}/$${REPO_NAME} \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --env DB_PATH=/go/src/$${GIT_HOST}/$${REPO_ORG}/$${REPO_NAME}/data/dpsapi.db \ + --env LOG_LEVEL=$${LOG_LEVEL:-INFO} \ + $${GO_BUILDER}:$${GO_VERSION} \ + go run . + +dockerrun: + mkdir -p ./data + docker run \ + --rm \ + --tty \ + --interactive \ + --publish $${CONTAINER_IP}:$${CONTAINER_PORT}:80 \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume $(shell pwd)/data:/data \ + --env DB_PATH=/data/dpsapi.db \ + --env LOG_LEVEL=$${LOG_LEVEL:-INFO} \ + $${CONTAINER_ORG}/$${CONTAINER_IMAGE}:$${APP_VERSION} diff --git a/vars.example b/vars.example new file mode 100644 index 0000000..d0f2869 --- /dev/null +++ b/vars.example @@ -0,0 +1,13 @@ +#/usr/bin/env bash + +export GIT_HOST=git.bjphoster.com +export GO_BUILDER=golang +export GO_VERSION=1.27.1-alpine +export GO_OS=linux +export GO_ARCH=amd64 +export REPO_ORG=source +export REPO_NAME=dpsapi +export CONTAINER_ORG=git.bjphoster.com/source +export CONTAINER_IMAGE=dpsapi +export CONTAINER_IP=127.0.0.1 +export CONTAINER_PORT=3000 diff --git a/version.go b/version.go new file mode 100644 index 0000000..2be72f5 --- /dev/null +++ b/version.go @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 Bryan Joshua Pedini + +package main + +// Build metadata, overridden at link time through -ldflags -X +var APP_VERSION string = "latest" +var COMMIT_ID string = "undefined" diff --git a/version.sh b/version.sh new file mode 100755 index 0000000..ce70db3 --- /dev/null +++ b/version.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +if [ -z "${APP_VERSION}" ]; then + read -p "Version [latest]: " VERSIONINPUT + if [ -z "${VERSIONINPUT}" ]; then + APP_VERSION="latest" + else + APP_VERSION=${VERSIONINPUT} + fi +fi + +# Get docker push option from user +if [ -z "${DOCKERPUSH}" ]; then + read -p "Docker push? [n]: " DOCKERPUSH + if [ -z "${DOCKERPUSH}" ]; then + DOCKERPUSH=n + fi +fi + +# Create version tag (if provided) +if [ ! -z "${VERSIONINPUT}" ]; then + git tag "${APP_VERSION}" +fi + +# Build the app +export APP_VERSION +make docker +# If wanted, push the docker image +if [ "${DOCKERPUSH}" = "y" ]; then + make dockerpush +fi