openova/products/catalyst/bootstrap/api/Containerfile
e3mrah da1d3d1ffa
fix(build): unblock Build & Deploy Catalyst — Containerfile + test typing (#1172)
* fix(build): unblock Build & Deploy Catalyst — Containerfile + test typing

The Build & Deploy Catalyst workflow has been failing on every PR since
EPIC-2 Slice I (#1152) merged. Two real bugs caught after the founder
flagged that no images had been built or deployed:

1. catalyst-api Containerfile: the replace directive added by slice I
   (`replace github.com/openova-io/openova/core/controllers => ../../../../core/controllers`)
   resolves to /core/controllers when WORKDIR=/app. The Containerfile only
   copied products/catalyst/bootstrap/api/go.{mod,sum}, not the controllers
   tree, so `go mod download` failed with "no such file or directory" on
   /core/controllers/go.mod. Fix: COPY the controllers tree BEFORE go mod.

2. SessionsPage.test.tsx (slice X2+E #1169): vi.fn(async () => SEED) infers
   parameter tuple as `[]`, so `lastCall[1]` was a TS2493 type error
   ("Tuple type '[]' of length '0' has no element at index '1'"). Cast
   lastCall to the actual listSessions signature.

Per canon §7 + the founder's "you are the merger" rule, this is the kind
of CI-pipeline regression that MUST be caught BEFORE claiming slice
completion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deploy: update catalyst images to 7235431

---------

Co-authored-by: hatiyildiz <hati.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-09 12:28:59 +04:00

138 lines
7.3 KiB
Docker

# catalyst-api — Catalyst-Zero's provisioner backend.
#
# This image needs helm + kubectl + tofu on PATH because:
# - internal/bootstrap execs helm + kubectl when installing the
# 11-component bootstrap kit into a freshly-provisioned Sovereign;
# - internal/provisioner execs `tofu init / plan / apply` against the
# bundled infra/hetzner/ module — the catalyst-api Pod IS the OpenTofu
# runner, so the CLI must be in the image (no network fetch at
# provision time).
# We use Alpine + the static binaries so the runtime stays small while
# still having all three tools available.
#
# Build context: this Containerfile expects the public openova repo root as
# its build context (i.e. `docker build -f products/catalyst/bootstrap/api/
# Containerfile .` run from the repo root). That is required so the runtime
# image can bundle the canonical OpenTofu module under infra/hetzner/ — the
# catalyst-api Pod is itself the OpenTofu runner, so the .tf sources have to
# be present inside the image. provisioner.New() reads
# CATALYST_TOFU_MODULE_PATH (default /infra/hetzner) and stages those files
# into a per-deployment workdir on every Launch.
# ---------- Stage 1: build the Go binaries ----------
FROM docker.io/library/golang:1.26-alpine AS build
# core/controllers/ is required by the replace directive in
# products/catalyst/bootstrap/api/go.mod (added by EPIC-2 slice I #1152
# so the catalyst-api preview handler renders byte-identical manifests
# to application-controller). The shared-module path resolves to
# /core/controllers when WORKDIR=/app, so we COPY the controllers tree
# BEFORE `go mod download`.
COPY core/controllers/ /core/controllers/
WORKDIR /app
COPY products/catalyst/bootstrap/api/go.mod products/catalyst/bootstrap/api/go.sum ./
RUN go mod download
COPY products/catalyst/bootstrap/api/ ./
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /catalyst-api ./cmd/api
# catalyst-dns helper — invoked by the OpenTofu module's null_resource.dns_pool
# via local-exec at Phase-0 apply time. Lives at /usr/local/bin/catalyst-dns
# in the runtime image so the OpenTofu run (which executes inside this same
# container — the catalyst-api Pod is also the OpenTofu runner) can shell out
# to it. See infra/hetzner/main.tf comments around null_resource.dns_pool.
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /catalyst-dns ./cmd/catalyst-dns
# ---------- Stage 2: download + verify the OpenTofu CLI ----------
# The catalyst-api Pod IS the OpenTofu runner — provisioner.runTofu() execs
# the `tofu` binary directly (see internal/provisioner/provisioner.go). The
# binary therefore MUST be present in the runtime image; without it every
# Launch fails with `exec: "tofu": executable file not found in $PATH`.
#
# Pinned to a specific version (no `latest`) for reproducible builds, with
# a SHA256 checksum from the canonical release-checksums file
# (https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/
# tofu_${TOFU_VERSION}_SHA256SUMS) verified before extraction. We branch on
# $TARGETARCH so the same Containerfile works under buildx multi-arch
# (linux/amd64 + linux/arm64); the CI workflow today is amd64 only but the
# Sovereign-side rebuild path may need arm64 in future.
FROM docker.io/library/alpine:3.20 AS tofu
ARG TARGETARCH=amd64
ARG TOFU_VERSION=1.11.6
# SHA256 from tofu_${TOFU_VERSION}_SHA256SUMS, fetched from the canonical
# OpenTofu GitHub release. Update both values together when bumping the
# pinned version.
ARG TOFU_SHA256_AMD64=02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e
ARG TOFU_SHA256_ARM64=d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850
RUN apk add --no-cache ca-certificates curl tar \
&& mkdir -p /out \
&& case "${TARGETARCH}" in \
amd64) TOFU_SHA256="${TOFU_SHA256_AMD64}" ;; \
arm64) TOFU_SHA256="${TOFU_SHA256_ARM64}" ;; \
*) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& curl -fsSL -o /tmp/tofu.tar.gz \
"https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_linux_${TARGETARCH}.tar.gz" \
&& echo "${TOFU_SHA256} /tmp/tofu.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/tofu.tar.gz -C /tmp tofu \
&& install -m 0755 /tmp/tofu /out/tofu \
&& /out/tofu version
# ---------- Stage 3: runtime ----------
FROM docker.io/library/alpine:3.20
# kubectl + helm must be on PATH so internal/bootstrap can exec them when
# installing the 11-component bootstrap kit. Pin versions for reproducible
# bootstraps; the K8s minor must match what the wizard provisions.
ARG KUBECTL_VERSION=v1.31.4
ARG HELM_VERSION=v3.16.3
RUN apk add --no-cache ca-certificates curl bash git \
&& curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" -o /usr/local/bin/kubectl \
&& chmod +x /usr/local/bin/kubectl \
&& curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" | tar xz -C /tmp \
&& mv /tmp/linux-amd64/helm /usr/local/bin/helm \
&& rm -rf /tmp/linux-amd64 \
&& chmod +x /usr/local/bin/helm
# git is required by the SME tenant provisioning pipeline (#804) and the
# marketplace-settings GitOps writer — both shell out to `git clone /
# commit / push` against the catalyst-zero / Sovereign GitOps repo.
# Without git on PATH the pipeline fails with `exec: "git": executable
# file not found` at the first reconcile (issue #878).
# tofu CLI from the verified-checksum builder stage. Installed mode 0755 so
# every user (including UID 65534 from runAsUser) can execute it.
COPY --from=tofu /out/tofu /usr/local/bin/tofu
COPY --from=build /catalyst-api /catalyst-api
COPY --from=build /catalyst-dns /usr/local/bin/catalyst-dns
# Bundle the canonical OpenTofu module. provisioner.stageModule() copies these
# .tf / .tftpl files into a per-deployment workdir at Launch; the path is
# CATALYST_TOFU_MODULE_PATH (default /infra/hetzner — see provisioner.New()).
# Copying the FULL tree (main.tf, variables.tf, outputs.tf, versions.tf,
# cloudinit-*.tftpl, plus any future submodules under modules/) keeps the
# image self-contained: the OpenTofu run inside this Pod has every file the
# module references, with no network fetch needed at provision time.
COPY infra/hetzner/ /infra/hetzner/
# Pre-create the deployments-store directory with the runtime UID's
# ownership so the catalyst-api process (UID 65534) can write to it.
# In K8s the Pod template's securityContext.fsGroup=65534 chown-walks
# the PVC at attach time, but docker bind-mounts (used by the
# manual-proof harness in the ticket: `docker run -v /tmp/catalyst-
# test:/var/lib/catalyst/deployments`) inherit the host directory's
# permissions, NOT the image's. The store's New() probes writability
# at process start; if the bind-mount isn't writable, the Pod logs
# "deployment store unavailable" and falls back to in-memory state
# (no panic). This mkdir guarantees the in-image path exists with
# correct ownership for the K8s case where the PVC is empty on
# first attach.
RUN mkdir -p /var/lib/catalyst/deployments \
&& chown -R 65534:65534 /var/lib/catalyst \
&& chmod 0700 /var/lib/catalyst/deployments
# Alpine 3.20 already ships UID 65534 as `nobody`. Reuse that rather than
# creating a duplicate `nonroot` account (adduser would fail with
# "uid '65534' in use"). The numeric form satisfies runAsNonRoot in K8s.
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/catalyst-api"]