diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2cdd5fa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,206 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + chart: + name: Chart contract + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + + - name: Set up Helm + uses: azure/setup-helm@v4.3.1 + with: + version: v3.20.0 + + - name: Install helm-unittest + run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.0.3 + + - name: Lint and render both profiles + run: | + set -euo pipefail + helm lint --strict . + helm template forge . --namespace forge-ci >/dev/null + helm template forge . --namespace forge-ci \ + --set profile=production \ + --set localAuth.enabled=false \ + --set database.existingSecret=forge-database \ + --set objectStore.existingSecret=forge-object-store \ + --set objectStore.endpoint=https://account.r2.cloudflarestorage.com \ + --set identity.issuer=https://identity.example.invalid \ + --set identity.projectId=forge \ + --set-string images.edge.digest=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ + --set-string images.site.digest=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ + --set-string images.api.digest=sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc \ + >/dev/null + + - name: Prove schema rejection + run: | + set -euo pipefail + if helm template forge . --set unknownTopLevel=true >/dev/null 2>&1; then + echo 'schema accepted an unknown top-level value' >&2 + exit 1 + fi + if helm template forge . \ + --set profile=production \ + --set localAuth.enabled=true \ + >/dev/null 2>&1; then + echo 'schema accepted an unsafe production profile' >&2 + exit 1 + fi + + - name: Run render contract tests + run: helm unittest --strict . + + - name: Reject copied application source + run: | + set -euo pipefail + test ! -e Cargo.toml + test ! -d crates + test ! -d src + + security: + name: Security scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + + - name: Scan chart configuration + uses: aquasecurity/trivy-action@v0.35.0 + with: + scan-type: config + scan-ref: . + exit-code: '1' + severity: HIGH,CRITICAL + + entrypoint: + name: Combined-image entrypoint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + + - name: Build inert fixture bases + run: | + set -euo pipefail + fixture="$RUNNER_TEMP/gitkb-forge-fixture" + mkdir -p "$fixture/harmony" "$fixture/git-api" + + cat >"$fixture/harmony/harmony-api" <<'SCRIPT' + #!/bin/sh + set -eu + mkdir -p /tmp/fixture + case "${1:-}" in + migrate) + : > /tmp/fixture/harmony.migrated + ;; + serve) + : > /tmp/fixture/harmony.started + trap ': > /tmp/fixture/harmony.terminated; exit 0' TERM INT + if [ "${FAIL_HARMONY:-0}" = 1 ]; then + sleep 2 + exit 23 + fi + while :; do sleep 1; done + ;; + *) exit 64 ;; + esac + SCRIPT + chmod 755 "$fixture/harmony/harmony-api" + cat >"$fixture/harmony/Containerfile" <<'CONTAINERFILE' + FROM alpine:3.22 + COPY --chmod=0755 harmony-api /app/harmony-api + CONTAINERFILE + + cat >"$fixture/git-api/walgit" <<'SCRIPT' + #!/bin/sh + set -eu + mkdir -p /tmp/fixture + test "${1:-}" = serve + if [ "${PORT+x}" = x ]; then + : > /tmp/fixture/walgit.port-leaked + fi + : > /tmp/fixture/walgit.started + trap ': > /tmp/fixture/walgit.terminated; exit 0' TERM INT + while :; do sleep 1; done + SCRIPT + chmod 755 "$fixture/git-api/walgit" + cat >"$fixture/git-api/Containerfile" <<'CONTAINERFILE' + FROM alpine:3.22 + RUN apk add --no-cache tini \ + && addgroup -g 1000 forge \ + && adduser -D -u 1000 -G forge forge + COPY --chmod=0755 walgit /usr/local/bin/walgit + USER 1000:1000 + ENTRYPOINT ["tini", "--", "/usr/local/bin/walgit"] + CMD ["serve"] + CONTAINERFILE + + docker build -f "$fixture/harmony/Containerfile" -t fixture-harmony-api "$fixture/harmony" + docker build -f "$fixture/git-api/Containerfile" -t fixture-git-api "$fixture/git-api" + docker build -f images/api/Containerfile \ + --build-arg HARMONY_API_IMAGE=fixture-harmony-api \ + --build-arg GIT_API_IMAGE=fixture-git-api \ + --build-arg GITKB_FORGE_REVISION="$GITHUB_SHA" \ + -t fixture-gitkb-forge-api . + test "$(docker image inspect fixture-gitkb-forge-api --format '{{.Config.User}}')" = 1000:1000 + + - name: Prove API supervision and child failure + run: | + set -euo pipefail + trap 'docker rm -f forge-entrypoint-api >/dev/null 2>&1 || true' EXIT + docker run -d --name forge-entrypoint-api \ + -e FAIL_HARMONY=1 -e PORT=8080 fixture-gitkb-forge-api api >/dev/null + status=$(docker wait forge-entrypoint-api) + test "$status" = 23 + docker cp forge-entrypoint-api:/tmp/fixture "$RUNNER_TEMP/api-fixture" + test -f "$RUNNER_TEMP/api-fixture/harmony.migrated" + test -f "$RUNNER_TEMP/api-fixture/harmony.started" + test -f "$RUNNER_TEMP/api-fixture/walgit.started" + test -f "$RUNNER_TEMP/api-fixture/walgit.terminated" + test ! -e "$RUNNER_TEMP/api-fixture/walgit.port-leaked" + + - name: Prove API signal handling + run: | + set -euo pipefail + trap 'docker rm -f forge-entrypoint-signal >/dev/null 2>&1 || true' EXIT + docker run -d --name forge-entrypoint-signal \ + -e PORT=8080 fixture-gitkb-forge-api api >/dev/null + for attempt in $(seq 1 20); do + if docker exec forge-entrypoint-signal \ + sh -c 'test -f /tmp/fixture/harmony.started && test -f /tmp/fixture/walgit.started'; then + break + fi + test "$attempt" -lt 20 + sleep 1 + done + docker stop --time 10 forge-entrypoint-signal >/dev/null + docker cp forge-entrypoint-signal:/tmp/fixture "$RUNNER_TEMP/signal-fixture" + test -f "$RUNNER_TEMP/signal-fixture/harmony.terminated" + test -f "$RUNNER_TEMP/signal-fixture/walgit.terminated" + test ! -e "$RUNNER_TEMP/signal-fixture/walgit.port-leaked" + + - name: Prove worker runs only Git maintenance + run: | + set -euo pipefail + trap 'docker rm -f forge-entrypoint-worker >/dev/null 2>&1 || true' EXIT + docker run -d --name forge-entrypoint-worker fixture-gitkb-forge-api worker >/dev/null + for attempt in $(seq 1 20); do + if docker exec forge-entrypoint-worker test -f /tmp/fixture/walgit.started; then + break + fi + test "$attempt" -lt 20 + sleep 1 + done + docker exec forge-entrypoint-worker test ! -e /tmp/fixture/harmony.migrated + docker exec forge-entrypoint-worker test ! -e /tmp/fixture/harmony.started + docker stop --time 10 forge-entrypoint-worker >/dev/null + docker cp forge-entrypoint-worker:/tmp/fixture "$RUNNER_TEMP/worker-fixture" + test -f "$RUNNER_TEMP/worker-fixture/walgit.terminated" diff --git a/Chart.yaml b/Chart.yaml new file mode 100644 index 0000000..4ab826f --- /dev/null +++ b/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: gitkb-forge +description: Four-container GitKB Forge appliance +type: application +version: 0.1.0 +appVersion: "0.1.0" +home: https://github.com/gitkb/gitkb-forge +sources: + - https://github.com/gitkb/gitkb-forge +annotations: + artifacthub.io/license: MIT diff --git a/README.md b/README.md index f12c559..bc60355 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,173 @@ # GitKB Forge -Public packaging for the GitKB Forge Helm appliance. +GitKB Forge is a four-container Helm appliance for hosting and composing +Git/knowledge repositories. It packages existing GitKB binaries; it does not +copy Harmony API or Git API source. -The chart, combined API image assembly, site shell, operator documentation, and -hermetic appliance proof are developed on scoped task branches before review. +The internal-alpha topology is explicit: -Implements [[tasks/harmony-1329]]. +| Product workload | Responsibility | +| --- | --- | +| `edge` | Public HTTP routing and streaming reverse proxy | +| `site` | Dependency-free status and entry shell | +| `api` | Harmony API on 8080 plus read-serving Git API on 8081 | +| `worker` | Git writes, maintenance, and ref events on 8082 | + +The default `appliance` profile also runs PostgreSQL and RustFS as persistent +backing services. They are not additional GitKB product workloads. The chart +contains no Forgejo, Redis, identity server, SMTP service, billing service, +runner, sidecar, or Git SSH endpoint. + +## Install the appliance + +A default dynamic StorageClass is the only prerequisite. Once the release +image exists, no values file or external service is required: + +```sh +helm install forge . \ + --namespace gitkb-forge \ + --create-namespace \ + --wait \ + --wait-for-jobs \ + --timeout 5m + +helm test forge --namespace gitkb-forge --logs +kubectl port-forward --namespace gitkb-forge \ + service/forge-gitkb-forge-edge 8080:8080 +``` + +On first install, a normal Kubernetes Job creates the RustFS bucket after the +Helm-managed Secret exists; API readiness stays false until PostgreSQL and the +bucket are usable. An in-place upgrade runs the same check as a pre-upgrade +hook, so `--wait` cannot report a dependency-incomplete appliance as Ready. + +The site is then available at . Generated credentials +and the appliance's separated PostgreSQL bootstrap/migration/runtime roles are +stored in `forge-gitkb-forge-secrets`, retained across upgrades through Helm +`lookup`, and never printed by the chart or test. + +Capture the Git token directly into the shell environment without displaying +it, then use a temporary askpass helper: + +```sh +export WALGIT_TOKEN_MVP=$(kubectl get secret forge-gitkb-forge-secrets \ + --namespace gitkb-forge \ + -o 'go-template={{ index .data "WALGIT_TOKEN_MVP" | base64decode }}') + +ASKPASS=$(mktemp) +chmod 700 "$ASKPASS" +trap 'rm -f "$ASKPASS"' EXIT +printf '%s\n' '#!/bin/sh' \ + 'case "$1" in' \ + ' *Username*) printf "%s\\n" mvp-admin ;;' \ + ' *) printf "%s\\n" "$WALGIT_TOKEN_MVP" ;;' \ + 'esac' >"$ASKPASS" +export GIT_ASKPASS="$ASKPASS" GIT_TERMINAL_PROMPT=0 + +git clone http://127.0.0.1:8080/OWNER/REPOSITORY.git +``` + +Credentials must not be embedded in Git URLs, command arguments, values files, +or diagnostic output. An authenticated first push creates a repository. + +## External PostgreSQL and R2 + +The `production` profile renders only the four product workloads. It requires +an existing PostgreSQL URL, S3-compatible credentials, an external OIDC +issuer/project, and digest-selected product images. Cloudflare R2 uses region +`auto`, explicit path-style requests, and separate knowledge/Git prefixes. + +Create the Secrets from existing environment variables through stdin so values +do not appear in process arguments or terminal output: + +```sh +: "${DATABASE_URL:?required}" +: "${AWS_ACCESS_KEY_ID:?required}" +: "${AWS_SECRET_ACCESS_KEY:?required}" +: "${WALGIT_TOKEN_MVP:?required}" +: "${INVITATION_HMAC_SECRET:?required}" + +printf '%s\n' "DATABASE_URL=$DATABASE_URL" | \ + kubectl create secret generic forge-database \ + --namespace gitkb-forge --from-env-file=/dev/stdin + +printf '%s\n' \ + "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" \ + "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" \ + "WALGIT_TOKEN_MVP=$WALGIT_TOKEN_MVP" \ + "INVITATION_HMAC_SECRET=$INVITATION_HMAC_SECRET" | \ + kubectl create secret generic forge-object-store \ + --namespace gitkb-forge --from-env-file=/dev/stdin +``` + +Install with non-secret endpoints and immutable image digests: + +```sh +helm install forge . --namespace gitkb-forge \ + --set profile=production \ + --set localAuth.enabled=false \ + --set database.existingSecret=forge-database \ + --set objectStore.existingSecret=forge-object-store \ + --set objectStore.endpoint=https://ACCOUNT.r2.cloudflarestorage.com \ + --set objectStore.bucket=gitkb-forge \ + --set identity.issuer=https://identity.example.com \ + --set identity.projectId=gitkb-forge \ + --set-string images.edge.digest=sha256:EDGE_DIGEST \ + --set-string images.site.digest=sha256:SITE_DIGEST \ + --set-string images.api.digest=sha256:API_DIGEST \ + --wait --wait-for-jobs --timeout 5m +``` + +`values.schema.json` rejects missing production inputs, local development auth, +non-digest product images, unknown top-level values, and path-style behavior +that differs from the qualified Git engine profile. + +## Build the combined API image + +The assembly consumes two immutable base-image references: + +```sh +docker build -f images/api/Containerfile \ + --build-arg HARMONY_API_IMAGE=REGISTRY/harmony-api@sha256:HARMONY_DIGEST \ + --build-arg GIT_API_IMAGE=REGISTRY/git-api@sha256:GIT_API_DIGEST \ + --build-arg GITKB_FORGE_REVISION=$(git rev-parse HEAD) \ + -t gitkb-forge-api:0.1.0 . +``` + +The resulting non-root image contains `/usr/local/bin/harmony-api` and +`/usr/local/bin/walgit`. `api` mode migrates the appliance database, starts +both HTTP processes, and terminates the survivor if either exits. `worker` +mode executes only `walgit`. `tini` remains PID 1 and delegates signals. + +## Verify + +Static pull-request gates are secret-free: + +```sh +helm lint --strict . +helm template forge . >/tmp/gitkb-forge.yaml +helm unittest . +trivy config . --severity HIGH,CRITICAL --exit-code 1 +``` + +The full local gate builds both private source images through their supported +paths and runs the persistent appliance against RustFS in a disposable kind +cluster: + +```sh +scripts/kind-smoke.sh +``` + +The script refuses to reuse another cluster, keeps credentials out of URLs and +arguments, removes only `gitkb-forge-mvp` unless `KEEP_CLUSTER=1`, and emits a +redacted JSON receipt. Live R2 is optional and is not required by this task. + +## Alpha limits + +This release is single-replica and not an HA claim. It does not provide SSH, +LFS, pull requests, reviews, issues, Actions, hosted runners, public signup, +multi-user RBAC, backup automation, billing, quotas, or an enterprise support +matrix. The site is an operator entry shell, not a product dashboard. + +Implements [[tasks/harmony-1329]], [[specs/forge/runtime-topology]], and +[[specs/forge/bets/hosted-git-appliance]]. diff --git a/images/api/Containerfile b/images/api/Containerfile new file mode 100644 index 0000000..a95bba5 --- /dev/null +++ b/images/api/Containerfile @@ -0,0 +1,22 @@ +ARG HARMONY_API_IMAGE +ARG GIT_API_IMAGE + +FROM ${HARMONY_API_IMAGE} AS harmony-api +FROM ${GIT_API_IMAGE} + +ARG GITKB_FORGE_REVISION=dev +ARG GITKB_FORGE_VERSION=0.1.0 + +COPY --from=harmony-api --chmod=0755 /app/harmony-api /usr/local/bin/harmony-api +COPY --chmod=0755 images/api/entrypoint.sh /usr/local/bin/gitkb-forge-entrypoint + +LABEL org.opencontainers.image.source="https://github.com/gitkb/gitkb-forge" \ + org.opencontainers.image.revision="${GITKB_FORGE_REVISION}" \ + org.opencontainers.image.version="${GITKB_FORGE_VERSION}" \ + org.opencontainers.image.title="gitkb-forge-api" \ + org.opencontainers.image.licenses="MIT" + +USER 1000:1000 +ENV HOME=/tmp +ENTRYPOINT ["tini", "--", "/usr/local/bin/gitkb-forge-entrypoint"] +CMD ["api"] diff --git a/images/api/entrypoint.sh b/images/api/entrypoint.sh new file mode 100755 index 0000000..0ddd72e --- /dev/null +++ b/images/api/entrypoint.sh @@ -0,0 +1,98 @@ +#!/bin/sh + +set -eu + +HARMONY_PID="" +WALGIT_PID="" +CHILD_STATUS=0 + +terminate_children() { + trap - HUP INT TERM + if [ -n "$HARMONY_PID" ]; then + kill -TERM "$HARMONY_PID" 2>/dev/null || true + fi + if [ -n "$WALGIT_PID" ]; then + kill -TERM "$WALGIT_PID" 2>/dev/null || true + fi + if [ -n "$HARMONY_PID" ]; then + wait "$HARMONY_PID" 2>/dev/null || true + fi + if [ -n "$WALGIT_PID" ]; then + wait "$WALGIT_PID" 2>/dev/null || true + fi +} + +child_status() { + child=$1 + set +e + wait "$child" + CHILD_STATUS=$? + set -e +} + +run_api() { + migration_attempt=0 + until /usr/local/bin/harmony-api migrate; do + migration_attempt=$((migration_attempt + 1)) + if [ "$migration_attempt" -ge 60 ]; then + echo "Harmony database migration did not become ready" >&2 + exit 1 + fi + sleep 2 + done + + /usr/local/bin/harmony-api serve & + HARMONY_PID=$! + ( + # Harmony uses the serverless PORT convention. WalGit also recognizes it, + # so remove it only for this child and retain the chart's explicit listener. + unset PORT + exec /usr/local/bin/walgit serve + ) & + WALGIT_PID=$! + + trap 'terminate_children; exit 143' HUP INT TERM + + while :; do + if ! kill -0 "$HARMONY_PID" 2>/dev/null; then + child_status "$HARMONY_PID" + status=$CHILD_STATUS + HARMONY_PID="" + terminate_children + exit "$status" + fi + if ! kill -0 "$WALGIT_PID" 2>/dev/null; then + child_status "$WALGIT_PID" + status=$CHILD_STATUS + WALGIT_PID="" + terminate_children + exit "$status" + fi + sleep 1 & + wait $! + done +} + +case "${1:-}" in + api) + shift + if [ "$#" -ne 0 ]; then + echo "api mode accepts no additional arguments" >&2 + exit 64 + fi + run_api + ;; + worker) + shift + if [ "$#" -ne 0 ]; then + echo "worker mode accepts no additional arguments" >&2 + exit 64 + fi + unset PORT + exec /usr/local/bin/walgit serve + ;; + *) + echo "usage: gitkb-forge-entrypoint api|worker" >&2 + exit 64 + ;; +esac diff --git a/scripts/kind-smoke.sh b/scripts/kind-smoke.sh new file mode 100755 index 0000000..b7185f4 --- /dev/null +++ b/scripts/kind-smoke.sh @@ -0,0 +1,367 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +set +x + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +META_ROOT=$(cd "$ROOT/.." && pwd) +CLUSTER=gitkb-forge-mvp +NAMESPACE=gitkb-forge-mvp +RELEASE=forge +NODE_IMAGE='kindest/node:v1.35.0@sha256:452d707d4862f52530247495d180205e029056831160e22870e37e3f6c1ac31f' +GIT_API_IMAGE=${GIT_API_IMAGE:-git-api:0.1.0} +HARMONY_API_IMAGE=${HARMONY_API_IMAGE:-harmony-api:gitkb-forge-mvp} +COMBINED_IMAGE=${COMBINED_IMAGE:-gitkb-forge-api:0.1.0} +FORWARD_PORT=${FORGE_KIND_PORT:-18080} +TMP_DIR=$(mktemp -d /tmp/gitkb-forge-mvp.XXXXXX) +PORT_FORWARD_PID="" +CLUSTER_CREATED=false +STARTED_AT=$(date +%s) +BUILD_STARTED_AT=$STARTED_AT +INSTALL_STARTED_AT=0 +PROOF_STARTED_AT=0 + +die() { + printf 'kind-smoke: %s\n' "$*" >&2 + exit 1 +} + +kind_cmd() { + if [ -n "${KIND_BIN:-}" ]; then + "$KIND_BIN" "$@" + elif command -v kind >/dev/null 2>&1; then + kind "$@" + else + mise x kind@0.31.0 -- kind "$@" + fi +} + +kube() { + kubectl --context "kind-$CLUSTER" "$@" +} + +cleanup() { + status=$? + trap - EXIT + set +e + if [ -n "$PORT_FORWARD_PID" ]; then + kill "$PORT_FORWARD_PID" 2>/dev/null || true + wait "$PORT_FORWARD_PID" 2>/dev/null || true + fi + if [ "$CLUSTER_CREATED" = true ]; then + if [ "${KEEP_CLUSTER:-0}" = 1 ]; then + printf 'kind-smoke: KEEP_CLUSTER=1; preserving only %s\n' "$CLUSTER" >&2 + else + kind_cmd delete cluster --name "$CLUSTER" >/dev/null 2>&1 || true + fi + fi + rm -rf -- "$TMP_DIR" + exit "$status" +} +trap cleanup EXIT + +for tool in docker helm kubectl jq git curl shasum uuidgen meta git-kb; do + command -v "$tool" >/dev/null 2>&1 || die "$tool is required" +done +if ! command -v kind >/dev/null 2>&1 && [ -z "${KIND_BIN:-}" ]; then + command -v mise >/dev/null 2>&1 || die 'kind 0.31.0 or mise is required' +fi +command -v trivy >/dev/null 2>&1 || die 'trivy is required for the security gate' + +if kind_cmd get clusters 2>/dev/null | grep -Fxq "$CLUSTER"; then + die "cluster $CLUSTER already exists; refusing to reuse or delete it" +fi +case "$CLUSTER" in + gitkb-forge-mvp) ;; + *) die 'refusing to manage a cluster outside the task boundary' ;; +esac + +GIT_API_SOURCE=${GIT_API_SOURCE:-$META_ROOT/platform/git-api} +HARMONY_API_SOURCE=${HARMONY_API_SOURCE:-$META_ROOT/platform/api} +[ -f "$GIT_API_SOURCE/Containerfile" ] || die "Git API source is missing at $GIT_API_SOURCE" +[ -f "$HARMONY_API_SOURCE/Dockerfile" ] || die "Harmony API source is missing at $HARMONY_API_SOURCE" + +printf 'kind-smoke: building or selecting Git API image\n' >&2 +if ! docker image inspect "$GIT_API_IMAGE" >/dev/null 2>&1; then + git_revision=$(git -C "$GIT_API_SOURCE" rev-parse HEAD) + if ! docker build \ + --build-arg "WALGIT_BUILD_SHA=$git_revision" \ + -f "$GIT_API_SOURCE/Containerfile" \ + -t "$GIT_API_IMAGE" \ + "$GIT_API_SOURCE" >"$TMP_DIR/git-api-build.log" 2>&1; then + tail -n 80 "$TMP_DIR/git-api-build.log" >&2 + die 'Git API image build failed' + fi +fi + +printf 'kind-smoke: building or selecting Harmony API image\n' >&2 +if ! docker image inspect "$HARMONY_API_IMAGE" >/dev/null 2>&1; then + if [ -z "${HARMONY_BUILD_GITHUB_TOKEN:-}" ]; then + command -v gh >/dev/null 2>&1 || die 'gh is required to supply the private build secret' + HARMONY_BUILD_GITHUB_TOKEN=$(gh auth token 2>/dev/null) || die 'unable to obtain the private build secret' + fi + export HARMONY_BUILD_GITHUB_TOKEN + if ! DOCKER_BUILDKIT=1 docker build \ + --secret id=github_token,env=HARMONY_BUILD_GITHUB_TOKEN \ + -f "$HARMONY_API_SOURCE/Dockerfile" \ + -t "$HARMONY_API_IMAGE" \ + "$HARMONY_API_SOURCE" >"$TMP_DIR/harmony-api-build.log" 2>&1; then + unset HARMONY_BUILD_GITHUB_TOKEN + tail -n 80 "$TMP_DIR/harmony-api-build.log" >&2 + die 'Harmony API supported Docker build failed' + fi + unset HARMONY_BUILD_GITHUB_TOKEN +fi + +printf 'kind-smoke: assembling the shared API image\n' >&2 +forge_revision=$(git -C "$ROOT" rev-parse HEAD) +if ! docker build \ + --build-arg "HARMONY_API_IMAGE=$HARMONY_API_IMAGE" \ + --build-arg "GIT_API_IMAGE=$GIT_API_IMAGE" \ + --build-arg "GITKB_FORGE_REVISION=$forge_revision" \ + -f "$ROOT/images/api/Containerfile" \ + -t "$COMBINED_IMAGE" \ + "$ROOT" >"$TMP_DIR/combined-build.log" 2>&1; then + tail -n 80 "$TMP_DIR/combined-build.log" >&2 + die 'combined API image build failed' +fi + +test "$(docker image inspect "$COMBINED_IMAGE" --format '{{.Config.User}}')" = '1000:1000' \ + || die 'combined image must run as UID/GID 1000' +docker run --rm --entrypoint /usr/local/bin/harmony-api "$COMBINED_IMAGE" --help >/dev/null +docker run --rm --entrypoint /usr/local/bin/walgit "$COMBINED_IMAGE" --help >/dev/null +docker run --rm --entrypoint git "$COMBINED_IMAGE" --version >/dev/null +docker run --rm --entrypoint git-lfs "$COMBINED_IMAGE" version >/dev/null + +GIT_API_DIGEST=$(docker image inspect "$GIT_API_IMAGE" --format '{{.Id}}') +HARMONY_API_DIGEST=$(docker image inspect "$HARMONY_API_IMAGE" --format '{{.Id}}') +COMBINED_DIGEST=$(docker image inspect "$COMBINED_IMAGE" --format '{{.Id}}') +BUILD_FINISHED_AT=$(date +%s) + +printf 'kind-smoke: running chart, schema, unit, and security gates\n' >&2 +helm lint --strict "$ROOT" >/dev/null +helm template forge "$ROOT" --namespace "$NAMESPACE" >/dev/null +helm template forge "$ROOT" --namespace "$NAMESPACE" \ + --set profile=production \ + --set localAuth.enabled=false \ + --set database.existingSecret=database \ + --set objectStore.existingSecret=object-store \ + --set objectStore.endpoint=https://account.r2.cloudflarestorage.com \ + --set identity.issuer=https://identity.example.invalid \ + --set identity.projectId=forge \ + --set-string images.edge.digest=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ + --set-string images.site.digest=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ + --set-string images.api.digest=sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc \ + >/dev/null +helm unittest --strict "$ROOT" >/dev/null +trivy config "$ROOT" --severity HIGH,CRITICAL --exit-code 1 >/dev/null + +printf 'kind-smoke: creating pinned kind cluster\n' >&2 +kind_cmd create cluster --name "$CLUSTER" --image "$NODE_IMAGE" --wait 5m +CLUSTER_CREATED=true + +docker image inspect "$COMBINED_IMAGE" >/dev/null 2>&1 \ + || die "combined first-party image $COMBINED_IMAGE is missing" +kind_cmd load docker-image "$COMBINED_IMAGE" --name "$CLUSTER" >/dev/null + +INSTALL_STARTED_AT=$(date +%s) +kube create namespace "$NAMESPACE" >/dev/null + +IMAGE_OVERRIDES=( + --set-string images.api.repository=gitkb-forge-api + --set-string images.api.tag=0.1.0 + --set-string images.api.digest= + --set-string images.api.pullPolicy=Never +) + +helm upgrade --install "$RELEASE" "$ROOT" \ + --kube-context "kind-$CLUSTER" \ + --namespace "$NAMESPACE" \ + "${IMAGE_OVERRIDES[@]}" \ + --wait --wait-for-jobs --timeout 5m +INSTALL_FINISHED_AT=$(date +%s) +PROOF_STARTED_AT=$INSTALL_FINISHED_AT + +deployments=$(kube get deployments -n "$NAMESPACE" -o json) +test "$(printf '%s' "$deployments" | jq '[.items[] | select(.metadata.labels["gitkb.io/workload"] == "product")] | length')" = 4 \ + || die 'expected exactly four product Deployments' +test "$(printf '%s' "$deployments" | jq '[.items[] | select(.metadata.labels["gitkb.io/workload"] == "product") | .spec.template.spec.containers | length] | add')" = 4 \ + || die 'each product Deployment must contain exactly one container' +test "$(printf '%s' "$deployments" | jq -r '[.items[] | select(.metadata.labels["gitkb.io/workload"] == "product") | .metadata.labels["app.kubernetes.io/component"]] | sort | join(",")')" = 'api,edge,site,worker' \ + || die 'product component inventory differs from api,edge,site,worker' +test "$(printf '%s' "$deployments" | jq '[.items[] | select(.metadata.labels["gitkb.io/workload"] == "product" and .status.availableReplicas == 1)] | length')" = 4 \ + || die 'all four product Deployments must be Ready' + +statefulsets=$(kube get statefulsets -n "$NAMESPACE" -o json) +test "$(printf '%s' "$statefulsets" | jq '[.items[] | select(.metadata.labels["gitkb.io/workload"] == "backing")] | length')" = 2 \ + || die 'expected PostgreSQL and RustFS backing StatefulSets' +test "$(printf '%s' "$statefulsets" | jq '[.items[] | select(.metadata.labels["gitkb.io/workload"] == "backing" and .status.readyReplicas == 1)] | length')" = 2 \ + || die 'PostgreSQL and RustFS must both be Ready' +test "$(kube get deployments -n "$NAMESPACE" -o json | jq '.items | length')" = 4 \ + || die 'a forbidden Deployment was rendered' + +kube port-forward -n "$NAMESPACE" "service/$RELEASE-gitkb-forge-edge" "$FORWARD_PORT:8080" \ + >"$TMP_DIR/port-forward.log" 2>&1 & +PORT_FORWARD_PID=$! +base_url="http://127.0.0.1:$FORWARD_PORT" +for _ in $(seq 1 90); do + if curl -fsS "$base_url/" >/dev/null 2>&1; then break; fi + sleep 1 +done +curl -fsS "$base_url/" | grep -q 'GitKB Forge' +curl -fsS "$base_url/health" >/dev/null +curl -fsS "$base_url/ready" >/dev/null +curl -fsS "$base_url/_git/read/healthz" >/dev/null +curl -fsS "$base_url/_git/write/healthz" >/dev/null + +WALGIT_TOKEN_MVP=$(kube get secret "$RELEASE-gitkb-forge-secrets" -n "$NAMESPACE" \ + -o 'go-template={{ index .data "WALGIT_TOKEN_MVP" | base64decode }}') +export WALGIT_TOKEN_MVP +[ -n "$WALGIT_TOKEN_MVP" ] || die 'generated Git token is empty' +ASKPASS="$TMP_DIR/askpass.sh" +cat >"$ASKPASS" <<'ASKPASS' +#!/bin/sh +case "$1" in + *Username*) printf '%s\n' mvp-admin ;; + *) printf '%s\n' "$WALGIT_TOKEN_MVP" ;; +esac +ASKPASS +chmod 700 "$ASKPASS" +export GIT_ASKPASS="$ASKPASS" +export GIT_TERMINAL_PROMPT=0 +export GIT_CONFIG_NOSYSTEM=1 +export GIT_CONFIG_GLOBAL=/dev/null + +RUN_ID=$(uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]') +REPOSITORY="kind-smoke/repository-$RUN_ID.git" +REMOTE="$base_url/$REPOSITORY" +WORK="$TMP_DIR/work" +CLONE="$TMP_DIR/clone" +mkdir -p "$WORK" +git -C "$WORK" init -q --initial-branch=main +git -C "$WORK" config user.name 'GitKB Forge kind smoke' +git -C "$WORK" config user.email 'forge-kind@gitkb.invalid' +git -C "$WORK" config commit.gpgsign false +printf 'GitKB Forge deterministic kind fixture\n' >"$WORK/README.md" +git -C "$WORK" add README.md +tree=$(git -C "$WORK" write-tree) +ORIGINAL_OID=$(printf '%s\n' 'deterministic four-container kind commit' | \ + GIT_AUTHOR_NAME='GitKB Forge kind smoke' \ + GIT_AUTHOR_EMAIL='forge-kind@gitkb.invalid' \ + GIT_AUTHOR_DATE='2026-08-24T00:00:00+0000' \ + GIT_COMMITTER_NAME='GitKB Forge kind smoke' \ + GIT_COMMITTER_EMAIL='forge-kind@gitkb.invalid' \ + GIT_COMMITTER_DATE='2026-08-24T00:00:00+0000' \ + git -C "$WORK" commit-tree "$tree") +git -C "$WORK" update-ref refs/heads/main "$ORIGINAL_OID" +git -C "$WORK" remote add origin "$REMOTE" +git -C "$WORK" push -q origin HEAD:refs/heads/main +git clone -q "$REMOTE" "$CLONE" +CLONED_OID=$(git -C "$CLONE" rev-parse HEAD) +test "$CLONED_OID" = "$ORIGINAL_OID" || die 'clean clone OID differs from first push' +git -C "$CLONE" fsck --strict >/dev/null + +kube logs -n "$NAMESPACE" "deployment/$RELEASE-gitkb-forge-edge" >"$TMP_DIR/edge.log" +grep -Fq "method=GET uri=/$REPOSITORY/info/refs status=200 route=git-worker service=git-receive-pack" "$TMP_DIR/edge.log" \ + || die 'receive-pack discovery did not reach the worker' +grep -Fq "method=POST uri=/$REPOSITORY/git-receive-pack status=200 route=git-worker service=" "$TMP_DIR/edge.log" \ + || die 'receive-pack body did not reach the worker' +grep -Fq "method=GET uri=/$REPOSITORY/info/refs status=200 route=git-api service=git-upload-pack" "$TMP_DIR/edge.log" \ + || die 'upload-pack discovery did not reach the API' +if grep -qi 'authorization' "$TMP_DIR/edge.log"; then + die 'edge log names an authorization header' +fi +if awk 'index($0, ENVIRON["WALGIT_TOKEN_MVP"]) { found=1 } END { exit found ? 0 : 1 }' "$TMP_DIR/edge.log"; then + die 'edge log contains the Git token' +fi + +API_POD_BEFORE=$(kube get pod -n "$NAMESPACE" -l app.kubernetes.io/component=api -o jsonpath='{.items[0].metadata.uid}') +WORKER_POD_BEFORE=$(kube get pod -n "$NAMESPACE" -l app.kubernetes.io/component=worker -o jsonpath='{.items[0].metadata.uid}') +kube delete pod -n "$NAMESPACE" -l app.kubernetes.io/component=api --wait=true >/dev/null +kube delete pod -n "$NAMESPACE" -l app.kubernetes.io/component=worker --wait=true >/dev/null +kube rollout status -n "$NAMESPACE" "deployment/$RELEASE-gitkb-forge-api" --timeout=5m >/dev/null +kube rollout status -n "$NAMESPACE" "deployment/$RELEASE-gitkb-forge-worker" --timeout=5m >/dev/null +API_POD_AFTER=$(kube get pod -n "$NAMESPACE" -l app.kubernetes.io/component=api -o jsonpath='{.items[0].metadata.uid}') +WORKER_POD_AFTER=$(kube get pod -n "$NAMESPACE" -l app.kubernetes.io/component=worker -o jsonpath='{.items[0].metadata.uid}') +test "$API_POD_BEFORE" != "$API_POD_AFTER" || die 'API Pod was not replaced' +test "$WORKER_POD_BEFORE" != "$WORKER_POD_AFTER" || die 'worker Pod was not replaced' + +RESTART_CLONE="$TMP_DIR/restart-clone" +git clone -q "$REMOTE" "$RESTART_CLONE" +RESTART_OID=$(git -C "$RESTART_CLONE" rev-parse HEAD) +test "$RESTART_OID" = "$ORIGINAL_OID" || die 'cache-loss clone OID differs' +git -C "$RESTART_CLONE" fsck --strict >/dev/null + +GRAPH_JSON=$(GITKB_FORGE_URL="$base_url" WALGIT_TOKEN_MVP="$WALGIT_TOKEN_MVP" \ + "$GIT_API_SOURCE/scripts/gitgraph-smoke.sh") +printf '%s' "$GRAPH_JSON" | jq -e '.children == 2 and .all_origins_on_forge and .meta_json and .gitkb_json' >/dev/null \ + || die 'GitGraph smoke did not materialize both children' +GRAPH_CHILDREN=$(printf '%s' "$GRAPH_JSON" | jq -r '.children') + +if ! helm test "$RELEASE" --kube-context "kind-$CLUSTER" -n "$NAMESPACE" --logs \ + >"$TMP_DIR/helm-test.log" 2>&1; then + tail -n 80 "$TMP_DIR/helm-test.log" >&2 + die 'helm test failed' +fi +HELM_TEST_OID=$(awk -F'oid=' '/helm smoke passed oid=/{print $2; exit}' "$TMP_DIR/helm-test.log" | tr -d '[:space:]') +test "${#HELM_TEST_OID}" = 40 || die 'helm test did not report a deterministic OID' + +secret_hash() { + kube get secret "$RELEASE-gitkb-forge-secrets" -n "$NAMESPACE" -o json \ + | jq -c '.data | to_entries | sort_by(.key)' \ + | shasum -a 256 \ + | awk '{print $1}' +} + +SECRET_HASH_BEFORE=$(secret_hash) +kube exec -n "$NAMESPACE" "statefulset/$RELEASE-gitkb-forge-postgresql" -- \ + psql -U gitkb -d gitkb -v ON_ERROR_STOP=1 \ + -c 'CREATE TABLE IF NOT EXISTS forge_smoke (id integer PRIMARY KEY, value text NOT NULL); INSERT INTO forge_smoke VALUES (1, '\''persistent'\'') ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value;' \ + >/dev/null + +helm upgrade "$RELEASE" "$ROOT" \ + --kube-context "kind-$CLUSTER" \ + --namespace "$NAMESPACE" \ + --reuse-values \ + --wait --wait-for-jobs --timeout 5m >/dev/null + +SECRET_HASH_AFTER=$(secret_hash) +test "$SECRET_HASH_AFTER" = "$SECRET_HASH_BEFORE" || die 'generated Secret changed during upgrade' +POSTGRES_VALUE=$(kube exec -n "$NAMESPACE" "statefulset/$RELEASE-gitkb-forge-postgresql" -- \ + psql -U gitkb -d gitkb -Atc 'SELECT value FROM forge_smoke WHERE id = 1') +test "$POSTGRES_VALUE" = persistent || die 'PostgreSQL persisted data did not survive upgrade' + +UPGRADE_CLONE="$TMP_DIR/upgrade-clone" +git clone -q "$REMOTE" "$UPGRADE_CLONE" +UPGRADE_OID=$(git -C "$UPGRADE_CLONE" rev-parse HEAD) +test "$UPGRADE_OID" = "$ORIGINAL_OID" || die 'RustFS-backed repository did not survive upgrade' +git -C "$UPGRADE_CLONE" fsck --strict >/dev/null + +FINISHED_AT=$(date +%s) +jq -cn \ + --arg cluster "$CLUSTER" \ + --arg git_api_image "$GIT_API_DIGEST" \ + --arg harmony_api_image "$HARMONY_API_DIGEST" \ + --arg combined_image "$COMBINED_DIGEST" \ + --arg original_oid "$ORIGINAL_OID" \ + --arg cloned_oid "$CLONED_OID" \ + --arg restart_oid "$RESTART_OID" \ + --arg upgrade_oid "$UPGRADE_OID" \ + --arg helm_test_oid "$HELM_TEST_OID" \ + --argjson graph_children "$GRAPH_CHILDREN" \ + --argjson build_seconds "$((BUILD_FINISHED_AT - BUILD_STARTED_AT))" \ + --argjson install_seconds "$((INSTALL_FINISHED_AT - INSTALL_STARTED_AT))" \ + --argjson proof_seconds "$((FINISHED_AT - PROOF_STARTED_AT))" \ + --argjson total_seconds "$((FINISHED_AT - STARTED_AT))" \ + '{cluster: $cluster, + images: {git_api: $git_api_image, harmony_api: $harmony_api_image, combined: $combined_image}, + workloads: {product: 4, backing: 2, components: ["edge", "site", "api", "worker"]}, + oids: {original: $original_oid, clone: $cloned_oid, restart: $restart_oid, upgrade: $upgrade_oid, helm_test: $helm_test_oid}, + routes: {receive_pack_worker: true, upload_pack_api: true, authorization_redacted: true}, + restart_cache_recovery: true, + graph_children: $graph_children, + secret_preserved: true, + postgresql_persisted: true, + rustfs_persisted: true, + durations: {build_seconds: $build_seconds, install_seconds: $install_seconds, proof_seconds: $proof_seconds, total_seconds: $total_seconds}, + success: true}' diff --git a/site/forge.css b/site/forge.css new file mode 100644 index 0000000..f2ddfd8 --- /dev/null +++ b/site/forge.css @@ -0,0 +1,219 @@ +:root { + --mineral: #0b0c0a; + --mineral-raised: #12140f; + --mineral-line: #30342b; + --warm: #f2efe5; + --warm-muted: #aaa99e; + --state: #c9ff3d; + --ember: #ff6433; + --focus: #fff5b8; + font-family: ui-monospace, "SFMono-Regular", "Cascadia Code", "Roboto Mono", monospace; + color: var(--warm); + background: var(--mineral); + font-synthesis: none; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + min-width: 320px; + margin: 0; + background: + linear-gradient(rgba(201, 255, 61, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(201, 255, 61, 0.035) 1px, transparent 1px), + radial-gradient(circle at 78% 8%, rgba(255, 100, 51, 0.09), transparent 31rem), + var(--mineral); + background-size: 48px 48px, 48px 48px, auto, auto; + line-height: 1.55; +} + +a { color: inherit; text-underline-offset: 0.24em; } + +a:focus-visible, +[tabindex="0"]:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 4px; +} + +.skip-link { + position: fixed; + z-index: 10; + top: 0.75rem; + left: 0.75rem; + padding: 0.65rem 0.9rem; + color: var(--mineral); + background: var(--focus); + transform: translateY(-180%); +} + +.skip-link:focus { transform: translateY(0); } + +.masthead, +main, +footer { + width: min(1180px, calc(100% - 2rem)); + margin-inline: auto; +} + +.masthead { + min-height: 82px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + border-bottom: 1px solid var(--mineral-line); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 0.7rem; + font-weight: 760; + text-decoration: none; +} + +.brand-mark { + width: 0.8rem; + height: 0.8rem; + background: var(--state); + box-shadow: 0 0 0 4px rgba(201, 255, 61, 0.12); + animation: signal 2.8s ease-in-out infinite; +} + +nav { display: flex; gap: 1.35rem; font-size: 0.85rem; color: var(--warm-muted); } +nav a:hover { color: var(--state); } + +.hero { + min-height: 630px; + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.75fr); + align-items: center; + gap: clamp(2rem, 7vw, 7rem); + padding-block: 5rem; +} + +h1, h2, p { margin-top: 0; } + +h1 { + max-width: 14ch; + margin-bottom: 1.5rem; + font-family: Inter, ui-sans-serif, system-ui, sans-serif; + font-size: clamp(2.65rem, 7vw, 6.3rem); + font-weight: 720; + letter-spacing: -0.065em; + line-height: 0.93; +} + +h2 { + margin-bottom: 0.9rem; + font-family: Inter, ui-sans-serif, system-ui, sans-serif; + font-size: clamp(1.6rem, 3vw, 2.6rem); + letter-spacing: -0.035em; +} + +.hero-copy > p, +.section-lead { + max-width: 66ch; + color: var(--warm-muted); + font-family: ui-sans-serif, system-ui, sans-serif; + font-size: 1.05rem; +} + +.actions { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 2rem; } + +.action { + display: inline-flex; + min-height: 48px; + align-items: center; + padding: 0.75rem 1rem; + border: 1px solid var(--mineral-line); + text-decoration: none; + transition: transform 140ms ease, border-color 140ms ease, background 140ms ease; +} + +.action:hover { transform: translateY(-2px); border-color: var(--warm); } +.action.primary { color: var(--mineral); background: var(--ember); border-color: var(--ember); font-weight: 760; } +.action.secondary { background: rgba(242, 239, 229, 0.035); } + +.receipt { + position: relative; + padding: 1.25rem; + border: 1px solid var(--mineral-line); + background: linear-gradient(145deg, rgba(242, 239, 229, 0.045), rgba(242, 239, 229, 0.01)); + box-shadow: 14px 14px 0 rgba(201, 255, 61, 0.055); +} + +.receipt::before { + content: ""; + position: absolute; + inset: -1px auto auto -1px; + width: 42%; + height: 2px; + background: var(--state); +} + +.receipt-state { display: flex; align-items: center; gap: 0.65rem; padding-bottom: 1rem; border-bottom: 1px solid var(--mineral-line); } +.receipt-state span { width: 0.55rem; height: 0.55rem; border-radius: 50%; background: var(--state); } +.receipt dl { margin: 0; } +.receipt dl div { display: grid; grid-template-columns: 1fr 1.3fr; gap: 1rem; padding: 0.85rem 0; border-bottom: 1px solid rgba(48, 52, 43, 0.65); } +.receipt dl div:last-child { border-bottom: 0; } +.receipt dt { color: var(--warm-muted); } +.receipt dd { margin: 0; text-align: right; } + +.panel { padding: clamp(2rem, 5vw, 4.5rem); border: 1px solid var(--mineral-line); background: rgba(18, 20, 15, 0.88); } +.panel + .panel { margin-top: 1rem; } + +.route-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1px; + margin: 2.5rem 0 0; + padding: 1px; + list-style: none; + background: var(--mineral-line); +} + +.route-grid li { min-height: 180px; padding: 1.2rem; background: var(--mineral-raised); } +.route-grid strong { display: block; margin-bottom: 3.8rem; color: var(--state); font-size: 1rem; } +.route-grid span { color: var(--warm-muted); font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.92rem; } + +.command-panel { border-top-color: var(--ember); } +.command-block { overflow-x: auto; margin-top: 0.8rem; padding: 1rem; border-left: 3px solid var(--ember); background: #080907; color: var(--warm); white-space: nowrap; } +.quiet { margin: 1.2rem 0 0; color: var(--warm-muted); font-size: 0.82rem; } + +.diagnostics { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 2rem; padding-block: 5rem; } +.diagnostics div { display: grid; } +.diagnostics a { display: flex; justify-content: space-between; gap: 1rem; padding: 1rem 0; border-bottom: 1px solid var(--mineral-line); text-decoration: none; } +.diagnostics a:hover { color: var(--state); } + +footer { display: flex; justify-content: space-between; gap: 1rem; padding-block: 1.5rem 2.5rem; border-top: 1px solid var(--mineral-line); color: var(--warm-muted); font-size: 0.75rem; } + +@keyframes signal { + 50% { box-shadow: 0 0 0 8px rgba(201, 255, 61, 0.035); } +} + +@media (max-width: 820px) { + .hero { min-height: auto; grid-template-columns: 1fr; padding-block: 4rem; } + .route-grid { grid-template-columns: repeat(2, 1fr); } + .diagnostics { grid-template-columns: 1fr; } +} + +@media (max-width: 520px) { + .masthead { align-items: flex-start; padding-block: 1rem; } + nav { flex-direction: column; gap: 0.35rem; text-align: right; } + .hero { padding-block: 3rem; } + .actions { align-items: stretch; flex-direction: column; } + .action { justify-content: center; } + .panel { padding: 1.25rem; } + .route-grid { grid-template-columns: 1fr; } + .route-grid li { min-height: 140px; } + .route-grid strong { margin-bottom: 2.4rem; } + footer { flex-direction: column; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; } +} diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..955cbcc --- /dev/null +++ b/site/index.html @@ -0,0 +1,99 @@ + + + + + + + + GitKB Forge + + + + +
+ + + GitKB Forge + + +
+ +
+
+
+

