diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..95f0e68 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,42 @@ +# Deploying this site + +**`loom.hypertheory-labs.dev`, on the home cluster.** + +**No container image is built.** *Two public images — `nginx` and `git-sync` — +and the site arrives over git.* **`git-sync` follows the `site` branch of this +repository**, *which holds `dist/` and nothing else*, **and `nginx` serves what +lands in the shared volume.** + +> **This deliberately sidesteps the cluster's open gap on +> [publishing container images](https://git.hypertheory-labs.dev/jeffry/homelab-cluster/src/branch/main/.loom/gaps/publishing-container-images.md).** +> *Sidestepping is not closing:* **the first workload here that genuinely needs +> an image we built will still hit it**, *and this repository is not evidence +> that the gap is smaller than it looked.* + +## Publishing a new version + +```sh +npm run build +npm run publish-site # commits dist/ to the `site` branch and pushes +``` + +*`git-sync` polls, and `nginx` serves the new files without a restart —* **there +is no rollout, because there is no image.** + +## What is here + +| | | +|---|---| +| `namespace.yaml` | the `loom` namespace | +| `deployment.yaml` | `nginx` + `git-sync`, sharing an `emptyDir` | +| `service.yaml` | ClusterIP on 80 | +| `httproute.yaml` | pins to the `loom` listener and **carries no hostname of its own** | + +**The `Certificate` and the Gateway listener are not here** — *they are cluster +infrastructure and live in `homelab-impl`, beside the other four.* + +## Why the route carries no hostname + +*Because `public-access.md` says so:* **a route that carries its own `hostnames:` +stops inheriting from the listener and stops being portable between staging and +production.** *It pins by `sectionName` and takes the hostname it is given.* diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml new file mode 100644 index 0000000..8076f8d --- /dev/null +++ b/deploy/deployment.yaml @@ -0,0 +1,67 @@ +# The site is served from a git branch, not from an image we built. +# +# git-sync clones the `site` branch of loom/docs into a shared emptyDir and +# polls it; nginx serves whatever is there. Both images are public, which is +# what lets this deploy while the cluster's container-registry gap is open. +# +# Anonymous clone: loom/docs is a public repository. Nothing here holds a +# credential, and a private repository would need one — which is the same +# unanswered question the registry gap names. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: docs + namespace: loom +spec: + replicas: 1 + selector: + matchLabels: { app: docs } + template: + metadata: + labels: { app: docs } + spec: + securityContext: + fsGroup: 65533 + volumes: + - name: site + emptyDir: {} + - name: nginx-conf + configMap: { name: docs-nginx } + initContainers: + # --one-time, so the pod is not Ready until the site is actually on + # disk. Without this nginx serves 404s for the first few seconds after + # every reschedule. + - name: git-sync-init + image: registry.k8s.io/git-sync/git-sync:v4.4.0 + args: + - --repo=https://git.hypertheory-labs.dev/loom/docs.git + - --ref=site + - --root=/site + - --link=current + - --one-time + volumeMounts: + - { name: site, mountPath: /site } + containers: + - name: git-sync + image: registry.k8s.io/git-sync/git-sync:v4.4.0 + args: + - --repo=https://git.hypertheory-labs.dev/loom/docs.git + - --ref=site + - --root=/site + - --link=current + - --period=60s + volumeMounts: + - { name: site, mountPath: /site } + - name: nginx + image: nginx:1.29-alpine + ports: + - { containerPort: 8080, name: http } + volumeMounts: + - { name: site, mountPath: /site, readOnly: true } + - { name: nginx-conf, mountPath: /etc/nginx/conf.d } + readinessProbe: + httpGet: { path: /, port: http } + initialDelaySeconds: 2 + resources: + requests: { cpu: 10m, memory: 32Mi } + limits: { memory: 128Mi } diff --git a/deploy/httproute.yaml b/deploy/httproute.yaml new file mode 100644 index 0000000..4bb9c32 --- /dev/null +++ b/deploy/httproute.yaml @@ -0,0 +1,17 @@ +# Carries no hostname of its own, on purpose: it pins to the `loom` listener by +# sectionName and inherits the hostname from it. A route with its own +# `hostnames:` stops inheriting and stops being portable between environments. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: docs + namespace: loom +spec: + parentRefs: + - name: traefik-gateway + namespace: traefik + sectionName: loom + rules: + - backendRefs: + - name: docs + port: 80 diff --git a/deploy/namespace.yaml b/deploy/namespace.yaml new file mode 100644 index 0000000..671632b --- /dev/null +++ b/deploy/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: loom diff --git a/deploy/nginx-conf.yaml b/deploy/nginx-conf.yaml new file mode 100644 index 0000000..a071602 --- /dev/null +++ b/deploy/nginx-conf.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: docs-nginx + namespace: loom +data: + default.conf: | + server { + listen 8080; + # git-sync keeps `current` pointed at the checked-out worktree, so this + # path follows a new commit without nginx restarting. + root /site/current/dist; + index index.html; + + # Astro emits directory-style URLs: /bedrock/starting/ -> .../index.html + location / { + try_files $uri $uri/ $uri/index.html =404; + } + + # Hashed assets are immutable; pages are not, and a cached page is how a + # corrected document keeps reading as true. + location /_astro/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + error_page 404 /404.html; + } diff --git a/deploy/service.yaml b/deploy/service.yaml new file mode 100644 index 0000000..e2be60e --- /dev/null +++ b/deploy/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: docs + namespace: loom +spec: + selector: { app: docs } + ports: + - { name: http, port: 80, targetPort: http } diff --git a/package.json b/package.json index e7476d8..c5843a7 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dev": "npm run generate && astro dev", "build": "npm run generate && astro build", "preview": "astro preview", - "check": "node scripts/generate.mjs --strict" + "check": "node scripts/generate.mjs --strict", + "publish-site": "bash scripts/publish-site.sh" }, "dependencies": { "@astrojs/starlight": "^0.36.0", diff --git a/scripts/publish-site.sh b/scripts/publish-site.sh new file mode 100755 index 0000000..c57d761 --- /dev/null +++ b/scripts/publish-site.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Put dist/ on the `site` branch, which holds build output and nothing else. +# git-sync follows that branch; there is no image and no rollout. +set -euo pipefail +cd "$(dirname "$0")/.." +[ -d dist ] || { echo "no dist/ — run npm run build first" >&2; exit 1; } +SHA=$(git rev-parse --short HEAD) +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +git worktree add -q --detach "$WORK" +cd "$WORK" +git checkout -q --orphan site +git rm -rq --cached . 2>/dev/null || true +find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} + +mkdir dist && cp -R "$OLDPWD/dist/." dist/ +git add -A +git commit -q -m "site: built from $SHA" +git push -qf origin site +cd "$OLDPWD" +git worktree remove --force "$WORK" +echo "published site branch from $SHA" diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 4d7a7da..8870160 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -2,6 +2,9 @@ title: loom description: Conventions for holding what a repository knows, so somebody arriving can find it. template: splash +head: + - tag: title + content: loom — conventions for what a repository knows hero: tagline: Conventions for holding what a repository knows — so that whoever arrives next, person or agent, can find it. actions: