1 Commits

Author SHA1 Message Date
frx
8566b98b3b fix(issue-7): add AGENTS.md to document project architecture 2026-07-01 08:58:36 +00:00
6 changed files with 194 additions and 341 deletions

View File

@ -1,26 +0,0 @@
# Version control & tooling
.git
.gitignore
.gitattributes
# Local task runner state & binaries
.task
bin/
tags
# Local databases & logs
*.sqlite
*.log
# Editor / IDE
.zed
.idea
.vscode
# Docs & examples not needed to build the image
*.md
htmlexamples/
# Don't send the docker metadata itself (optional, keeps context lean)
Dockerfile
.dockerignore

409
AGENTS.md
View File

@ -1,250 +1,227 @@
# AGENTS.md # AGENTS.md
Guidance for AI agents (and humans) working in this repository. Guidance for AI agents (and humans) working in this repository. Read this
before making changes.
## Project Overview ## Project overview
**kurious** (`git.loyso.art/frx/kurious`) is a course/education platform that **kurious** (module `git.loyso.art/frx/kurious`) is a Go service that
aggregates and serves educational course listings. It scrapes/syncs course data aggregates, stores, and serves educational/learning course data sourced from
from an external source (sravni.ru) via a rate-limited HTTP client, stores it [sravni.ru](https://www.sravni.ru/kursy). It exposes a server-rendered web UI
locally, and exposes it through a server-rendered web UI with filtering, for browsing courses and runs a background job that periodically syncs data
pagination, and statistics. from the upstream sravni.ru API.
The project is written in Go and follows a hexagonal (ports & adapters) Go version: **1.26** (`toolchain go1.26.4`). Builds are CGO-free
architecture with a CQRS-flavored application layer. (`CGO_ENABLED=0`), using the pure-Go `modernc.org/sqlite` driver.
## Tech Stack ### Entry points (`cmd/`)
- **Language:** Go 1.26 (toolchain `go1.26.4`; see `go.mod`) | Binary | Path | Purpose |
- **HTTP routing:** `github.com/gorilla/mux` | -------------- | ----------------------- | ----------------------------------------------------------------------- |
- **HTTP client:** `github.com/go-resty/resty/v2` (sravni.ru scraper) | `kuriousweb` | `cmd/kuriweb` | HTTP web server. Serves the course browser UI + REST endpoints. |
- **Database:** SQLite via `modernc.org/sqlite` (pure-Go, CGO disabled). | `kuriousbg` | `cmd/background` | Background worker. Cron-scheduled sync of courses/orgs from sravni.ru. |
YDB was historically supported but is **no longer supported** | `sravnicli` | `cmd/dev/sravnicli` | Developer/debug CLI for inspecting the sravni.ru API and redux state. |
(`service.NewApplication` returns an error for the YDB engine).
- **DB access:** `github.com/jmoiron/sqlx` (named queries) Each binary reads a JSON config from `argv[1]` (default `config.json` for
- **Templating:** `github.com/a-h/templ` (`.templ` files compile to Go) servers, `config_cli.json` for the CLI). Note: `*.json` is gitignored.
- **Observability:** OpenTelemetry (`go.opentelemetry.io/otel`) — traces and
metrics with stdout / OTLP (HTTP & gRPC) exporters Build metadata (`version`, `commit`, `buildTime`) is injected via `-ldflags`
- **Background jobs:** `github.com/robfig/cron/v3` into the root package `kurious.go` (see `Version()`, `Commit()`, `BuildTime()`).
- **Logging:** standard `log/slog` (text or JSON, configurable)
- **Rate limiting:** `golang.org/x/time/rate`
- **Testing:** `github.com/stretchr/testify` (assert, require, mock)
- **Mocking:** `github.com/vektra/mockery/v2` (config in `.mockery.yaml`)
- **Linting:** `golangci-lint` v1.55.2
- **Build/task runner:** [Taskfile](https://taskfile.dev) (`Taskfile.yml`)
- **Build flags:** ldflags inject `version`, `commit`, `buildTime`
(see `kurious.go`)
## Architecture ## Architecture
The codebase implements a **hexagonal architecture** (ports & adapters) with a The codebase follows **Hexagonal Architecture (Ports & Adapters)** with
CQRS-style separation between commands (writes) and queries (reads). **CQRS** (command/query separation). The domain core has no knowledge of
databases, HTTP, or external services; everything is wired together in the
composition root.
``` ```
┌─────────────────────────────────────────────┐ cmd/ # application entrypoints (thin mains)
delivery │ ports/ (HTTP server, cron jobs) │ internal/kurious/
mechanisms └──────────────────────┬──────────────────────┘ domain/ # CORE: entities, repository interfaces, value/param types
│ depends on app/ # application layer
┌──────────────────────▼──────────────────────┐ command/ # write-side handlers (CQRS commands)
application app/ (command/, query/) │ query/ # read-side handlers (CQRS queries)
layer │ app.Application { Commands, Queries } │ app.go # Application struct aggregating Commands + Queries
│ decorator/ (logging decorators) │ ports/ # DRIVING adapters (what drives the domain)
└──────────────────────┬──────────────────────┘ http/ # HTTP server (gorilla/mux + templ HTML)
│ depends on background/ # cron job handlers
┌──────────────────────▼──────────────────────┐ background.go # cron scheduler wrapper
domain domain/ (entities, repository ports) │ services.go # Services aggregate
└──────────────────────┬──────────────────────┘ adapters/ # DRIVEN adapters (what the domain drives)
│ implemented by sqlite_* # SQLite repositories (course, organization, learning category)
┌──────────────────────▼──────────────────────┐ memory_mapper.go # in-memory CourseMapper implementation
adapters │ adapters/ (sqlite_*, memory_mapper, │ ydb_course_repository.go # legacy YDB adapter (deprecated, unsupported)
ydb_* legacy stub) │ mocks/ # generated mockery mocks
└─────────────────────────────────────────────┘ service/ # COMPOSITION ROOT: wires adapters into app.Application
internal/common/ # cross-cutting utilities (see below)
pkg/xdefault/ # reusable helper (WithFallback)
migrations/sqlite/ # numbered *.sql migrations + embed migrator
assets/kurious/ # embedded static assets (go:embed)
``` ```
- **`domain/`** — pure business entities (`Course`, `Organization`, ### Layer responsibilities
`LearningCategory`) and the **repository interfaces** (ports) that the
application depends on: `CourseRepository`, `OrganizationRepository`,
`LearningCategoryRepository`. No I/O or framework code lives here.
- **`app/`** — the application layer. `app.Application` aggregates a
`Commands` struct and a `Queries` struct. Handlers in `app/command/` perform
writes; handlers in `app/query/` perform reads. Each handler implements a
generic `decorator.CommandHandler[T]` or `decorator.QueryHandler[Q, U]`
interface and is wrapped with logging decorators at construction time.
- **`adapters/`** — concrete implementations of the domain repository ports.
`sqlite_course_repository.go`, `sqlite_organization_repository.go`, etc.
translate between domain types and SQL rows. `memory_mapper.go` is an
in-memory mapper translating external dictionary IDs to human-readable names.
- **`ports/`** — delivery mechanisms. `ports/http/` is the HTTP server (gorilla/mux
+ templ templates); `ports/background/` runs scheduled cron jobs (e.g.
`SyncSravniHandler`).
- **`service/`** — the **composition root**. `service.NewApplication` selects
the DB engine, constructs the SQLite connection, builds the adapters, and
wires everything into an `app.Application`. This is what binaries call.
Data flows: an HTTP request enters `ports/http`, calls a handler on - **`domain/`** — Pure business types: `Course`, `Organization`,
`service.Application` (which delegates to `app/command` or `app/query`), which `LearningCategory`, `Category`. Defines repository interfaces
calls a `domain` repository interface implemented by an `adapters/*` repository. (`CourseRepository`, `OrganizationRepository`, `LearningCategoryRepository`)
and the `CourseMapper` interface. Each repository interface carries a
`//go:generate mockery ...` directive. Params/results are plain structs.
- **`app/`** — Use-case handlers. `app.go` defines `Application{ Commands,
Queries }`. Commands (`app/command/`) mutate state; queries (`app/query/`)
read and project. Each handler is a private struct exposed via a
`NewXxxHandler` constructor that returns a decorated
`decorator.CommandHandler[T]` / `decorator.QueryHandler[Q, U]`.
- **`adapters/`** — Implementations of `domain` interfaces. SQLite is the
active engine; `sqlite_connection.go` opens the DB and runs migrations.
`memory_mapper.go` holds dictionary name/id maps and aggregate counts.
- **`ports/`** — Inbound transports. `ports/http/server.go` renders with templ
templates and normalizes errors to HTTP status codes. `ports/background.go`
wraps `robfig/cron/v3`.
- **`service/`** — `NewApplication` is the composition root: picks the DB
engine, builds repositories, and constructs all commands/queries.
## Directory Structure ### `internal/common/` utility packages
``` | Package | Role |
. | ------------ | ------------------------------------------------------------------------------------ |
├── kurious.go # Root package: version/commit/buildTime getters | `config` | JSON config structs (`Log`, `HTTP`, `Sqlite`, `YDB`, `Trace`) + `NewSLogger` factory |
├── Taskfile.yml # Build, test, lint, generate, run task definitions | `errors` | Sentinel errors (`ErrNotFound`, `ErrNotImplemented`, `ErrUnexpectedStatus` as `SimpleError`) and `ValidationError` |
├── .mockery.yaml # mockery config (with-expecter, keeptree) | `decorator` | CQRS decorators adding slog logging + OpenTelemetry tracing around every handler |
├── go.mod / go.sum | `xcontext` | Context-aware structured logging (`LogInfo/Debug/Error/...`) carrying request id + log fields |
├── cmd/ # Entry points (one package per binary) | `xlog` | Log level/format enums + slog adapters (e.g. cron logger bridge) |
│ ├── kuriweb/ # Main HTTP web server (config.go, main.go, http.go, trace.go) | `nullable` | Generic `nullable.Value[T]` for optional fields |
│ ├── background/ # Background sync process (cron-driven sravni sync) | `xslices` | Generic slice helpers (`Map`, ...) |
│ └── dev/sravnicli/ # Developer CLI for inspecting the sravni source | `generator` | ID generation (e.g. `RandomInt64ID` for request ids) |
├── internal/ | `client/sravni` | HTTP client for sravni.ru (resty), parses `__NEXT_DATA__` redux state, rate-limited |
│ ├── kurious/ # Application core (the hexagon) | `xdefault` (pkg) | `WithFallback(value, default)` helper |
│ │ ├── domain/ # Entities + repository interface ports
│ │ ├── app/
│ │ │ ├── app.go # Application{Commands, Queries} aggregate
│ │ │ ├── command/ # Write-side handlers (Create, Delete, Update...)
│ │ │ └── query/ # Read-side handlers (List, Get, Stats...)
│ │ ├── adapters/ # Repository implementations + mocks/
│ │ ├── ports/
│ │ │ ├── http/ # HTTP server, course routes
│ │ │ │ └── bootstrap/ # templ templates (.templ) + generated _templ.go
│ │ │ ├── background/ # Cron job handlers
│ │ │ ├── background.go # BackgroundProcess (cron scheduler)
│ │ │ └── services.go # Services{HTTP, Background} aggregate
│ │ └── service/ # Composition root: NewApplication wiring
│ └── common/ # Shared, reusable utilities
│ ├── client/sravni/ # sravni.ru HTTP client + mocks/
│ ├── config/ # Config structs (HTTP, Log, Sqlite, Trace, YDB, Duration)
│ ├── decorator/ # Command/Query handler interfaces + logging decorators
│ ├── errors/ # SimpleError, ValidationError, sentinel errors
│ ├── generator/ # ID generators
│ ├── nullable/ # Generic nullable Value[T]
│ ├── xcontext/ # context helpers (request id, log fields)
│ ├── xlog/ # slog + cron logger adapters
│ └── xslices/ # slice helpers (Map, Filter, ForEach, LRU)
├── pkg/
│ └── xdefault/ # WithFallback helper (public-ish pkg)
├── migrations/
│ └── sqlite/ # SQL migrations (embed.FS) + migrator.go
├── assets/kurious/static/ # Static web assets (embedded via go:embed)
└── htmlexamples/ # Standalone HTML/templ prototyping examples
```
## Build, Test, and Lint Commands ## Build & test commands
All operations are driven by **Taskfile** (`task <name>`). The toolchain is The project uses [Task](https://taskfile.dev) (`Taskfile.yml`). Install tools
installed into a local `bin/` (`GOBIN={{.USER_WORKING_DIR}}/bin`) and first with `task install_tools` (installs `golangci-lint`, `templ`, `mockery`
`CGO_ENABLED=0` is enforced. into `./bin`).
| Command | What it does | | Task | What it does |
|--------------------------|--------------------------------------------------------------------| | ----------------------- | ------------------------------------------------------------------ |
| `task install_tools` | Install `golangci-lint`, `templ`, `mockery` into `bin/` | | `task install_tools` | Install dev tools into `./bin` |
| `task generate` | Run `templ generate` (compiles `.templ``_templ.go`) | | `task generate` | Run `templ generate` for `*.templ` sources (run before build/test) |
| `task mocks` | Run `go generate ./internal/...` (regenerate mockery mocks) | | `task mocks` | `go generate ./internal/...` (regenerate mockery mocks) |
| `task check` | Run `golangci-lint run ./...` (depends on `generate`) | | `task check` | `golangci-lint run ./...` (depends on `generate`) |
| `task test` | Run `go test ./internal/...` (depends on `generate`) | | `task test` | `go test ./internal/...` (depends on `generate`) |
| `task build_web` | Build `bin/kuriousweb` (depends on `check` + `test`) | | `task build_web` | Build `kuriousweb` (depends on `check`, `test`) |
| `task build_background` | Build `bin/kuriousbg` (depends on `check` + `test`) | | `task build_background` | Build `kuriousbg` (depends on `check`, `test`) |
| `task build_dev_cli` | Build `bin/sravnicli` (depends on `check` + `test`) | | `task build_dev_cli` | Build `sravnicli` (depends on `check`, `test`) |
| `task build` | Build all three binaries | | `task build` | Build all three binaries |
| `task run` | Build then run `bin/kuriousweb` | | `task run` | Build then run `kuriousweb` |
Typical workflow before committing: `task check && task test`. Plain Go equivalents (use when Task is unavailable):
### Running the binaries
Binaries read a JSON config file path as their first argument, defaulting to
`config.json` (note: `*.json` is gitignored, so configs are local). Example
fields: `log`, `sqlite`, `http.listen_addr`, `db_engine`, `tracing`.
```bash ```bash
./bin/kuriousweb path/to/config.json go generate ./internal/... # regenerate mocks
templ generate # render .templ -> .go
golangci-lint run ./... # lint
go test ./... # run all tests
go build -o bin/kuriousweb ./cmd/kuriweb # build a binary
``` ```
## Code Conventions > Note: `templ generate` must run before `go build`/`go test` whenever a
> `*.templ` file changes — the generated `*_templ.go` files are committed-free
> and required for compilation.
### Decorators / handler pattern ## Code conventions
Command and query handlers implement generic interfaces and are wrapped with
logging decorators at construction time. When adding a new use case:
1. Define the command/query struct and a `*Handler` type alias of
`decorator.CommandHandler[T]` / `decorator.QueryHandler[Q, U]` in
`app/command/` or `app/query/`.
2. Implement a private `*Handler` struct holding its repository dependencies.
3. Expose a `New*Handler(deps..., log *slog.Logger)` constructor that returns
the decorated handler (`decorator.ApplyCommandDecorators` /
`decorator.AddQueryDecorators`).
4. Register the new handler on `app.Commands` / `app.Queries` in
`app/app.go` and wire it in `service/service.go`.
### Mock generation (mockery)
- Interfaces marked with `//go:generate mockery ...` directives (see
`domain/repository.go`, `common/client/sravni/client.go`) are mocked into
sibling `mocks/` packages (`keeptree: True`, `with-expecter: true`).
- Regenerate with `task mocks` (runs `go generate ./internal/...`).
- Mocks are committed to the repo.
### Templating (templ)
- HTML is authored as `.templ` files under
`internal/kurious/ports/http/bootstrap/`.
- `task generate` compiles them to `*_templ.go` (committed). Run it whenever a
`.templ` file changes; `task check` and `task test` both depend on it.
### Database / migrations
- SQLite migrations live in `migrations/sqlite/*.sql` and are embedded via
`go:embed`. The migrator (`migrations/sqlite/migrator.go`) applies them in
order inside a transaction.
- Repository adapters use `sqlx` named queries; domain ↔ row translation is
centralized via `AsDomain()` methods.
### Logging & observability
- Use `log/slog` with structured attributes. Request-scoped fields (e.g.
`request_id`) are propagated through `context` via `internal/common/xcontext`.
- HTTP handlers wrap requests in trace spans and record metrics
(request duration histogram). Database adapters emit spans tagged with
`db.*` attributes.
### Error handling ### Error handling
- Domain/repository errors use sentinels from `internal/common/errors` - Wrap errors with context using `fmt.Errorf("<action>: %w", err)`. Preserve
(`ErrNotFound`, `ErrNotImplemented`) and `*ValidationError` (mapped to HTTP the chain so callers can `errors.Is` / `errors.As`.
400/404 in `ports/http/server.go`). - Use the sentinels in `internal/common/errors/error.go`:
- Wrap errors with `fmt.Errorf("doing X: %w", err)` to add context while - `errors.ErrNotFound` / `errors.ErrNotImplemented` / `errors.ErrUnexpectedStatus`
preserving the underlying error for `errors.Is` / `errors.As`. (these are `SimpleError`, a string-backed error type).
- `errors.NewValidationError(field, reason)` for invalid input.
- The HTTP layer (`ports/http/server.go::handleError`) maps these to status
codes: `ValidationError` → 400, `ErrNotFound` → 404, anything else → 500
(with a generic message, never leaking internals).
- Repository `Get` methods contractually return `ErrNotFound` when a row is
missing — honor this in new adapters.
## Testing Patterns ### Logging
- **Scope:** unit tests live next to the code they test - Use `log/slog` (structured), never `fmt.Println` in library code.
(`*_test.go`). `task test` runs `go test ./internal/...`. - Prefer the `internal/common/xcontext` helpers (`LogInfo`, `LogDebug`,
- **Framework:** `github.com/stretchr/testify``require` for fatal `LogError`, `LogWithError`, `LogWithWarnError`) over raw `log.InfoContext`,
assertions, `assert` for non-fatal, `mock` for mock interactions. because they automatically attach context-scoped fields (e.g. `request_id`,
- **Mocks:** generated mockery mocks with the **expecter** API: `handler`).
```go - Create loggers via `config.NewSLogger(cfg.Log)` and tag components with
repo.EXPECT().Create(mock.Anything, expectedParams). `log.With(slog.String("component", "..."))`.
Return(domain.Course{ID: "c1"}, nil).Once() - In tests, use a discard logger: `slog.New(slog.NewTextHandler(io.Discard, nil))`.
```
Note: when the generated mock does not yet implement every interface method,
tests embed the mock and add the missing methods (see the `fullRepo` pattern
in `internal/kurious/app/command/command_test.go`).
- **Logger:** tests use a `quietLogger()` helper that discards output
(`slog.New(slog.NewTextHandler(io.Discard, nil))`).
- **Adapter tests:** `internal/kurious/adapters/sqlite_*_test.go` exercise the
real SQLite repositories against an in-memory/temp database.
- **Conventions:** table-driven where appropriate; use `context.Background()`
in tests; assert on wrapped errors with `assert.ErrorIs` and message
fragments with `assert.Contains`.
## Notes for Agents ### CQRS & handlers
- Run `task generate` before linting/testing if you touched any `.templ` file. - Every command/query handler is wrapped at construction time by
- Do not re-introduce YDB as a working engine without updating `decorator.ApplyCommandDecorators` / `decorator.AddQueryDecorators`. The
`service.NewApplication` and the migration story (currently SQLite-only). decorator adds a tracing span and start/finish log lines — **do not** add
- Generated files (`*_templ.go`, `mocks/*.go`) are committed — regenerate and your own top-level logging inside `Handle`; rely on the decorator.
commit them alongside source changes. - Handler signature pattern:
- `*.json`, `*.sqlite`, `bin/`, and `*.log` are gitignored; do not commit - command: `Handle(ctx context.Context, cmd T) error`
local configs or databases. - query: `Handle(ctx context.Context, q Q) (U, error)`
- Keep handlers thin: translate the command/query into domain params, call the
repository, and map results. Put business types in `domain/`.
### Naming & structure
- Constructors are `NewXxx`. Unexported handler struct, exported aliased type
(`decorator.CommandHandler[T]` / `decorator.QueryHandler[Q, U]`).
- Nullable fields use `nullable.Value[T]` rather than pointers.
- Exported interfaces that external code might mock get a
`//go:generate mockery --name <Interface> --output ../adapters/mocks` line
directly above the interface declaration.
### Tracing & metrics
- OpenTelemetry is wired throughout (`cmd/*/trace.go` sets up the SDK).
- DB adapters use the `adapters.db` tracer; CQRS decorators use the `api`
tracer; the HTTP layer uses the `web` tracer. Record errors on spans with
`span.RecordError(err)` and set `codes.Error` status.
## Testing
- Framework: `github.com/stretchr/testify` (`suite`, `assert`, `require`,
`mock`). Mocks are generated by mockery into `internal/kurious/adapters/mocks`
and `internal/common/client/sravni/mocks`.
- SQLite adapter tests (`internal/kurious/adapters/*_test.go`) extend
`sqliteBaseSuite`, which spins up an in-memory (`:memory:`) SQLite DB and
applies the real migrations. `TearDownTest` clears tables between cases.
- Command/query tests use mockery mocks and a discard logger.
- After changing interfaces, regenerate mocks: `go generate ./internal/...`
(or `task mocks`).
## Key dependencies
| Dependency | Role |
| --------------------------------- | ------------------------------------------------------------- |
| `github.com/gorilla/mux` | HTTP routing & middleware |
| `github.com/a-h/templ` | Type-safe HTML templates (`*.templ`) |
| `github.com/jmoiron/sqlx` | Ergonomic SQL layer over `database/sql` |
| `modernc.org/sqlite` | Pure-Go (CGO-free) SQLite driver |
| `github.com/robfig/cron/v3` | Cron scheduler for the background sync |
| `github.com/go-resty/resty/v2` | HTTP client for the sravni.ru API |
| `golang.org/x/net/html` | HTML parsing of sravni.ru `__NEXT_DATA__` redux state |
| `golang.org/x/time/rate` | Rate limiting the sravni.ru client |
| `golang.org/x/sync` | `errgroup` for goroutine lifecycle |
| `github.com/teris-io/cli` | CLI framework for `sravnicli` |
| `go.opentelemetry.io/otel` (+ exporters) | Tracing & metrics (OTLP gRPC/HTTP, stdout) |
| `github.com/stretchr/testify` | Test assertions, suites, and mocks |
| `github.com/ydb-platform/ydb-go-sdk/v3` | YDB SDK — **deprecated**, `NewApplication` returns an error for the `ydb` engine |
## Gotchas
- `*.json` is gitignored (configs contain local settings). Do not commit
config files; document config shape in `cmd/<binary>/config.go` instead.
- The `background` binary currently panics on start
(`savingOrganizationIDInternalInsteadOfExternal` guard) until the
organization-id saving semantics are fixed.
- Templ-generated files are not committed; always run `templ generate` (or
`task generate`) after pulling or before building/testing.
- The YDB engine path is dead code kept for reference — do not extend it; add
new storage work to the SQLite adapters.

View File

@ -1,55 +0,0 @@
# syntax=docker/dockerfile:1
# ---- Build stage ----
FROM golang:1.26-alpine AS builder
ENV CGO_ENABLED=0 GOOS=linux
ARG VERSION=docker
ARG COMMIT=docker
ARG BUILD_TIME=unknown
ARG PROJECT=git.loyso.art/frx/kurious
WORKDIR /src
# Cache module downloads.
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the source.
# Generated files (templ *_templ.go, mockery mocks) are committed, so no
# generation step is required here.
COPY . .
# Build the web server and the healthcheck probe.
RUN go build -trimpath \
-ldflags "-X ${PROJECT}.version=${VERSION} -X ${PROJECT}.commit=${COMMIT} -X ${PROJECT}.buildTime=${BUILD_TIME}" \
-o /out/kuriweb ./cmd/kuriweb \
&& go build -trimpath -o /out/healthcheck ./cmd/healthcheck
# Bake a default config into the image (config files are gitignored locally,
# so the image must be self-contained).
RUN echo '{"log":{"level":"info","format":"json"},"http":{"listen_addr":":8080","mount_live":false},"sqlite":{"dsn":"/tmp/kurious.sqlite","shutdown_timeout":"10s"},"db_engine":"sqlite","tracing":{"type":"stdout","show_metrics":false}}' > /out/config.json
# ---- Final stage ----
FROM gcr.io/distroless/static-debian12
LABEL org.opencontainers.image.title="kuriousweb" \
org.opencontainers.image.source="git.loyso.art/frx/kurious"
COPY --from=builder /out/kuriweb /kuriweb
COPY --from=builder /out/healthcheck /healthcheck
COPY --from=builder /out/config.json /etc/kurious/config.json
# static-debian12 ships a "nonroot" user (uid 65532).
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/kuriweb"]
CMD ["/etc/kurious/config.json"]
# Distroless static has no shell/curl, so probing is done via the tiny
# healthcheck binary built above.
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/healthcheck", "http://127.0.0.1:8080/healthz"]

View File

@ -67,16 +67,6 @@ tasks:
- task: build_background - task: build_background
- task: build_web - task: build_web
build_docker:
desc: "Build the kuriousweb Docker image locally (no push)"
cmds:
- >-
docker build
--build-arg VERSION={{.GIT_VERSION}}
--build-arg COMMIT={{.GIT_COMMIT}}
--build-arg BUILD_TIME={{.BUILD_TIME}}
-t kuriousweb .
run: run:
deps: [build] deps: [build]
cmds: cmds:

View File

@ -1,28 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
url := "http://127.0.0.1:8080/healthz"
if len(os.Args) > 1 {
url = os.Args[1]
}
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "healthcheck error: %v\n", err)
os.Exit(1)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "healthcheck failed: status %s\n", resp.Status)
os.Exit(1)
}
}

View File

@ -63,11 +63,6 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server
middlewareMetrics(), middlewareMetrics(),
) )
router.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}).Methods(http.MethodGet)
setupCoursesHTTP(srv, router, log) setupCoursesHTTP(srv, router, log)
if cfg.MountLive { if cfg.MountLive {