A durable Git surface for knowledge graphs.

+

+ This appliance routes browser, knowledge API, Git read, and Git + write traffic through one public edge. Repository bytes remain + ordinary Git and compose through committed .meta.yaml + graphs. +

+ +
+
+
Installed boundary
+
+
Public entry
edge :8080
+
Product roles
edge · site · api · worker
+
Git transport
Smart HTTP
+
Durability
PostgreSQL + object storage
+
+
+
+ +
+

Request topology

+

+ Four product workloads keep browser delivery, synchronous control, + Git reads, and mutation work independently observable. +

+
    +
  1. EdgeNormalizes and streams every public request.
  2. +
  3. SiteServes this dependency-free operator shell.
  4. +
  5. APIServes knowledge routes and Git upload-pack reads.
  6. +
  7. WorkerOwns receive-pack, maintenance, and ref events.
  8. +
+
+ +
+

Connect stock Git

+

+ Use the token supplied by your operator as the password when Git asks. + The first authenticated push creates the repository. +

+
+ git clone https://FORGE_HOST/OWNER/REPOSITORY.git +
+
+ git remote add forge https://FORGE_HOST/OWNER/REPOSITORY.git
git push forge main
+
+

+ Credentials are never part of the remote URL. Automation should use + GIT_ASKPASS and disable terminal prompts. +

