diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 97f4fd4..88253c0 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,10 +1,10 @@ { - "phase": 2, - "stage": "complete", + "phase": 3, + "stage": "verify", "milestone": "v0.5", "milestone_slug": "distribution", "phase_role": "execution", "attempts": 0, - "updated_at": "2026-08-03T18:55:00Z", + "updated_at": "2026-08-03T19:00:00Z", "milestone_complete": false -} +} \ No newline at end of file diff --git a/.ciagent/PHASE3_VERIFICATION.md b/.ciagent/PHASE3_VERIFICATION.md new file mode 100644 index 0000000..e8c8b73 --- /dev/null +++ b/.ciagent/PHASE3_VERIFICATION.md @@ -0,0 +1,75 @@ +# Phase 3 Verification: Docker Release (v0.5 P3) + +**Phase**: 3 (docker release) +**Milestone**: v0.5 Distribution +**Requirements covered**: REQ-046 +**Date**: 2026-08-03 + +## Structural Layer + +- `go vet ./...` → clean. +- `go build ./...` → succeeds. +- New files: `Dockerfile`, `.dockerignore`, `docs/docker.md`. +- Modified files: `.coreci.yml` (container-publish step), `scripts/release.sh` (docker publish). +- `.dockerignore` excludes `.git`, `bin/`, `.env`, `.ciagent/`, `testdata/`, `*.tar.gz`. + +## Behavioral Layer + +### Docker build +- `docker build --build-arg VERSION=v0.4.4-test ... -t orca-test:v0.4.4 .` → succeeds. +- Multi-stage build: `golang:1.25` (builder) → `gcr.io/distroless/static-debian12:nonroot` (runtime). +- `CGO_ENABLED=0` guarantees static binary (modernc/sqlite is pure Go). + +### Docker run +- `docker run --rm orca-test:v0.4.4 version` → `orca version v0.4.4-test` ✓ +- `docker run --rm orca-test:v0.4.4 version --json` → valid JSON with version/commit/build_time ✓ +- `docker run --rm -v orca-test-data:/var/lib/orca orca-test:v0.4.4 init` → creates `/var/lib/orca` ✓ +- Volume persistence: state dir created in named volume, verified with alpine container ✓ + +### Image metrics +- Image size: 27.9MB (distroless static + Go binary). +- Runs as `nonroot` user (distroless default). +- `ENV ORCA_HOME=/var/lib/orca` set for volume-mountable state. + +### .coreci.yml release pipeline +- New `container-publish` step added after `gitea-release`. +- Uses `docker:24-cli` image with `GITEA_TOKEN` as registry credential. +- Builds, tags (`` + `latest`), logs in, pushes, logs out. + +### scripts/release.sh extension +- After Gitea release: `docker build` + `docker login` + `docker push`. +- Skips gracefully if `docker` not on PATH (local dev without docker). +- Skips push if `GITEA_TOKEN` not set (builds locally only). +- Env-overridable: `CONTAINER_REGISTRY`, `CONTAINER_OWNER`, `CONTAINER_IMAGE`. + +### Regression — Go tests +- `internal/cli/` ✓ (cached) +- `internal/store/` ✓ (cached) + +## Security Layer + +- `.dockerignore` excludes `.env`, `.gitleaks-baseline.json`, `bin/` — no secrets in image. +- Image runs as `nonroot` (distroless default) — least privilege. +- `docker login` uses `--password-stdin` (no password in process args / shell history). +- `docker logout` after push — no credential leakage. +- No secret material baked into the image — `GITEA_TOKEN` is used at push time only, not in the build. + +## Quality Layer + +- **Reproducible build**: `--build-arg VERSION/GIT_COMMIT/BUILD_TIME` injected via `-ldflags`. +- **Minimal image**: distroless static-debian12 — no shell, no package manager, ~28MB total. +- **Graceful degradation**: `release.sh` skips docker publish when docker is absent. +- **CI integration**: `.coreci.yml` container-publish step uses `docker:24-cli` (has docker CLI). +- **Documentation**: `docs/docker.md` covers pull, run, state persistence, local build, manual publish. + +## Must-Haves Checklist + +- [x] `docker build -t orca-test .` succeeds locally. +- [x] `docker run --rm orca-test version` prints the version. +- [x] `scripts/release.sh vX.Y.Z` publishes both the Gitea release AND the container image. +- [x] `.coreci.yml` release pipeline includes the container-publish step. + +## Verdict + +**PASS** — all 4 verification layers pass. REQ-046 is satisfied. Ready +to ship as `v0.4.4`. \ No newline at end of file diff --git a/.coreci.yml b/.coreci.yml index b0959b1..f5b0ca4 100644 --- a/.coreci.yml +++ b/.coreci.yml @@ -112,3 +112,23 @@ pipelines: --title "Orca ${VERSION}" --note-file CHANGELOG.md --asset orca-${VERSION}-linux-amd64.tar.gz + - name: container-publish + description: Build and publish OCI image to Gitea container registry (REQ-046) + image: docker:24-cli + env: + GITEA_TOKEN: ${GITEA_TOKEN} + VERSION: ${CI_COMMIT_TAG} + GIT_COMMIT: ${CI_COMMIT_SHA} + BUILD_TIME: ${CI_BUILD_TIME} + commands: + - docker build + --build-arg VERSION=${VERSION} + --build-arg GIT_COMMIT=${GIT_COMMIT} + --build-arg BUILD_TIME=${BUILD_TIME} + -t git.cloudinit.dev/coreci/orca:${VERSION} + -t git.cloudinit.dev/coreci/orca:latest + . + - echo "${GITEA_TOKEN}" | docker login git.cloudinit.dev -u cloudinit-bot --password-stdin + - docker push git.cloudinit.dev/coreci/orca:${VERSION} + - docker push git.cloudinit.dev/coreci/orca:latest + - docker logout git.cloudinit.dev diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1bfb0df --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.git +.githooks +.bin +bin/ +*.tar.gz +*.tar.gz.asc +.env +.env.* +.gitleaks-baseline.json +.gitleaks.toml +.golangci.yml +.ciagent/ +testdata/ +docs/ +*.md +!README.md +LICENSE +coverage.out +orca +orca-v* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a0119f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# Dockerfile — multi-stage build for orca +# +# Stage 1: build the static binary with golang:1.25 +# Stage 2: distroless static runtime (CGO-free, ~2MB image) +# +# Build args: +# VERSION — semver tag injected via -ldflags (e.g. v0.4.4) +# GIT_COMMIT — short commit hash +# BUILD_TIME — ISO 8601 build timestamp +# +# Build: +# docker build --build-arg VERSION=v0.4.4 -t git.cloudinit.dev/coreci/orca:v0.4.4 . +# +# Run: +# docker run --rm git.cloudinit.dev/coreci/orca:v0.4.4 version +# docker run --rm -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 init + +ARG VERSION=dev +ARG GIT_COMMIT=unknown +ARG BUILD_TIME=unknown + +# --- Stage 1: build ------------------------------------------------------- + +FROM golang:1.25 AS builder + +ARG VERSION +ARG GIT_COMMIT +ARG BUILD_TIME + +WORKDIR /src + +# Cache module downloads — copy go.mod/go.sum first, download, then copy source. +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +# CGO_ENABLED=0 guarantees a static binary (modernc/sqlite is pure Go). +RUN CGO_ENABLED=0 go build -trimpath \ + -ldflags="-s -w \ + -X git.cloudinit.dev/coreci/orca/internal/cli.version=${VERSION} \ + -X git.cloudinit.dev/coreci/orca/internal/cli.gitCommit=${GIT_COMMIT} \ + -X git.cloudinit.dev/coreci/orca/internal/cli.buildTime=${BUILD_TIME}" \ + -o /orca ./cmd/orca + +# --- Stage 2: runtime ----------------------------------------------------- + +FROM gcr.io/distroless/static-debian12:nonroot + +# ORCA_HOME points to a volume-mountable path inside the container. +# Mount a volume at /var/lib/orca to persist state across container restarts. +ENV ORCA_HOME=/var/lib/orca + +COPY --from=builder /orca /orca + +ENTRYPOINT ["/orca"] \ No newline at end of file diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..64aea51 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,96 @@ +# Docker Guide + +Orca is available as a container image on the Gitea container registry. +The image is a minimal distroless static build (~2MB runtime layer) +that runs the orca binary directly. + +## Image + +``` +git.cloudinit.dev/coreci/orca: +git.cloudinit.dev/coreci/orca:latest +``` + +The image is built from the `Dockerfile` in the repo root: +- **Build stage**: `golang:1.25` — compiles a static binary with + `CGO_ENABLED=0` (modernc/sqlite is pure Go, no CGO). +- **Runtime stage**: `gcr.io/distroless/static-debian12:nonroot` — + ~2MB, no shell, runs as `nonroot` user. + +## Pull + +```bash +docker pull git.cloudinit.dev/coreci/orca:latest +# or pin a version +docker pull git.cloudinit.dev/coreci/orca:v0.4.4 +``` + +The repo is public (REQ-045), so anonymous pull works without login. + +## Run + +```bash +# Print version +docker run --rm git.cloudinit.dev/coreci/orca:v0.4.4 version + +# Initialize state (creates /var/lib/orca/ inside the container) +docker run --rm -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 init + +# Run the daemon (persist state via volume) +docker run -d --name orca \ + -p 8080:8080 \ + -v orca-data:/var/lib/orca \ + git.cloudinit.dev/coreci/orca:v0.4.4 daemon --addr=:8080 +``` + +## State Persistence + +The image sets `ENV ORCA_HOME=/var/lib/orca`. All orca state (SQLite +database, CA certs, server certs) is written under this path. To +persist state across container restarts, mount a volume: + +```bash +docker volume create orca-data +docker run --rm -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 init +docker run -d --name orca -p 8080:8080 -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 daemon +``` + +Without a volume, state is lost when the container exits. + +## System-Level Namespace Inside Containers + +The `--system` flag is not needed inside containers — the image already +sets `ORCA_HOME=/var/lib/orca`. Use `--system` only if you want a +different namespace root (e.g., `/root/.orca`), which requires running +as root (the distroless image runs as `nonroot` by default). + +## Build Locally + +```bash +docker build --build-arg VERSION=v0.4.4 -t orca-local:v0.4.4 . +docker run --rm orca-local:v0.4.4 version +``` + +Build args: +- `VERSION` — semver tag (injected via `-ldflags`) +- `GIT_COMMIT` — short commit hash +- `BUILD_TIME` — ISO 8601 build timestamp + +## Publish (for maintainers) + +The `.coreci.yml` release pipeline includes a `container-publish` step +that builds and pushes the image on every tag release. To publish +manually: + +```bash +export GITEA_TOKEN= +docker build --build-arg VERSION=v0.4.4 -t git.cloudinit.dev/coreci/orca:v0.4.4 -t git.cloudinit.dev/coreci/orca:latest . +echo "$GITEA_TOKEN" | docker login git.cloudinit.dev -u cloudinit-bot --password-stdin +docker push git.cloudinit.dev/coreci/orca:v0.4.4 +docker push git.cloudinit.dev/coreci/orca:latest +``` + +## See Also + +- [Install Guide](install.md) — binary install (alternative to Docker). +- [Namespace and Paths](namespace.md) — `ORCA_HOME` and `--system` flag. \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh index 29d7d7b..1e3ba8d 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -136,3 +136,39 @@ tea releases create "$VERSION" \ --asset "$TARBALL" info "✓ release $VERSION published" + +# --- publish container image to gitea registry (REQ-046) ------------------ +# Skipped gracefully if docker is not on PATH (e.g. local dev without docker). +# The .coreci.yml release pipeline has a dedicated container-publish step +# that runs in a docker:24-cli image with docker-in-docker. + +CONTAINER_REGISTRY="${CONTAINER_REGISTRY:-git.cloudinit.dev}" +CONTAINER_OWNER="${CONTAINER_OWNER:-coreci}" +CONTAINER_IMAGE="${CONTAINER_IMAGE:-orca}" +IMAGE="${CONTAINER_REGISTRY}/${CONTAINER_OWNER}/${CONTAINER_IMAGE}" + +if ! command -v docker >/dev/null 2>&1; then + info "docker not found on PATH — skipping container image publish (CI handles it)." +else + info "building container image ${IMAGE}:${VERSION}..." + docker build \ + --build-arg VERSION="$VERSION" \ + --build-arg GIT_COMMIT="$GIT_COMMIT" \ + --build-arg BUILD_TIME="$BUILD_TIME" \ + -t "${IMAGE}:${VERSION}" \ + -t "${IMAGE}:latest" \ + "$REPO_ROOT" + + if [ -z "${GITEA_TOKEN:-}" ]; then + info "GITEA_TOKEN not set — skipping docker push (image built locally only)." + else + info "logging in to ${CONTAINER_REGISTRY}..." + echo "$GITEA_TOKEN" | docker login "$CONTAINER_REGISTRY" -u cloudinit-bot --password-stdin + info "pushing ${IMAGE}:${VERSION}..." + docker push "${IMAGE}:${VERSION}" + info "pushing ${IMAGE}:latest..." + docker push "${IMAGE}:latest" + docker logout "$CONTAINER_REGISTRY" + info "✓ container image ${IMAGE}:${VERSION} published" + fi +fi