Adds a StepRunUpdater step that starts app-dono-updater after the app container comes up: a poll loop (docker pull, compare image IDs, recreate on change) baked into the official docker:cli image via `sh -c`, reusing the same docker run argv as the initial container start so the two can't drift. Not built on Watchtower: containrrr/watchtower was archived upstream in Dec 2025 with no maintained successor recommended for production use, so this avoids taking on that dependency.
174 lines
9.2 KiB
Markdown
174 lines
9.2 KiB
Markdown
# CLAUDE.md
|
||
|
||
Guidance for Claude Code when working in this repository.
|
||
|
||
> User-facing docs are in Portuguese (BR): [README.md](README.md) and [docs/](docs/).
|
||
> This file is in English and is the technical source of truth for how the code works.
|
||
|
||
## What this is
|
||
|
||
A terminal UI (TUI) **installer** for the "App do Dono" **client middleware**. The
|
||
middleware runs on a tenant's infrastructure and brokers communication between the
|
||
tenant's servers and davinTI's **central server**.
|
||
|
||
The installer is a guided wizard that: checks Docker → logs into the private registry
|
||
→ pulls images → (optionally) sets up a WireGuard tunnel (`vproxy`) when there is no
|
||
public IP → collects configuration via terminal forms → generates config files
|
||
(`config.toml`, `envs`) → runs the Docker containers.
|
||
|
||
It is **not** the middleware itself — it only provisions and launches it via `docker`.
|
||
|
||
## Tech stack
|
||
|
||
- **Go 1.25**
|
||
- **Bubble Tea v2** (`charm.land/bubbletea/v2`) — Elm-architecture TUI runtime.
|
||
- **Bubbles v2** — `textinput`, `spinner` components.
|
||
- **Lip Gloss v2** — styling.
|
||
- **BurntSushi/toml** — read existing `config.toml` for form defaults.
|
||
- Module path: `git.davinti.com.br/davinTI/app-dono/tui`.
|
||
|
||
## Layout
|
||
|
||
```
|
||
cmd/main.go Entry point; starts the Bubble Tea program.
|
||
internal/tui/
|
||
model.go Model struct, ConfigValues, loadConfig (defaults), InitialModel, Init.
|
||
steps.go step enum — the ordered wizard stages.
|
||
update.go Update: global keys + per-step dispatch (handlers).
|
||
view.go View: header + per-step body + help footer (AltScreen).
|
||
form.go Generic FormStep/FormField component (text/password/number/select).
|
||
cmds.go Msg types and async tea.Cmd wrappers around docker/file ops.
|
||
docker.go os/exec wrappers around the `docker` binary.
|
||
config.go Generates & writes config.toml and envs (+ numeric validation).
|
||
styles.go Lip Gloss palette/styles.
|
||
Makefile Multi-arch build + S3 publish.
|
||
```
|
||
|
||
## Architecture (Model–Update–View)
|
||
|
||
- **Model** ([model.go](internal/tui/model.go)) holds all state: `currentStep`,
|
||
progress/error flags, one `FormStep` per form, and `configValues` (`ConfigValues`)
|
||
which accumulates each form's `map[string]string` output by section.
|
||
- **Update** ([update.go](internal/tui/update.go)) handles global messages
|
||
(`Ctrl+C`, window size, spinner tick) then dispatches on `currentStep` to per-step
|
||
handlers. Step transitions happen when a form returns `done == true` or when an
|
||
async completion message arrives.
|
||
- **View** ([view.go](internal/tui/view.go)) renders per-step.
|
||
- **Commands** ([cmds.go](internal/tui/cmds.go)) wrap every blocking op (docker calls,
|
||
file writes) as a `tea.Cmd` returning a `Msg`: `DockerCheckedMsg`,
|
||
`ImageDownloadFinishedMsg`, `ConfigFileMsg`, `DockerRunMsg`, `TickMsg`.
|
||
|
||
## Wizard flow
|
||
|
||
`steps.go` defines the order. See [docs/fluxo.md](docs/fluxo.md) for full detail.
|
||
|
||
1. `StepCheckDocker` — `exec.LookPath("docker")`. Missing → `StepDockerInstall`
|
||
(instructs manual install and exits; **never auto-installs Docker**).
|
||
2. `StepDockerLogin` → `StepDownloadImage` — `docker login` + `pull` of the app image.
|
||
These registry credentials are **reused** later for the vproxy image.
|
||
3. `StepIPQuestion` — "public IP available?"
|
||
- **Yes** → jump to `StepAppConfig`.
|
||
- **No** → vproxy block: `StepWireguardConfig` → `StepGenerateWireguardFile`
|
||
(writes `envs`) → `StepDownloadWireguard` → `StepRunWireguard`.
|
||
4. Config forms: `StepAppConfig` → `StepServerConfig` → `StepDatabaseConfig` →
|
||
`StepCertConfig`.
|
||
5. `StepReview` — shows all collected config; Enter confirms.
|
||
6. `StepGenerateFile` — writes `config.toml` (validates numeric fields first).
|
||
7. `StepRunDocker` — runs `app-dono-cliente` container.
|
||
8. `StepRunUpdater` — runs `app-dono-updater`, which auto-updates `app-dono-cliente`
|
||
whenever a new image is pushed to `:latest`.
|
||
9. `StepDone`.
|
||
|
||
### Navigation & error handling
|
||
|
||
- **Back navigation:** `Esc` returns to the previous *input* step. The Model keeps a
|
||
`history []step` stack; `advance(next)` pushes the current input step, `goBack()`
|
||
pops. `isInputStep` ([steps.go](internal/tui/steps.go)) gates which steps participate
|
||
— action/wait steps (downloads, file gen, container runs) are excluded so back never
|
||
re-enters a side-effecting step. `Esc` is handled globally in `Update`; forms don't
|
||
consume it. Form values survive going back because the `FormStep`s live on the Model.
|
||
- **Retry vs. fix:** transient failures (image pull, container run) offer `r` to re-run
|
||
just that command (`q`/other quits). Validation failures (file generation) route back
|
||
to the relevant form to correct the value instead of quitting.
|
||
|
||
## Key constants
|
||
|
||
In [update.go](internal/tui/update.go):
|
||
|
||
- `imageName = hub.davinti.com.br:443/app-dono/app-cliente:latest`
|
||
- `wireguardImageName = hub.davinti.com.br:443/davinti-vproxy:latest`
|
||
- `updaterImageName = docker:cli`
|
||
- `configPath = config.toml`, `wireguardConfigPath = envs`
|
||
|
||
In [docker.go](internal/tui/docker.go): `networkName = app-dono_app` (all containers
|
||
join this network; created on demand).
|
||
|
||
## Generated files
|
||
|
||
- **`config.toml`** — app config, bind-mounted at `/app/config.toml`. If present at
|
||
startup it seeds form defaults via `loadConfig`. Generated by `GenerateConfigTOML`.
|
||
- **`envs`** — vproxy/WireGuard env vars, passed via `--env-file`. Generated only when
|
||
there is no public IP. By `GenerateWireguardConfig`.
|
||
|
||
Both are gitignored.
|
||
|
||
## Containers
|
||
|
||
| Container | Image | Notes |
|
||
| ------------------ | -------------------- | -------------------------------------------------------- |
|
||
| `app-dono-cliente` | app-cliente | `<host port>:8080`, mounts config.toml + cert dir, `--restart unless-stopped`, runs as host uid:gid. |
|
||
| `vproxy` | davinti-vproxy | `--cap-add=NET_ADMIN`, `/dev/net/tun`, `--env-file envs`, only without public IP. |
|
||
| `app-dono-updater` | docker:cli | Not a third-party updater — a poll loop (`sh -c`) baked into the official `docker:cli` image, since Watchtower was archived upstream in Dec 2025 with no maintained successor recommended for production. Every `updaterPollIntervalSeconds` (300s) it `docker pull`s `app-dono-cliente`'s image, compares image IDs, and if changed, stops/removes/recreates the container using the exact same `docker run` argv as the original start (`appClienteRunArgs` in `docker.go`, shared by both call sites so they can't drift). Mounts `/var/run/docker.sock` and the host's `~/.docker/config.json` (written by the earlier `docker login`) for private-registry auth. Not on `app-dono_app` — only talks to the Docker daemon. |
|
||
|
||
`seccomp=unconfined` is added to either container when the "Modo Compatibilidade"
|
||
(`seccomp_unconfined`) select is `"Sim"` — for old machines.
|
||
|
||
## Gotchas (read before editing)
|
||
|
||
- **Port:** `config.toml` always writes `port = 8080` (hardcoded in
|
||
`GenerateConfigTOML`). The form's "Porta" value only sets the **host-side** port in
|
||
the `docker run` mapping (`<port>:8080`); inside the container the app always listens
|
||
on 8080.
|
||
- **Network name** is the `networkName` constant (`app-dono_app`) in
|
||
[docker.go](internal/tui/docker.go); use it rather than re-typing the literal.
|
||
- **Registry credentials** are collected once (login form) and reused for the vproxy
|
||
pull — no second prompt.
|
||
- Containers run detached and are verified with `docker inspect` for `running`; on
|
||
failure the last 20 log lines are surfaced.
|
||
|
||
## Build & run
|
||
|
||
```bash
|
||
go run ./cmd # run from source
|
||
go build -o installer ./cmd # local binary
|
||
|
||
make build # multi-arch (linux/darwin/windows, amd64/arm64) → ./dist
|
||
make build VERSION=2.0.0
|
||
make clean
|
||
make push S3_BUCKET=<bucket> VERSION=<v> # sync dist/ to S3 (version + latest)
|
||
make release S3_BUCKET=<bucket> VERSION=<v> # per-binary copy with OS/arch naming
|
||
```
|
||
|
||
**Versioning:** `tui.Version` ([internal/tui/version.go](internal/tui/version.go))
|
||
defaults to `"dev"` and is stamped at build time via `VERSION_LDFLAG`
|
||
(`-X .../internal/tui.Version=$(VERSION)`) in the Makefile. `installer --version`
|
||
(or `-v`) prints it and exits before the TUI starts; it is also shown in the TUI
|
||
header. A plain `go build` leaves it as `"dev"`. The Makefile build rules have no
|
||
source deps, so `make build` will not rebuild existing `dist/` artifacts — run
|
||
`make clean` first when re-stamping a new VERSION.
|
||
|
||
Tests (`go test ./...`) cover the pure logic in
|
||
[config_test.go](internal/tui/config_test.go) (config.toml/envs generation + numeric
|
||
validation) and [form_test.go](internal/tui/form_test.go) (`FormStep.Values()`). The
|
||
docker wrappers, Update and View are not unit-tested — verify those by running
|
||
`go build ./...` and exercising the TUI manually. Fully running the installer requires
|
||
a working Docker daemon and registry access.
|
||
|
||
## Conventions
|
||
|
||
- User-facing strings in the TUI are **Portuguese (BR)**; keep new ones consistent.
|
||
- Keep code comments and identifiers matching the surrounding style (mixed
|
||
English identifiers, Portuguese user messages).
|
||
- When adding a wizard step: add it to `steps.go`, a handler in `update.go`, a render
|
||
branch in `view.go`, and any new `Msg`/`Cmd` in `cmds.go`.
|