+
+ +
+

Live diagnostic surfaces

+
+ Harmony health + Dependency readiness + Git read health + Git write health +
+
+
+ + + + diff --git a/templates/NOTES.txt b/templates/NOTES.txt new file mode 100644 index 0000000..4a98d09 --- /dev/null +++ b/templates/NOTES.txt @@ -0,0 +1,30 @@ +GitKB Forge {{ .Chart.AppVersion }} is installed in namespace {{ .Release.Namespace }}. + +Wait for the four product workloads: + + kubectl rollout status deployment/{{ include "gitkb-forge.fullname" . }}-edge -n {{ .Release.Namespace }} + kubectl rollout status deployment/{{ include "gitkb-forge.fullname" . }}-site -n {{ .Release.Namespace }} + kubectl rollout status deployment/{{ include "gitkb-forge.fullname" . }}-api -n {{ .Release.Namespace }} + kubectl rollout status deployment/{{ include "gitkb-forge.fullname" . }}-worker -n {{ .Release.Namespace }} + +Run the deterministic stock-Git proof: + + helm test {{ .Release.Name }} -n {{ .Release.Namespace }} --logs + +{{- if .Values.ingress.enabled }} +Public URL: https://{{ .Values.ingress.host }} +Clone shape: https://{{ .Values.ingress.host }}/OWNER/REPOSITORY.git +{{- else }} +Open a local edge tunnel: + + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "gitkb-forge.fullname" . }}-edge 8080:{{ .Values.service.port }} + +Clone shape: http://127.0.0.1:8080/OWNER/REPOSITORY.git +{{- end }} + +Use the generated token as a GIT_ASKPASS environment value. Capture it without +printing it: + + FORGE_TOKEN=$(kubectl get secret {{ include "gitkb-forge.secretName" . }} -n {{ .Release.Namespace }} -o 'go-template={{ `{{ index .data "WALGIT_TOKEN_MVP" | base64decode }}` }}') + +Do not place the token in a remote URL or command argument. diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl new file mode 100644 index 0000000..fe9a664 --- /dev/null +++ b/templates/_helpers.tpl @@ -0,0 +1,96 @@ +{{/* Chart identity. */}} +{{- define "gitkb-forge.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "gitkb-forge.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := include "gitkb-forge.name" . -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "gitkb-forge.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "gitkb-forge.labels" -}} +helm.sh/chart: {{ include "gitkb-forge.chart" . }} +app.kubernetes.io/name: {{ include "gitkb-forge.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end -}} + +{{- define "gitkb-forge.componentLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{ include "gitkb-forge.labels" $root }} +app.kubernetes.io/component: {{ $component }} +gitkb.io/workload: product +{{- end -}} + +{{- define "gitkb-forge.backingLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{ include "gitkb-forge.labels" $root }} +app.kubernetes.io/component: {{ $component }} +gitkb.io/workload: backing +{{- end -}} + +{{- define "gitkb-forge.selectorLabels" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +app.kubernetes.io/name: {{ include "gitkb-forge.name" $root }} +app.kubernetes.io/instance: {{ $root.Release.Name }} +app.kubernetes.io/component: {{ $component }} +{{- end -}} + +{{- define "gitkb-forge.image" -}} +{{- $root := index . 0 -}} +{{- $component := index . 1 -}} +{{- $image := index $root.Values.images $component -}} +{{- if $image.digest -}} +{{- printf "%s@%s" $image.repository $image.digest -}} +{{- else -}} +{{- printf "%s:%s" $image.repository $image.tag -}} +{{- end -}} +{{- end -}} + +{{- define "gitkb-forge.secretName" -}} +{{- if eq .Values.profile "production" -}} +{{- required "objectStore.existingSecret is required for production" .Values.objectStore.existingSecret -}} +{{- else -}} +{{- printf "%s-secrets" (include "gitkb-forge.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{- define "gitkb-forge.databaseSecretName" -}} +{{- if eq .Values.profile "production" -}} +{{- required "database.existingSecret is required for production" .Values.database.existingSecret -}} +{{- else -}} +{{- include "gitkb-forge.secretName" . -}} +{{- end -}} +{{- end -}} + +{{- define "gitkb-forge.objectEndpoint" -}} +{{- if .Values.objectStore.endpoint -}} +{{- trimSuffix "/" .Values.objectStore.endpoint -}} +{{- else -}} +{{- printf "http://%s-rustfs.%s.svc.cluster.local:9000" (include "gitkb-forge.fullname" .) .Release.Namespace -}} +{{- end -}} +{{- end -}} + +{{- define "gitkb-forge.publicUrl" -}} +{{- if .Values.ingress.enabled -}} +{{- printf "https://%s" .Values.ingress.host -}} +{{- else -}} +{{- printf "http://%s-edge.%s.svc.cluster.local:%v" (include "gitkb-forge.fullname" .) .Release.Namespace .Values.service.port -}} +{{- end -}} +{{- end -}} diff --git a/templates/configmap-edge.yaml b/templates/configmap-edge.yaml new file mode 100644 index 0000000..586377c --- /dev/null +++ b/templates/configmap-edge.yaml @@ -0,0 +1,126 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gitkb-forge.fullname" . }}-edge + labels: + {{- include "gitkb-forge.componentLabels" (list . "edge") | nindent 4 }} +data: + nginx.conf: | + worker_processes auto; + pid /tmp/nginx.pid; + + events { + worker_connections 1024; + } + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 75s; + client_max_body_size 10g; + client_body_timeout 3600s; + proxy_connect_timeout 10s; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + send_timeout 3600s; + + map $http_x_request_id $forge_request_id { + "" $request_id; + default $http_x_request_id; + } + + log_format forge '$remote_addr request_id=$forge_request_id ' + 'method=$request_method uri=$uri status=$status ' + 'route=$forge_route service=$arg_service bytes=$body_bytes_sent'; + access_log /dev/stdout forge; + error_log /dev/stderr warn; + + upstream harmony_api { + server {{ include "gitkb-forge.fullname" . }}-api:8080; + } + upstream git_read { + server {{ include "gitkb-forge.fullname" . }}-api:8081; + } + upstream git_write { + server {{ include "gitkb-forge.fullname" . }}-worker:8082; + } + upstream forge_site { + server {{ include "gitkb-forge.fullname" . }}-site:8080; + } + + proxy_http_version 1.1; + proxy_request_buffering off; + proxy_buffering off; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + proxy_set_header Git-Protocol $http_git_protocol; + proxy_set_header X-Request-ID $forge_request_id; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + server { + listen 8080; + server_name _; + set $forge_route site; + + location = /api { + set $forge_route harmony-api; + proxy_pass http://harmony_api; + } + location ^~ /api/ { + set $forge_route harmony-api; + proxy_pass http://harmony_api; + } + location = /health { + set $forge_route harmony-api; + proxy_pass http://harmony_api; + } + location = /ready { + set $forge_route harmony-api; + proxy_pass http://harmony_api; + } + location = /metrics { + set $forge_route harmony-api; + proxy_pass http://harmony_api; + } + + location = /_git/read/healthz { + set $forge_route git-api; + proxy_pass http://git_read/healthz; + } + location = /_git/write/healthz { + set $forge_route git-worker; + proxy_pass http://git_write/healthz; + } + + location ~ ^/[^/]+/[^/]+\.git/info/refs$ { + set $forge_route git-api; + error_page 418 = @receive_pack_discovery; + if ($arg_service = git-receive-pack) { return 418; } + proxy_pass http://git_read; + } + + location @receive_pack_discovery { + set $forge_route git-worker; + proxy_pass http://git_write; + } + + location ~ ^/[^/]+/[^/]+\.git/git-receive-pack$ { + set $forge_route git-worker; + proxy_pass http://git_write; + } + + location ~ ^/[^/]+/[^/]+\.git(?:/.*)?$ { + set $forge_route git-api; + proxy_pass http://git_read; + } + + location / { + set $forge_route site; + proxy_pass http://forge_site; + } + } + } diff --git a/templates/configmap-git-api.yaml b/templates/configmap-git-api.yaml new file mode 100644 index 0000000..e169cd7 --- /dev/null +++ b/templates/configmap-git-api.yaml @@ -0,0 +1,141 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gitkb-forge.fullname" . }}-git-api + labels: + {{- include "gitkb-forge.labels" . | nindent 4 }} +data: + api.toml: | + [server] + listen = "0.0.0.0:8081" + roles = ["serve"] + auto_create_on_push = true + public_url = {{ include "gitkb-forge.publicUrl" . | quote }} + + [server.tls] + mode = "off" + + [server.auth] + mode = "token" + anonymous_read = false + tokens = [{ principal = "mvp-admin", token_env = "WALGIT_TOKEN_MVP", write = true }] + + [store] + backend = "s3" + bucket = {{ .Values.objectStore.bucket | quote }} + prefix = {{ .Values.objectStore.gitPrefix | quote }} + + [store.s3] + endpoint = {{ include "gitkb-forge.objectEndpoint" . | quote }} + region = {{ .Values.objectStore.region | quote }} + access_key_env = "AWS_ACCESS_KEY_ID" + secret_key_env = "AWS_SECRET_ACCESS_KEY" + force_path_style = {{ .Values.objectStore.forcePathStyle }} + + [cache] + dir = "/var/lib/walgit" + mode = "budget" + max_bytes = "3GiB" + + [wal] + fsck_objects = true + check_connectivity = true + + [maintenance] + checkpoints = true + + [compaction] + enabled = true + + [bundles] + serve_via = "signed_url" + signed_url_ttl = "1h" + advertise = true + + [[bundles.strategy]] + name = "weekly" + kind = "full" + schedule = "0 0 23 * * Sun" + keep = 2 + backfill_max = 1 + + [[bundles.strategy]] + name = "daily" + kind = "incremental" + base = "weekly" + schedule = "0 0 23 * * *" + backfill_max = 0 + chain = true + + [lfs] + enabled = false + + worker.toml: | + [server] + listen = "0.0.0.0:8082" + roles = ["serve", "maintain", "events"] + auto_create_on_push = true + public_url = {{ include "gitkb-forge.publicUrl" . | quote }} + + [server.tls] + mode = "off" + + [server.auth] + mode = "token" + anonymous_read = false + tokens = [{ principal = "mvp-admin", token_env = "WALGIT_TOKEN_MVP", write = true }] + + [store] + backend = "s3" + bucket = {{ .Values.objectStore.bucket | quote }} + prefix = {{ .Values.objectStore.gitPrefix | quote }} + + [store.s3] + endpoint = {{ include "gitkb-forge.objectEndpoint" . | quote }} + region = {{ .Values.objectStore.region | quote }} + access_key_env = "AWS_ACCESS_KEY_ID" + secret_key_env = "AWS_SECRET_ACCESS_KEY" + force_path_style = {{ .Values.objectStore.forcePathStyle }} + + [cache] + dir = "/var/lib/walgit" + mode = "budget" + max_bytes = "3GiB" + + [wal] + fsck_objects = true + check_connectivity = true + + [maintenance] + checkpoints = true + + [compaction] + enabled = true + + [bundles] + serve_via = "signed_url" + signed_url_ttl = "1h" + advertise = true + + [[bundles.strategy]] + name = "weekly" + kind = "full" + schedule = "0 0 23 * * Sun" + keep = 2 + backfill_max = 1 + + [[bundles.strategy]] + name = "daily" + kind = "incremental" + base = "weekly" + schedule = "0 0 23 * * *" + backfill_max = 0 + chain = true + + [lfs] + enabled = false +{{- if .Values.events.webhookUrl }} + + [events] + webhook_url = {{ .Values.events.webhookUrl | quote }} +{{- end }} diff --git a/templates/configmap-site.yaml b/templates/configmap-site.yaml new file mode 100644 index 0000000..c7dd70c --- /dev/null +++ b/templates/configmap-site.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gitkb-forge.fullname" . }}-site + labels: + {{- include "gitkb-forge.componentLabels" (list . "site") | nindent 4 }} +data: + index.html: | +{{ .Files.Get "site/index.html" | nindent 4 }} + forge.css: | +{{ .Files.Get "site/forge.css" | nindent 4 }} diff --git a/templates/deployment-api.yaml b/templates/deployment-api.yaml new file mode 100644 index 0000000..d70aa1c --- /dev/null +++ b/templates/deployment-api.yaml @@ -0,0 +1,147 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gitkb-forge.fullname" . }}-api + labels: + {{- include "gitkb-forge.componentLabels" (list . "api") | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "gitkb-forge.selectorLabels" (list . "api") | nindent 6 }} + template: + metadata: + labels: + {{- include "gitkb-forge.componentLabels" (list . "api") | nindent 8 }} + annotations: + checksum/git-config: {{ include (print $.Template.BasePath "/configmap-git-api.yaml") . | sha256sum }} + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: api + image: {{ include "gitkb-forge.image" (list . "api") | quote }} + imagePullPolicy: {{ .Values.images.api.pullPolicy }} + args: ["api"] + env: + - {name: HOME, value: /tmp} + - {name: HOST, value: 0.0.0.0} + - {name: PORT, value: "8080"} +{{- if eq .Values.profile "appliance" }} + - {name: HARMONY_MIGRATION_MODE, value: protected} + - name: BOOTSTRAP_DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.databaseSecretName" . }} + key: BOOTSTRAP_DATABASE_URL + - name: MIGRATION_DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.databaseSecretName" . }} + key: MIGRATION_DATABASE_URL +{{- else }} + - {name: HARMONY_MIGRATION_MODE, value: local} +{{- if eq .Values.profile "production" }} + - {name: APP_ENV, value: production} +{{- end }} + - name: MIGRATION_DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.databaseSecretName" . }} + key: {{ .Values.database.urlKey }} +{{- end }} + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.databaseSecretName" . }} + key: {{ .Values.database.urlKey }} + - {name: R2_ENDPOINT, value: {{ include "gitkb-forge.objectEndpoint" . | quote }}} + - {name: R2_BUCKET, value: {{ .Values.objectStore.bucket | quote }}} + - {name: R2_KNOWLEDGE_PREFIX, value: {{ .Values.objectStore.knowledgePrefix | quote }}} + - name: R2_ACCESS_KEY_ID + valueFrom: &accessKey + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.accessKeyIdKey }} + - name: R2_SECRET_ACCESS_KEY + valueFrom: &secretKey + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.secretAccessKeyKey }} + - name: AWS_ACCESS_KEY_ID + valueFrom: *accessKey + - name: AWS_SECRET_ACCESS_KEY + valueFrom: *secretKey + - name: WALGIT_TOKEN_MVP + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.tokenKey }} + - name: INVITATION_HMAC_SECRET + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.invitationHmacSecretKey }} + - {name: ZITADEL_ISSUER, value: {{ default "https://identity.invalid" .Values.identity.issuer | quote }}} + - {name: ZITADEL_PROJECT_ID, value: {{ default "gitkb-forge-appliance" .Values.identity.projectId | quote }}} +{{- if .Values.localAuth.enabled }} + - name: HARMONY_LOCAL_DEV_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: HARMONY_LOCAL_DEV_JWT_SECRET +{{- end }} + - {name: WAITLIST_ENABLED, value: {{ ternary "false" "true" .Values.localAuth.enabled | quote }}} + - {name: CONSOLE_URL, value: {{ include "gitkb-forge.publicUrl" . | quote }}} + - {name: INVITATION_ACCEPT_BASE_URL, value: {{ include "gitkb-forge.publicUrl" . | quote }}} + - {name: METRICS_ENABLED, value: "false"} + - {name: WALGIT_CONFIG, value: /etc/walgit/api.toml} + - {name: WALGIT__SERVER__LISTEN, value: "0.0.0.0:8081"} + ports: + - {name: harmony, containerPort: 8080, protocol: TCP} + - {name: git-read, containerPort: 8081, protocol: TCP} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + startupProbe: + tcpSocket: {port: harmony} + periodSeconds: 2 + failureThreshold: 90 + readinessProbe: + httpGet: {path: /ready, port: harmony} + periodSeconds: 5 + timeoutSeconds: 3 + livenessProbe: + httpGet: {path: /health, port: harmony} + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources.api | nindent 12 }} + volumeMounts: + - {name: git-config, mountPath: /etc/walgit, readOnly: true} + - {name: cache, mountPath: /var/lib/walgit} + - {name: tmp, mountPath: /tmp} + volumes: + - name: git-config + configMap: + name: {{ include "gitkb-forge.fullname" . }}-git-api + defaultMode: 0444 + - name: cache + emptyDir: + sizeLimit: {{ .Values.cache.sizeLimit | quote }} + - name: tmp + emptyDir: {} diff --git a/templates/deployment-edge.yaml b/templates/deployment-edge.yaml new file mode 100644 index 0000000..8ba690f --- /dev/null +++ b/templates/deployment-edge.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gitkb-forge.fullname" . }}-edge + labels: + {{- include "gitkb-forge.componentLabels" (list . "edge") | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "gitkb-forge.selectorLabels" (list . "edge") | nindent 6 }} + template: + metadata: + labels: + {{- include "gitkb-forge.componentLabels" (list . "edge") | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap-edge.yaml") . | sha256sum }} + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + containers: + - name: edge + image: {{ include "gitkb-forge.image" (list . "edge") | quote }} + imagePullPolicy: {{ .Values.images.edge.pullPolicy }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + startupProbe: + httpGet: {path: /, port: http} + periodSeconds: 2 + failureThreshold: 60 + readinessProbe: + httpGet: {path: /, port: http} + periodSeconds: 5 + timeoutSeconds: 3 + livenessProbe: + httpGet: {path: /, port: http} + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources.edge | nindent 12 }} + volumeMounts: + - name: config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + - name: tmp + mountPath: /tmp + - name: cache + mountPath: /var/cache/nginx + volumes: + - name: config + configMap: + name: {{ include "gitkb-forge.fullname" . }}-edge + defaultMode: 0444 + - name: tmp + emptyDir: {} + - name: cache + emptyDir: {} diff --git a/templates/deployment-site.yaml b/templates/deployment-site.yaml new file mode 100644 index 0000000..dd5f637 --- /dev/null +++ b/templates/deployment-site.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gitkb-forge.fullname" . }}-site + labels: + {{- include "gitkb-forge.componentLabels" (list . "site") | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "gitkb-forge.selectorLabels" (list . "site") | nindent 6 }} + template: + metadata: + labels: + {{- include "gitkb-forge.componentLabels" (list . "site") | nindent 8 }} + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + containers: + - name: site + image: {{ include "gitkb-forge.image" (list . "site") | quote }} + imagePullPolicy: {{ .Values.images.site.pullPolicy }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + startupProbe: + httpGet: {path: /, port: http} + periodSeconds: 2 + failureThreshold: 60 + readinessProbe: + httpGet: {path: /, port: http} + periodSeconds: 5 + timeoutSeconds: 3 + livenessProbe: + httpGet: {path: /, port: http} + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources.site | nindent 12 }} + volumeMounts: + - name: site + mountPath: /usr/share/nginx/html + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: site + configMap: + name: {{ include "gitkb-forge.fullname" . }}-site + defaultMode: 0444 + - name: tmp + emptyDir: {} diff --git a/templates/deployment-worker.yaml b/templates/deployment-worker.yaml new file mode 100644 index 0000000..41e883c --- /dev/null +++ b/templates/deployment-worker.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gitkb-forge.fullname" . }}-worker + labels: + {{- include "gitkb-forge.componentLabels" (list . "worker") | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "gitkb-forge.selectorLabels" (list . "worker") | nindent 6 }} + template: + metadata: + labels: + {{- include "gitkb-forge.componentLabels" (list . "worker") | nindent 8 }} + annotations: + checksum/git-config: {{ include (print $.Template.BasePath "/configmap-git-api.yaml") . | sha256sum }} + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: worker + image: {{ include "gitkb-forge.image" (list . "api") | quote }} + imagePullPolicy: {{ .Values.images.api.pullPolicy }} + args: ["worker"] + env: + - {name: HOME, value: /tmp} + - {name: WALGIT_CONFIG, value: /etc/walgit/worker.toml} + - {name: WALGIT__SERVER__LISTEN, value: "0.0.0.0:8082"} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.accessKeyIdKey }} + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.secretAccessKeyKey }} + - name: WALGIT_TOKEN_MVP + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.tokenKey }} +{{- if .Values.events.webhookUrl }} + - name: WALGIT__EVENTS__WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.eventWebhookSecretKey }} +{{- end }} + ports: + - {name: git-write, containerPort: 8082, protocol: TCP} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + startupProbe: + tcpSocket: {port: git-write} + periodSeconds: 2 + failureThreshold: 90 + readinessProbe: + httpGet: {path: /readyz, port: git-write} + periodSeconds: 5 + timeoutSeconds: 3 + livenessProbe: + httpGet: {path: /healthz, port: git-write} + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources.worker | nindent 12 }} + volumeMounts: + - {name: git-config, mountPath: /etc/walgit, readOnly: true} + - {name: cache, mountPath: /var/lib/walgit} + - {name: tmp, mountPath: /tmp} + volumes: + - name: git-config + configMap: + name: {{ include "gitkb-forge.fullname" . }}-git-api + defaultMode: 0444 + - name: cache + emptyDir: + sizeLimit: {{ .Values.cache.sizeLimit | quote }} + - name: tmp + emptyDir: {} diff --git a/templates/ingress.yaml b/templates/ingress.yaml new file mode 100644 index 0000000..d9454ad --- /dev/null +++ b/templates/ingress.yaml @@ -0,0 +1,32 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "gitkb-forge.fullname" . }} + labels: + {{- include "gitkb-forge.componentLabels" (list . "edge") | nindent 4 }} +{{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className | quote }} +{{- end }} + rules: + - host: {{ required "ingress.host is required when ingress is enabled" .Values.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "gitkb-forge.fullname" . }}-edge + port: + name: http +{{- if .Values.ingress.tlsSecretName }} + tls: + - hosts: [{{ .Values.ingress.host | quote }}] + secretName: {{ .Values.ingress.tlsSecretName | quote }} +{{- end }} +{{- end }} diff --git a/templates/job-buckets.yaml b/templates/job-buckets.yaml new file mode 100644 index 0000000..00d2515 --- /dev/null +++ b/templates/job-buckets.yaml @@ -0,0 +1,81 @@ +{{- if eq .Values.profile "appliance" }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "gitkb-forge.fullname" . }}-buckets{{ if .Release.IsUpgrade }}-upgrade{{ end }} + labels: + {{- include "gitkb-forge.backingLabels" (list . "bucket-init") | nindent 4 }} +{{- if .Release.IsUpgrade }} + annotations: + helm.sh/hook: pre-upgrade + helm.sh/hook-weight: "10" + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +{{- end }} +spec: + backoffLimit: 2 + ttlSecondsAfterFinished: 300 + template: + metadata: + labels: + {{- include "gitkb-forge.backingLabels" (list . "bucket-init") | nindent 8 }} + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: bucket-init + image: {{ include "gitkb-forge.image" (list . "bucketInit") | quote }} + imagePullPolicy: {{ .Values.images.bucketInit.pullPolicy }} + command: ["/bin/sh", "-ec"] + args: + - | + attempt=0 + until aws --endpoint-url "$OBJECT_ENDPOINT" s3api head-bucket --bucket "$OBJECT_BUCKET" >/dev/null 2>&1; do + attempt=$((attempt + 1)) + if aws --endpoint-url "$OBJECT_ENDPOINT" s3api create-bucket --bucket "$OBJECT_BUCKET" >/dev/null 2>&1; then + break + fi + if [ "$attempt" -ge 60 ]; then + echo "object-store bucket initialization timed out" >&2 + exit 1 + fi + sleep 2 + done + aws --endpoint-url "$OBJECT_ENDPOINT" s3api head-bucket --bucket "$OBJECT_BUCKET" >/dev/null + echo "object-store bucket is ready" + env: + - {name: HOME, value: /tmp} + - {name: AWS_EC2_METADATA_DISABLED, value: "true"} + - {name: AWS_DEFAULT_REGION, value: {{ .Values.objectStore.region | quote }}} + - {name: OBJECT_ENDPOINT, value: {{ include "gitkb-forge.objectEndpoint" . | quote }}} + - {name: OBJECT_BUCKET, value: {{ .Values.objectStore.bucket | quote }}} + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.accessKeyIdKey }} + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.secretAccessKeyKey }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + {{- toYaml .Values.resources.bucketInit | nindent 12 }} + volumeMounts: + - {name: tmp, mountPath: /tmp} + volumes: + - {name: tmp, emptyDir: {}} +{{- end }} diff --git a/templates/secret.yaml b/templates/secret.yaml new file mode 100644 index 0000000..4e4032a --- /dev/null +++ b/templates/secret.yaml @@ -0,0 +1,56 @@ +{{- if eq .Values.profile "appliance" }} +{{- $name := include "gitkb-forge.secretName" . -}} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name -}} +{{- $postgresPassword := randAlphaNum 40 -}} +{{- $accessKey := randAlphaNum 24 -}} +{{- $secretKey := randAlphaNum 48 -}} +{{- $gitToken := randAlphaNum 48 -}} +{{- $jwtSecret := randAlphaNum 48 -}} +{{- $invitationSecret := randAlphaNum 48 -}} +{{- $eventSecret := randAlphaNum 48 -}} +{{- $migrationPassword := randAlphaNum 40 -}} +{{- $runtimePassword := randAlphaNum 40 -}} +{{- if $existing }} +{{- $postgresPassword = (index $existing.data "POSTGRES_PASSWORD" | b64dec) -}} +{{- $accessKey = (index $existing.data "AWS_ACCESS_KEY_ID" | b64dec) -}} +{{- $secretKey = (index $existing.data "AWS_SECRET_ACCESS_KEY" | b64dec) -}} +{{- $gitToken = (index $existing.data "WALGIT_TOKEN_MVP" | b64dec) -}} +{{- $jwtSecret = (index $existing.data "HARMONY_LOCAL_DEV_JWT_SECRET" | b64dec) -}} +{{- $invitationSecret = (index $existing.data "INVITATION_HMAC_SECRET" | b64dec) -}} +{{- $eventSecret = (index $existing.data "WALGIT_EVENT_WEBHOOK_SECRET" | b64dec) -}} +{{- end }} +{{- if and $existing (hasKey $existing.data "HARMONY_MIGRATION_DATABASE_PASSWORD") }} +{{- $migrationPassword = (index $existing.data "HARMONY_MIGRATION_DATABASE_PASSWORD" | b64dec) -}} +{{- end }} +{{- if and $existing (hasKey $existing.data "HARMONY_RUNTIME_DATABASE_PASSWORD") }} +{{- $runtimePassword = (index $existing.data "HARMONY_RUNTIME_DATABASE_PASSWORD" | b64dec) -}} +{{- end }} +{{- $databaseHost := printf "%s-postgresql.%s.svc.cluster.local" (include "gitkb-forge.fullname" .) .Release.Namespace -}} +{{- $bootstrapDatabaseUrl := printf "postgres://%s:%s@%s:5432/%s" .Values.postgresql.username $postgresPassword $databaseHost .Values.postgresql.database -}} +{{- $migrationDatabaseUrl := printf "postgres://harmony_api_migrator:%s@%s:5432/%s" $migrationPassword $databaseHost .Values.postgresql.database -}} +{{- $databaseUrl := printf "postgres://harmony_app:%s@%s:5432/%s" $runtimePassword $databaseHost .Values.postgresql.database -}} +{{- $bootstrapSql := printf "CREATE ROLE harmony_api_migrator LOGIN PASSWORD '%s' NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;\nCREATE ROLE harmony_app LOGIN PASSWORD '%s' NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;\n" $migrationPassword $runtimePassword -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: + {{- include "gitkb-forge.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep +type: Opaque +data: + POSTGRES_PASSWORD: {{ $postgresPassword | b64enc | quote }} + BOOTSTRAP_DATABASE_URL: {{ $bootstrapDatabaseUrl | b64enc | quote }} + MIGRATION_DATABASE_URL: {{ $migrationDatabaseUrl | b64enc | quote }} + DATABASE_URL: {{ $databaseUrl | b64enc | quote }} + HARMONY_MIGRATION_DATABASE_PASSWORD: {{ $migrationPassword | b64enc | quote }} + HARMONY_RUNTIME_DATABASE_PASSWORD: {{ $runtimePassword | b64enc | quote }} + AWS_ACCESS_KEY_ID: {{ $accessKey | b64enc | quote }} + AWS_SECRET_ACCESS_KEY: {{ $secretKey | b64enc | quote }} + WALGIT_TOKEN_MVP: {{ $gitToken | b64enc | quote }} + HARMONY_LOCAL_DEV_JWT_SECRET: {{ $jwtSecret | b64enc | quote }} + INVITATION_HMAC_SECRET: {{ $invitationSecret | b64enc | quote }} + WALGIT_EVENT_WEBHOOK_SECRET: {{ $eventSecret | b64enc | quote }} + harmony-bootstrap.sql: {{ $bootstrapSql | b64enc | quote }} +{{- end }} diff --git a/templates/service-api.yaml b/templates/service-api.yaml new file mode 100644 index 0000000..a26ff54 --- /dev/null +++ b/templates/service-api.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitkb-forge.fullname" . }}-api + labels: + {{- include "gitkb-forge.componentLabels" (list . "api") | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "gitkb-forge.selectorLabels" (list . "api") | nindent 4 }} + ports: + - {name: harmony, port: 8080, targetPort: harmony, protocol: TCP} + - {name: git-read, port: 8081, targetPort: git-read, protocol: TCP} diff --git a/templates/service-edge.yaml b/templates/service-edge.yaml new file mode 100644 index 0000000..5433df0 --- /dev/null +++ b/templates/service-edge.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitkb-forge.fullname" . }}-edge + labels: + {{- include "gitkb-forge.componentLabels" (list . "edge") | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "gitkb-forge.selectorLabels" (list . "edge") | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/templates/service-postgresql.yaml b/templates/service-postgresql.yaml new file mode 100644 index 0000000..dff067a --- /dev/null +++ b/templates/service-postgresql.yaml @@ -0,0 +1,15 @@ +{{- if eq .Values.profile "appliance" }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitkb-forge.fullname" . }}-postgresql + labels: + {{- include "gitkb-forge.backingLabels" (list . "postgresql") | nindent 4 }} +spec: + clusterIP: None + publishNotReadyAddresses: true + selector: + {{- include "gitkb-forge.selectorLabels" (list . "postgresql") | nindent 4 }} + ports: + - {name: postgresql, port: 5432, targetPort: postgresql, protocol: TCP} +{{- end }} diff --git a/templates/service-rustfs.yaml b/templates/service-rustfs.yaml new file mode 100644 index 0000000..24aa619 --- /dev/null +++ b/templates/service-rustfs.yaml @@ -0,0 +1,14 @@ +{{- if eq .Values.profile "appliance" }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitkb-forge.fullname" . }}-rustfs + labels: + {{- include "gitkb-forge.backingLabels" (list . "rustfs") | nindent 4 }} +spec: + clusterIP: None + selector: + {{- include "gitkb-forge.selectorLabels" (list . "rustfs") | nindent 4 }} + ports: + - {name: s3, port: 9000, targetPort: s3, protocol: TCP} +{{- end }} diff --git a/templates/service-site.yaml b/templates/service-site.yaml new file mode 100644 index 0000000..4fa5e8c --- /dev/null +++ b/templates/service-site.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitkb-forge.fullname" . }}-site + labels: + {{- include "gitkb-forge.componentLabels" (list . "site") | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "gitkb-forge.selectorLabels" (list . "site") | nindent 4 }} + ports: + - {name: http, port: 8080, targetPort: http, protocol: TCP} diff --git a/templates/service-worker.yaml b/templates/service-worker.yaml new file mode 100644 index 0000000..da40471 --- /dev/null +++ b/templates/service-worker.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gitkb-forge.fullname" . }}-worker + labels: + {{- include "gitkb-forge.componentLabels" (list . "worker") | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "gitkb-forge.selectorLabels" (list . "worker") | nindent 4 }} + ports: + - {name: git-write, port: 8082, targetPort: git-write, protocol: TCP} diff --git a/templates/statefulset-postgresql.yaml b/templates/statefulset-postgresql.yaml new file mode 100644 index 0000000..dcdaf31 --- /dev/null +++ b/templates/statefulset-postgresql.yaml @@ -0,0 +1,100 @@ +{{- if eq .Values.profile "appliance" }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "gitkb-forge.fullname" . }}-postgresql + labels: + {{- include "gitkb-forge.backingLabels" (list . "postgresql") | nindent 4 }} +spec: + serviceName: {{ include "gitkb-forge.fullname" . }}-postgresql + replicas: 1 + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Retain + selector: + matchLabels: + {{- include "gitkb-forge.selectorLabels" (list . "postgresql") | nindent 6 }} + template: + metadata: + labels: + {{- include "gitkb-forge.backingLabels" (list . "postgresql") | nindent 8 }} + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 70 + runAsGroup: 70 + fsGroup: 70 + seccompProfile: + type: RuntimeDefault + containers: + - name: postgresql + image: {{ include "gitkb-forge.image" (list . "postgresql") | quote }} + imagePullPolicy: {{ .Values.images.postgresql.pullPolicy }} + env: + - {name: HOME, value: /tmp} + - {name: POSTGRES_DB, value: {{ .Values.postgresql.database | quote }}} + - {name: POSTGRES_USER, value: {{ .Values.postgresql.username | quote }}} + - {name: PGDATA, value: /var/lib/postgresql/data/pgdata} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: POSTGRES_PASSWORD + ports: + - {name: postgresql, containerPort: 5432, protocol: TCP} + securityContext: + runAsNonRoot: true + runAsUser: 70 + runAsGroup: 70 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + startupProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgresql.username | quote }}, "-d", {{ .Values.postgresql.database | quote }}] + periodSeconds: 2 + failureThreshold: 90 + readinessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgresql.username | quote }}, "-d", {{ .Values.postgresql.database | quote }}] + periodSeconds: 5 + livenessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgresql.username | quote }}, "-d", {{ .Values.postgresql.database | quote }}] + periodSeconds: 10 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources.postgresql | nindent 12 }} + volumeMounts: + - {name: data, mountPath: /var/lib/postgresql/data} + - {name: runtime, mountPath: /var/run/postgresql} + - {name: tmp, mountPath: /tmp} + - name: bootstrap + mountPath: /docker-entrypoint-initdb.d/00-harmony-roles.sql + subPath: harmony-bootstrap.sql + readOnly: true + volumes: + - {name: runtime, emptyDir: {}} + - {name: tmp, emptyDir: {}} + - name: bootstrap + secret: + secretName: {{ include "gitkb-forge.secretName" . }} + defaultMode: 0444 + items: + - {key: harmony-bootstrap.sql, path: harmony-bootstrap.sql} + volumeClaimTemplates: + - metadata: + name: data + labels: + {{- include "gitkb-forge.backingLabels" (list . "postgresql") | nindent 10 }} + spec: + accessModes: ["ReadWriteOnce"] +{{- if .Values.postgresql.storage.storageClass }} + storageClassName: {{ .Values.postgresql.storage.storageClass | quote }} +{{- end }} + resources: + requests: + storage: {{ .Values.postgresql.storage.size | quote }} +{{- end }} diff --git a/templates/statefulset-rustfs.yaml b/templates/statefulset-rustfs.yaml new file mode 100644 index 0000000..fa98650 --- /dev/null +++ b/templates/statefulset-rustfs.yaml @@ -0,0 +1,92 @@ +{{- if eq .Values.profile "appliance" }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "gitkb-forge.fullname" . }}-rustfs + labels: + {{- include "gitkb-forge.backingLabels" (list . "rustfs") | nindent 4 }} +spec: + serviceName: {{ include "gitkb-forge.fullname" . }}-rustfs + replicas: 1 + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Retain + selector: + matchLabels: + {{- include "gitkb-forge.selectorLabels" (list . "rustfs") | nindent 6 }} + template: + metadata: + labels: + {{- include "gitkb-forge.backingLabels" (list . "rustfs") | nindent 8 }} + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + fsGroup: 10001 + seccompProfile: + type: RuntimeDefault + containers: + - name: rustfs + image: {{ include "gitkb-forge.image" (list . "rustfs") | quote }} + imagePullPolicy: {{ .Values.images.rustfs.pullPolicy }} + env: + - {name: HOME, value: /tmp} + - {name: RUSTFS_VOLUMES, value: /data} + - name: RUSTFS_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.accessKeyIdKey }} + - name: RUSTFS_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.secretAccessKeyKey }} + ports: + - {name: s3, containerPort: 9000, protocol: TCP} + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + startupProbe: + httpGet: {path: /health/live, port: s3} + periodSeconds: 2 + failureThreshold: 90 + readinessProbe: + httpGet: {path: /health/live, port: s3} + periodSeconds: 5 + timeoutSeconds: 3 + livenessProbe: + httpGet: {path: /health/live, port: s3} + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources.rustfs | nindent 12 }} + volumeMounts: + - {name: data, mountPath: /data} + - {name: logs, mountPath: /logs} + - {name: tmp, mountPath: /tmp} + volumes: + - {name: logs, emptyDir: {}} + - {name: tmp, emptyDir: {}} + volumeClaimTemplates: + - metadata: + name: data + labels: + {{- include "gitkb-forge.backingLabels" (list . "rustfs") | nindent 10 }} + spec: + accessModes: ["ReadWriteOnce"] +{{- if .Values.rustfs.storage.storageClass }} + storageClassName: {{ .Values.rustfs.storage.storageClass | quote }} +{{- end }} + resources: + requests: + storage: {{ .Values.rustfs.storage.size | quote }} +{{- end }} diff --git a/templates/tests/smoke.yaml b/templates/tests/smoke.yaml new file mode 100644 index 0000000..6e132f8 --- /dev/null +++ b/templates/tests/smoke.yaml @@ -0,0 +1,93 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "gitkb-forge.fullname" . }}-smoke + labels: + {{- include "gitkb-forge.labels" . | nindent 4 }} + app.kubernetes.io/component: smoke + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: smoke + image: {{ include "gitkb-forge.image" (list . "api") | quote }} + imagePullPolicy: {{ .Values.images.api.pullPolicy }} + command: ["/bin/sh", "-ec"] + args: + - | + set +x + base={{ include "gitkb-forge.publicUrl" . | quote }} + curl -fsS "$base/health" >/dev/null + curl -fsS "$base/ready" >/dev/null + + askpass=$(mktemp) + work=$(mktemp -d) + clone=$(mktemp -d) + cleanup() { rm -rf "$askpass" "$work" "$clone"; } + trap cleanup EXIT + cat >"$askpass" <<'ASKPASS' + #!/bin/sh + case "$1" in + *Username*) printf '%s\n' mvp-admin ;; + *) printf '%s\n' "$WALGIT_TOKEN_MVP" ;; + esac + ASKPASS + chmod 700 "$askpass" + export GIT_ASKPASS="$askpass" + export GIT_TERMINAL_PROMPT=0 + export GIT_CONFIG_NOSYSTEM=1 + + git -C "$work" init -q --initial-branch=main + git -C "$work" config user.name 'GitKB Forge Helm test' + git -C "$work" config user.email 'forge-helm-test@gitkb.invalid' + git -C "$work" config commit.gpgsign false + printf 'GitKB Forge Helm smoke\n' >"$work/README.md" + git -C "$work" add README.md + tree=$(git -C "$work" write-tree) + oid=$(printf '%s\n' 'deterministic helm smoke commit' | \ + GIT_AUTHOR_NAME='GitKB Forge Helm test' \ + GIT_AUTHOR_EMAIL='forge-helm-test@gitkb.invalid' \ + GIT_AUTHOR_DATE='2026-08-24T00:00:00+0000' \ + GIT_COMMITTER_NAME='GitKB Forge Helm test' \ + GIT_COMMITTER_EMAIL='forge-helm-test@gitkb.invalid' \ + GIT_COMMITTER_DATE='2026-08-24T00:00:00+0000' \ + git -C "$work" commit-tree "$tree") + git -C "$work" update-ref refs/heads/main "$oid" + remote="$base/helm-test/{{ include "gitkb-forge.fullname" . }}.git" + git -C "$work" remote add origin "$remote" + git -C "$work" push -q origin HEAD:refs/heads/main + git clone -q "$remote" "$clone" + test "$(git -C "$clone" rev-parse HEAD)" = "$oid" + git -C "$clone" fsck --strict + printf 'helm smoke passed oid=%s\n' "$oid" + env: + - {name: HOME, value: /tmp} + - name: WALGIT_TOKEN_MVP + valueFrom: + secretKeyRef: + name: {{ include "gitkb-forge.secretName" . }} + key: {{ .Values.objectStore.tokenKey }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: {cpu: 50m, memory: 64Mi} + limits: {cpu: 500m, memory: 512Mi} + volumeMounts: + - {name: tmp, mountPath: /tmp} + volumes: + - {name: tmp, emptyDir: {}} diff --git a/tests/render_test.yaml b/tests/render_test.yaml new file mode 100644 index 0000000..a679537 --- /dev/null +++ b/tests/render_test.yaml @@ -0,0 +1,263 @@ +suite: four-container GitKB Forge render contract +templates: + - templates/secret.yaml + - templates/configmap-edge.yaml + - templates/configmap-site.yaml + - templates/configmap-git-api.yaml + - templates/deployment-edge.yaml + - templates/deployment-site.yaml + - templates/deployment-api.yaml + - templates/deployment-worker.yaml + - templates/service-edge.yaml + - templates/service-site.yaml + - templates/service-api.yaml + - templates/service-worker.yaml + - templates/statefulset-postgresql.yaml + - templates/service-postgresql.yaml + - templates/statefulset-rustfs.yaml + - templates/service-rustfs.yaml + - templates/job-buckets.yaml + - templates/ingress.yaml + - templates/tests/smoke.yaml + +tests: + - it: renders exactly the four product workload shapes + asserts: + - isKind: {of: Deployment} + template: templates/deployment-edge.yaml + - equal: + path: metadata.labels["gitkb.io/workload"] + value: product + template: templates/deployment-edge.yaml + - equal: + path: spec.template.spec.containers[0].name + value: edge + template: templates/deployment-edge.yaml + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: true + template: templates/deployment-edge.yaml + - equal: + path: spec.template.spec.automountServiceAccountToken + value: false + template: templates/deployment-edge.yaml + - equal: + path: spec.template.spec.containers[0].name + value: site + template: templates/deployment-site.yaml + - equal: + path: spec.template.spec.containers[0].name + value: api + template: templates/deployment-api.yaml + - equal: + path: spec.template.spec.containers[0].args[0] + value: api + template: templates/deployment-api.yaml + - equal: + path: spec.template.spec.containers[0].ports[1].containerPort + value: 8081 + template: templates/deployment-api.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: WALGIT__SERVER__LISTEN + value: 0.0.0.0:8081 + template: templates/deployment-api.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: HARMONY_MIGRATION_MODE + value: protected + template: templates/deployment-api.yaml + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /ready + template: templates/deployment-api.yaml + - equal: + path: spec.template.spec.containers[0].name + value: worker + template: templates/deployment-worker.yaml + - equal: + path: spec.template.spec.containers[0].args[0] + value: worker + template: templates/deployment-worker.yaml + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 8082 + template: templates/deployment-worker.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: WALGIT__SERVER__LISTEN + value: 0.0.0.0:8082 + template: templates/deployment-worker.yaml + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /readyz + template: templates/deployment-worker.yaml + + - it: renders persistent appliance backing without forbidden services + asserts: + - isKind: {of: Secret} + template: templates/secret.yaml + - equal: + path: metadata.annotations["helm.sh/resource-policy"] + value: keep + template: templates/secret.yaml + - isNotNull: + path: data.BOOTSTRAP_DATABASE_URL + template: templates/secret.yaml + - isNotNull: + path: data.MIGRATION_DATABASE_URL + template: templates/secret.yaml + - isNotNull: + path: data.DATABASE_URL + template: templates/secret.yaml + - isKind: {of: StatefulSet} + template: templates/statefulset-postgresql.yaml + - equal: + path: metadata.labels["gitkb.io/workload"] + value: backing + template: templates/statefulset-postgresql.yaml + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: bootstrap + mountPath: /docker-entrypoint-initdb.d/00-harmony-roles.sql + subPath: harmony-bootstrap.sql + readOnly: true + template: templates/statefulset-postgresql.yaml + - isKind: {of: StatefulSet} + template: templates/statefulset-rustfs.yaml + - equal: + path: metadata.labels["gitkb.io/workload"] + value: backing + template: templates/statefulset-rustfs.yaml + - isKind: {of: Job} + template: templates/job-buckets.yaml + - equal: + path: metadata.name + value: RELEASE-NAME-gitkb-forge-buckets + template: templates/job-buckets.yaml + - notExists: + path: metadata.annotations["helm.sh/hook"] + template: templates/job-buckets.yaml + - notExists: + path: spec.template.spec.containers[0].env[5].valueFrom.secretKeyRef.optional + template: templates/job-buckets.yaml + - hasDocuments: {count: 0} + template: templates/ingress.yaml + + - it: verifies the bucket before an in-place upgrade + release: + upgrade: true + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-gitkb-forge-buckets-upgrade + template: templates/job-buckets.yaml + - equal: + path: metadata.annotations["helm.sh/hook"] + value: pre-upgrade + template: templates/job-buckets.yaml + - equal: + path: metadata.annotations["helm.sh/hook-delete-policy"] + value: before-hook-creation,hook-succeeded + template: templates/job-buckets.yaml + + - it: renders deterministic routing and split Git roles + asserts: + - matchRegex: + path: data["nginx.conf"] + pattern: git-receive-pack + template: templates/configmap-edge.yaml + - matchRegex: + path: data["nginx.conf"] + pattern: proxy_request_buffering off + template: templates/configmap-edge.yaml + - notMatchRegex: + path: data["nginx.conf"] + pattern: \$http_authorization.*log_format + template: templates/configmap-edge.yaml + - matchRegex: + path: data["api.toml"] + pattern: roles = \["serve"\] + template: templates/configmap-git-api.yaml + - matchRegex: + path: data["worker.toml"] + pattern: roles = \["serve", "maintain", "events"\] + template: templates/configmap-git-api.yaml + - matchRegex: + path: data["api.toml"] + pattern: prefix = "git" + template: templates/configmap-git-api.yaml + + - it: injects the optional event secret only into the worker + set: + events.webhookUrl: https://events.example.invalid/walgit + asserts: + - matchRegex: + path: data["worker.toml"] + pattern: webhook_url + template: templates/configmap-git-api.yaml + - notMatchRegex: + path: data["worker.toml"] + pattern: webhook_secret + template: templates/configmap-git-api.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: WALGIT__EVENTS__WEBHOOK_SECRET + valueFrom: + secretKeyRef: + name: RELEASE-NAME-gitkb-forge-secrets + key: WALGIT_EVENT_WEBHOOK_SECRET + template: templates/deployment-worker.yaml + + - it: removes bundled backing and local auth from production + set: + profile: production + localAuth.enabled: false + database.existingSecret: forge-database + objectStore.endpoint: https://account.r2.cloudflarestorage.com + objectStore.existingSecret: forge-object-store + identity.issuer: https://identity.example.com + identity.projectId: forge + images.edge.digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + images.site.digest: sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + images.api.digest: sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + asserts: + - hasDocuments: {count: 0} + template: templates/secret.yaml + - hasDocuments: {count: 0} + template: templates/statefulset-postgresql.yaml + - hasDocuments: {count: 0} + template: templates/statefulset-rustfs.yaml + - hasDocuments: {count: 0} + template: templates/job-buckets.yaml + - notContains: + path: spec.template.spec.containers[0].env + content: + name: HARMONY_LOCAL_DEV_JWT_SECRET + any: true + template: templates/deployment-api.yaml + - equal: + path: spec.template.spec.containers[0].image + value: ghcr.io/gitkb/gitkb-forge-api@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + template: templates/deployment-api.yaml + - equal: + path: spec.template.spec.containers[0].image + value: ghcr.io/gitkb/gitkb-forge-api@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + template: templates/deployment-worker.yaml + + - it: renders ingress only with an explicit host + set: + ingress.enabled: true + ingress.host: forge.example.com + asserts: + - isKind: {of: Ingress} + template: templates/ingress.yaml + - equal: + path: spec.rules[0].host + value: forge.example.com + template: templates/ingress.yaml diff --git a/values.schema.json b/values.schema.json new file mode 100644 index 0000000..dccdeef --- /dev/null +++ b/values.schema.json @@ -0,0 +1,235 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GitKB Forge values", + "type": "object", + "additionalProperties": false, + "required": [ + "profile", + "nameOverride", + "fullnameOverride", + "images", + "service", + "ingress", + "database", + "postgresql", + "objectStore", + "rustfs", + "identity", + "localAuth", + "events", + "cache", + "resources" + ], + "properties": { + "profile": {"type": "string", "enum": ["appliance", "production"]}, + "nameOverride": {"type": "string"}, + "fullnameOverride": {"type": "string"}, + "images": { + "type": "object", + "additionalProperties": false, + "required": ["edge", "site", "api", "postgresql", "rustfs", "bucketInit"], + "properties": { + "edge": {"$ref": "#/definitions/image"}, + "site": {"$ref": "#/definitions/image"}, + "api": {"$ref": "#/definitions/image"}, + "postgresql": {"$ref": "#/definitions/image"}, + "rustfs": {"$ref": "#/definitions/image"}, + "bucketInit": {"$ref": "#/definitions/image"} + } + }, + "service": { + "type": "object", + "additionalProperties": false, + "required": ["type", "port"], + "properties": { + "type": {"type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"]}, + "port": {"type": "integer", "minimum": 1, "maximum": 65535} + } + }, + "ingress": { + "type": "object", + "additionalProperties": false, + "required": ["enabled", "className", "annotations", "host", "tlsSecretName"], + "properties": { + "enabled": {"type": "boolean"}, + "className": {"type": "string"}, + "annotations": {"type": "object"}, + "host": {"type": "string"}, + "tlsSecretName": {"type": "string"} + } + }, + "database": { + "type": "object", + "additionalProperties": false, + "required": ["existingSecret", "urlKey"], + "properties": { + "existingSecret": {"type": "string"}, + "urlKey": {"type": "string", "minLength": 1} + } + }, + "postgresql": { + "type": "object", + "additionalProperties": false, + "required": ["database", "username", "storage"], + "properties": { + "database": {"type": "string", "minLength": 1}, + "username": {"type": "string", "minLength": 1}, + "storage": {"$ref": "#/definitions/storage"} + } + }, + "objectStore": { + "type": "object", + "additionalProperties": false, + "required": [ + "endpoint", + "bucket", + "region", + "forcePathStyle", + "knowledgePrefix", + "gitPrefix", + "existingSecret", + "accessKeyIdKey", + "secretAccessKeyKey", + "tokenKey", + "eventWebhookSecretKey", + "invitationHmacSecretKey" + ], + "properties": { + "endpoint": {"type": "string"}, + "bucket": {"type": "string", "minLength": 1}, + "region": {"type": "string", "minLength": 1}, + "forcePathStyle": {"const": true}, + "knowledgePrefix": {"type": "string", "minLength": 1}, + "gitPrefix": {"type": "string", "minLength": 1}, + "existingSecret": {"type": "string"}, + "accessKeyIdKey": {"type": "string", "minLength": 1}, + "secretAccessKeyKey": {"type": "string", "minLength": 1}, + "tokenKey": {"type": "string", "minLength": 1}, + "eventWebhookSecretKey": {"type": "string", "minLength": 1}, + "invitationHmacSecretKey": {"type": "string", "minLength": 1} + } + }, + "rustfs": { + "type": "object", + "additionalProperties": false, + "required": ["storage"], + "properties": {"storage": {"$ref": "#/definitions/storage"}} + }, + "identity": { + "type": "object", + "additionalProperties": false, + "required": ["issuer", "projectId"], + "properties": { + "issuer": {"type": "string"}, + "projectId": {"type": "string"} + } + }, + "localAuth": { + "type": "object", + "additionalProperties": false, + "required": ["enabled"], + "properties": {"enabled": {"type": "boolean"}} + }, + "events": { + "type": "object", + "additionalProperties": false, + "required": ["webhookUrl"], + "properties": {"webhookUrl": {"type": "string"}} + }, + "cache": { + "type": "object", + "additionalProperties": false, + "required": ["sizeLimit"], + "properties": {"sizeLimit": {"type": "string", "minLength": 1}} + }, + "resources": { + "type": "object", + "additionalProperties": false, + "required": ["edge", "site", "api", "worker", "postgresql", "rustfs", "bucketInit"], + "properties": { + "edge": {"$ref": "#/definitions/resources"}, + "site": {"$ref": "#/definitions/resources"}, + "api": {"$ref": "#/definitions/resources"}, + "worker": {"$ref": "#/definitions/resources"}, + "postgresql": {"$ref": "#/definitions/resources"}, + "rustfs": {"$ref": "#/definitions/resources"}, + "bucketInit": {"$ref": "#/definitions/resources"} + } + } + }, + "definitions": { + "image": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "tag", "digest", "pullPolicy"], + "properties": { + "repository": {"type": "string", "minLength": 1}, + "tag": {"type": "string"}, + "digest": {"type": "string", "pattern": "^$|^sha256:[a-f0-9]{64}$"}, + "pullPolicy": {"type": "string", "enum": ["Always", "IfNotPresent", "Never"]} + } + }, + "storage": { + "type": "object", + "additionalProperties": false, + "required": ["size", "storageClass"], + "properties": { + "size": {"type": "string", "minLength": 1}, + "storageClass": {"type": "string"} + } + }, + "resources": { + "type": "object", + "additionalProperties": false, + "required": ["requests", "limits"], + "properties": { + "requests": {"$ref": "#/definitions/resourcePair"}, + "limits": {"$ref": "#/definitions/resourcePair"} + } + }, + "resourcePair": { + "type": "object", + "additionalProperties": false, + "required": ["cpu", "memory"], + "properties": { + "cpu": {"type": ["string", "number"]}, + "memory": {"type": "string", "minLength": 1} + } + } + }, + "allOf": [ + { + "if": {"properties": {"ingress": {"properties": {"enabled": {"const": true}}}}}, + "then": {"properties": {"ingress": {"properties": {"host": {"minLength": 1}}}}} + }, + { + "if": {"properties": {"profile": {"const": "production"}}}, + "then": { + "properties": { + "database": {"properties": {"existingSecret": {"minLength": 1}}}, + "objectStore": { + "properties": { + "endpoint": {"minLength": 1}, + "existingSecret": {"minLength": 1}, + "region": {"const": "auto"} + } + }, + "identity": { + "properties": { + "issuer": {"minLength": 1}, + "projectId": {"minLength": 1} + } + }, + "localAuth": {"properties": {"enabled": {"const": false}}}, + "images": { + "properties": { + "edge": {"properties": {"digest": {"pattern": "^sha256:[a-f0-9]{64}$"}}}, + "site": {"properties": {"digest": {"pattern": "^sha256:[a-f0-9]{64}$"}}}, + "api": {"properties": {"digest": {"pattern": "^sha256:[a-f0-9]{64}$"}}} + } + } + } + } + } + ] +} diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..59c77fb --- /dev/null +++ b/values.yaml @@ -0,0 +1,113 @@ +profile: appliance + +nameOverride: "" +fullnameOverride: "" + +images: + edge: + repository: docker.io/nginxinc/nginx-unprivileged + tag: "1.29.1-alpine" + digest: sha256:27985295bdb22a1ef8f712863210bd5877c0f3006494a593e86b3fe0fa55467e + pullPolicy: IfNotPresent + site: + repository: docker.io/nginxinc/nginx-unprivileged + tag: "1.29.1-alpine" + digest: sha256:27985295bdb22a1ef8f712863210bd5877c0f3006494a593e86b3fe0fa55467e + pullPolicy: IfNotPresent + api: + repository: ghcr.io/gitkb/gitkb-forge-api + tag: "0.1.0" + digest: "" + pullPolicy: IfNotPresent + postgresql: + repository: docker.io/library/postgres + tag: "17.6-alpine" + digest: sha256:ef257d85f76e48da1c64832459b59fcaba1a4dac97bf5d7450c77753542eee94 + pullPolicy: IfNotPresent + rustfs: + repository: docker.io/rustfs/rustfs + tag: latest + digest: sha256:41fe89380f4120a337790c02af192c3fe7bb55c3edc2e6e9357b487b47c6ab21 + pullPolicy: IfNotPresent + bucketInit: + repository: docker.io/amazon/aws-cli + tag: "2.31.17" + digest: sha256:91163ddc4883b9318ba191e687c2d32f0eb0e6854ed722cb8c6172303dacb2e5 + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + host: "" + tlsSecretName: "" + +database: + existingSecret: "" + urlKey: DATABASE_URL + +postgresql: + database: gitkb + username: gitkb + storage: + size: 2Gi + storageClass: "" + +objectStore: + endpoint: "" + bucket: gitkb-forge + region: auto + forcePathStyle: true + knowledgePrefix: knowledge + gitPrefix: git + existingSecret: "" + accessKeyIdKey: AWS_ACCESS_KEY_ID + secretAccessKeyKey: AWS_SECRET_ACCESS_KEY + tokenKey: WALGIT_TOKEN_MVP + eventWebhookSecretKey: WALGIT_EVENT_WEBHOOK_SECRET + invitationHmacSecretKey: INVITATION_HMAC_SECRET + +rustfs: + storage: + size: 2Gi + storageClass: "" + +identity: + issuer: "" + projectId: "" + +localAuth: + enabled: true + +events: + webhookUrl: "" + +cache: + sizeLimit: 4Gi + +resources: + edge: + requests: {cpu: 50m, memory: 64Mi} + limits: {cpu: 250m, memory: 128Mi} + site: + requests: {cpu: 25m, memory: 32Mi} + limits: {cpu: 100m, memory: 64Mi} + api: + requests: {cpu: 250m, memory: 512Mi} + limits: {cpu: "1", memory: 1Gi} + worker: + requests: {cpu: 250m, memory: 512Mi} + limits: {cpu: "1", memory: 1Gi} + postgresql: + requests: {cpu: 100m, memory: 256Mi} + limits: {cpu: "1", memory: 1Gi} + rustfs: + requests: {cpu: 100m, memory: 256Mi} + limits: {cpu: "1", memory: 1Gi} + bucketInit: + requests: {cpu: 25m, memory: 64Mi} + limits: {cpu: 250m, memory: 256Mi}