docs: reconcile to as-built (ADR-0008, deploy runbook); clean text hero on detail pages
All checks were successful
CI / ci (push) Successful in 1m56s
All checks were successful
CI / ci (push) Successful in 1m56s
- ADR-0008: private package, External-Secrets pull over public HTTPS (no k3s restart); CI auth via CI_TOKEN PAT; Playwright dropped from CI. Marks ADR-0007 partially superseded. - gitops-deployment-strategy + deployment.md updated to as-built flow + operate/troubleshoot runbook - content-deviations: detail-page heroes are text-only (no image) - service/focus detail pages: clean text hero (drop shared photo/icon)
This commit is contained in:
parent
99d4a3bea9
commit
fc64909f3d
@ -3,6 +3,12 @@
|
||||
**Status:** Accepted — 2026-07-15 · **Supersedes** the GitHub Actions + GHCR specifics of
|
||||
[ADR-0001](0001-gitops-with-argocd.md) (the ArgoCD/GitOps model itself stands).
|
||||
|
||||
> ⚠️ **Partially superseded by [ADR-0008](0008-private-package-external-secrets-pull.md).** The
|
||||
> registry-visibility (public → **private**), image-pull path (registries.yaml/k3s-restart →
|
||||
> **public-HTTPS pull, no restart**), CI auth (auto token → **`CI_TOKEN` PAT**), and pipeline
|
||||
> (**Playwright e2e removed from CI**) below were changed once built. The "CI on Gitea Actions,
|
||||
> images in Gitea Packages" core still holds. Read ADR-0008 for the as-built details.
|
||||
|
||||
## Context
|
||||
|
||||
The repo lives on the homelab's self-hosted **Gitea** (`bmr_bluecap/bcs-website`), not GitHub, so
|
||||
|
||||
49
docs/adr/0008-private-package-external-secrets-pull.md
Normal file
49
docs/adr/0008-private-package-external-secrets-pull.md
Normal file
@ -0,0 +1,49 @@
|
||||
# ADR-0008: Private package, External-Secrets pull over public HTTPS (no k3s restart)
|
||||
|
||||
**Status:** Accepted — 2026-07-17 · **Supersedes** the registry-visibility, image-pull, CI-auth,
|
||||
and pipeline specifics of [ADR-0007](0007-gitea-actions-and-packages.md). (ADR-0007's core — CI on
|
||||
Gitea Actions, images in Gitea Packages — still stands; only the details below changed once we
|
||||
actually built and debugged it.)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0007 proposed a **public** package pulled by k3s from the internal HTTP registry
|
||||
(`10.66.15.22:3000`), which required `registries.yaml` on every node + a **k3s restart**, and CI
|
||||
auth via the runner's automatic token. In practice:
|
||||
- The automatic Gitea Actions token **lacked reliable package-write scope** → `docker login`
|
||||
returned `unauthorized`.
|
||||
- k3s trusting an HTTP registry means a **rolling cluster restart** — unacceptable disruption for
|
||||
a static site, and the user wanted zero risk to the (still-live) old site and other workloads.
|
||||
- The user chose a **private** package (least exposure) managed via the existing **External
|
||||
Secrets** + OpenBao stack.
|
||||
- Testing showed a cluster pod **can reach the registry over public HTTPS** (`git.7tl-homelab.com`,
|
||||
valid cert) — so no node-level registry config is needed at all.
|
||||
|
||||
## Decision
|
||||
|
||||
- **Package is PRIVATE.** (Gitea user packages are private by default; no public/anon pull.)
|
||||
- **Split push/pull, same Gitea package, two access paths:**
|
||||
- **CI pushes** to the **internal** endpoint `10.66.15.22:3000/bmr_bluecap/bcs-website` (HTTP,
|
||||
fast, no edge). The runner host's dockerd trusts it via `insecure-registries` (managed in the
|
||||
gitea Ansible role + SIGHUP live-reload — no container restart).
|
||||
- **k3s pulls** the **public HTTPS** endpoint `git.7tl-homelab.com/bmr_bluecap/bcs-website`
|
||||
(valid TLS) → **no `registries.yaml`, no node insecure config, no k3s restart.**
|
||||
- `.gitea/workflows/ci.yml` uses `PUSH_IMAGE` (internal) and `DEPLOY_IMAGE` (public); the tag
|
||||
bump / k8s image ref use `DEPLOY_IMAGE`.
|
||||
- **Pull auth via External Secrets:** `k8s/externalsecret-registry.yaml` (ClusterSecretStore
|
||||
`openbao-cluster`, OpenBao KV v2 mount `cluster`, path `bluecap-registry` → fields
|
||||
`username`/`password`) materializes a `kubernetes.io/dockerconfigjson` secret `gitea-registry`;
|
||||
the Deployment references it via `imagePullSecrets`. No static secret in Git.
|
||||
- **CI auth via a dedicated PAT** `CI_TOKEN` (repo Actions secret; scopes `write:package` +
|
||||
`write:repository`) for both the registry push and the tag-bump commit-back — not the auto token.
|
||||
- **Playwright e2e removed from CI** (heaviest step, headless Chrome, unneeded for a static site).
|
||||
CI runs `npm ci` → build (typecheck) → `seo:lint`. Run e2e locally: `npm run test:e2e`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **No cluster restart, no per-node registry config** — deploys never disturb k3s.
|
||||
- Two tokens by role (least privilege): `CI_TOKEN` (write) as a repo secret; a separate
|
||||
`read:package` PAT in OpenBao for cluster pulls.
|
||||
- The internal-push insecure-registry trust is the one host-level bit (gitea role), needed only on
|
||||
the runner host, applied by live-reload.
|
||||
- Verified 2026-07-17: full loop green, 2/2 pods pulling the private image over HTTPS and serving.
|
||||
@ -14,4 +14,5 @@ Format: Status · Context · Decision · Consequences. Statuses: Proposed · Acc
|
||||
| [0004](0004-reuse-monylog-for-monitoring.md) | Reuse the monylog stack for ops monitoring (not Uptime Kuma) | Accepted |
|
||||
| [0005](0005-analytics-posthog-and-umami-trial.md) | Visitor analytics: PostHog + Umami side-by-side trial | Accepted |
|
||||
| [0006](0006-launch-insights-blog.md) | Launch a blog at /insights/ | Accepted |
|
||||
| [0007](0007-gitea-actions-and-packages.md) | CI on Gitea Actions, images in Gitea Packages | Accepted |
|
||||
| [0007](0007-gitea-actions-and-packages.md) | CI on Gitea Actions, images in Gitea Packages | Accepted (registry/pull/auth specifics superseded by 0008) |
|
||||
| [0008](0008-private-package-external-secrets-pull.md) | Private package, External-Secrets pull over public HTTPS (no k3s restart) | Accepted |
|
||||
|
||||
@ -17,6 +17,15 @@ migration to Markdown collections (ADR-0002).
|
||||
Areas" section — the only live copy that exists for these (the live `/focus-areas/` page is empty).
|
||||
No additional copy was invented.
|
||||
|
||||
## Detail-page heroes — no hero image (2026-07-17)
|
||||
|
||||
- Service and focus-area **detail** pages use a **clean text hero** (eyebrow + title), **no hero
|
||||
image**. This matches the originals, which had no hero photo on those pages. An interim version
|
||||
put a shared photo (looked repetitive) then the per-service 64px icon (too small / orphaned) — both
|
||||
rejected; the text hero is the final call. Service **icons remain on the `/services/` overview
|
||||
cards**, at their intended small size. The `/services/`, `/focus-areas/`, and About pages keep
|
||||
their real photos.
|
||||
|
||||
## Metadata (not visible body copy)
|
||||
|
||||
- **SEO meta descriptions** (`description` frontmatter, 120–158 chars) are newly written marketing
|
||||
|
||||
@ -30,41 +30,54 @@ docker build -t bluecap-strategies-website:latest .
|
||||
docker run --rm -p 8080:80 bluecap-strategies-website:latest
|
||||
```
|
||||
|
||||
## k3s
|
||||
## k3s (as-built — deployed & serving in-cluster since 2026-07-17)
|
||||
|
||||
Production deployment should use the pull-based GitOps model documented in
|
||||
`docs/gitops-deployment-strategy.md`.
|
||||
|
||||
The intended flow is:
|
||||
Live GitOps flow (all working; see `docs/gitops-deployment-strategy.md` + ADR-0001/0007/0008):
|
||||
|
||||
```text
|
||||
local edit -> git push -> GitHub Actions test/build/publish -> Git manifest image tag update -> ArgoCD reconciles k3s
|
||||
local edit -> git push (Gitea main) -> Gitea Actions: build + seo:lint -> push image (internal
|
||||
10.66.15.22:3000) -> bump DEPLOY_IMAGE tag + commit back [skip ci] -> ArgoCD reconciles k3s ->
|
||||
k3s pulls git.7tl-homelab.com/... (private, ES pull secret, public HTTPS) -> pods serve
|
||||
```
|
||||
|
||||
ArgoCD is already running in the homelab k3s cluster, so it is the reconciler (not Flux). GitHub
|
||||
Actions should not receive kubeconfig or direct access to the homelab cluster.
|
||||
- ArgoCD is the reconciler (not Flux); the CI runner never gets kubeconfig or cluster access.
|
||||
- **Not yet public** — the ingress names `bluecapstrategies.com`/`www` but DNS still points at the
|
||||
old WordPress site. Going public = the DNS Cutover below, a deliberate step. See the
|
||||
`keep-old-site-live` rule: do nothing that affects the old site until cutover.
|
||||
|
||||
Manual `kubectl` use should be limited to bootstrapping the ArgoCD Application, read-only
|
||||
inspection, and emergency operations. If you intentionally choose to apply the current manifests
|
||||
by hand during a controlled bootstrap test, the command would be:
|
||||
### Operate
|
||||
|
||||
```bash
|
||||
kubectl apply -k k8s/
|
||||
# Review the running site locally (no cluster/DNS impact):
|
||||
kubectl -n bluecap-strategies port-forward svc/bluecap-website 18080:80 # then http://localhost:18080
|
||||
# or purely off-cluster from source: npm run dev
|
||||
|
||||
# Health / status:
|
||||
kubectl -n argocd get application bcs-website # Synced / Healthy
|
||||
kubectl -n bluecap-strategies get pods,deploy # 2/2 Running
|
||||
kubectl -n bluecap-strategies get externalsecret gitea-registry # SecretSynced
|
||||
|
||||
# Rollback: `git revert` the tag-bump commit and push (ArgoCD reconciles), or use ArgoCD history.
|
||||
```
|
||||
|
||||
Do not treat that as the normal production deployment process.
|
||||
### Troubleshoot (things that actually bit us)
|
||||
|
||||
The manifests assume Traefik ingress is available. Read-only research confirmed Traefik and
|
||||
cert-manager are installed, with a ready `letsencrypt-prod` ClusterIssuer. However, TLS and ingress
|
||||
hardening should remain documented rather than applied until the Pangolin/Newt edge path is
|
||||
confirmed.
|
||||
- **CI job never runs / "no matching runner"** → runner offline or mis-scoped. Must be a **global**
|
||||
(owner_id=0) runner on `gitea_network` with instance URL `http://gitea:3000`; token must be an
|
||||
**instance-level** registration token (Site Admin → Actions → Runners).
|
||||
- **Push: "server gave HTTP response to HTTPS client"** → runner host dockerd missing
|
||||
`insecure-registries: ["10.66.15.22:3000"]` (gitea Ansible role handles it).
|
||||
- **Push: `unauthorized`** → `CI_TOKEN` repo secret missing/insufficient (`write:package` + `write:repository`).
|
||||
- **Pod `ImagePullBackOff`** → ExternalSecret not synced (check OpenBao `cluster/bluecap-registry`
|
||||
fields) or the `read:package` PAT.
|
||||
- **`git push` rejected (fetch first)** → CI committed a tag bump to `main`; `git pull --rebase` first
|
||||
(`git config pull.rebase true`).
|
||||
|
||||
Items to confirm before production ingress changes:
|
||||
Manual `kubectl apply -k k8s/` is for emergency/bootstrap only, not normal deploys.
|
||||
|
||||
- Whether Pangolin/Newt terminates TLS, passes TLS through, or proxies HTTPS to Traefik.
|
||||
- Whether Pangolin/Newt preserves `Host` headers for the BlueCap hostnames.
|
||||
- Whether cert-manager's Cloudflare DNS-01 token can issue certificates for `bluecapstrategies.com`.
|
||||
- Whether the canonical production host should be apex or `www`.
|
||||
Ingress/TLS items to confirm before cutover: whether Pangolin/Newt terminates/passes/proxies TLS
|
||||
to Traefik and preserves `Host` headers; whether cert-manager's Cloudflare DNS-01 token can issue
|
||||
for `bluecapstrategies.com`; whether the canonical host is apex or `www`.
|
||||
|
||||
## DNS Cutover
|
||||
|
||||
|
||||
@ -30,22 +30,27 @@ pulls from Git and the image registry.
|
||||
> (`10.66.15.22:3000/bmr_bluecap/bcs-website`); ArgoCD Application in `argocd/bcs-website-application.yaml`.
|
||||
> Any lingering GitHub/GHCR wording below is the generic model; substitute Gitea/Gitea-Packages.
|
||||
|
||||
## Prerequisites
|
||||
## Prerequisites & as-built registry flow
|
||||
|
||||
Done: repo pushed to Gitea (`bmr_bluecap/bcs-website`, private); `k8s/deployment.yaml` image set to
|
||||
the Gitea Packages path; ArgoCD Application manifest written.
|
||||
> **As built (2026-07-17) — see [ADR-0008](adr/0008-private-package-external-secrets-pull.md).**
|
||||
> The earlier "k3s trusts the HTTP registry + restart" plan was dropped. Final flow avoids any k3s
|
||||
> restart or per-node config.
|
||||
|
||||
**Still to do before the first deploy works (homelab infra):**
|
||||
- **k3s must trust the HTTP registry.** Add a `registries.yaml` entry for `10.66.15.22:3000` as
|
||||
insecure/HTTP (via the k3s role `k3s_registry_mirrors`), then restart k3s — otherwise pods can't
|
||||
pull the image.
|
||||
- **Runner host dockerd** needs `10.66.15.22:3000` in `insecure-registries` to push over HTTP.
|
||||
- **Docker usable from job containers** (act_runner socket passthrough) — or switch the build to
|
||||
kaniko.
|
||||
- **Package visibility:** make the container package **public** (simplest; image is only the public
|
||||
site), or keep it private and add an imagePullSecret (`k8s/deployment.yaml`), ideally via the
|
||||
installed External Secrets operator.
|
||||
- Apply the ArgoCD Application (once) or add it to the homelab-gitops app-of-apps.
|
||||
**Registry flow (split push/pull, same private Gitea package, two access paths):**
|
||||
- **CI pushes** to the **internal** `10.66.15.22:3000/bmr_bluecap/bcs-website` (HTTP, fast). The
|
||||
runner host's dockerd trusts it via `insecure-registries` (gitea Ansible role + SIGHUP
|
||||
live-reload — no restart). Docker in jobs works because act_runner's config pins jobs to
|
||||
`gitea_network` and exposes the docker socket.
|
||||
- **k3s pulls** the **public HTTPS** `git.7tl-homelab.com/bmr_bluecap/bcs-website` (valid TLS) →
|
||||
**no `registries.yaml`, no node config, no k3s restart.** (`k3s_registry_mirrors` stays empty.)
|
||||
- **Pull auth:** package is **private**; a `read:package` PAT lives in OpenBao (`cluster/bluecap-registry`)
|
||||
and External Secrets (`k8s/externalsecret-registry.yaml`) materializes the `gitea-registry`
|
||||
dockerconfigjson used by the Deployment's `imagePullSecrets`.
|
||||
- **CI auth:** dedicated PAT `CI_TOKEN` (repo Actions secret; `write:package` + `write:repository`).
|
||||
|
||||
**One-time setup (all done):** repo on Gitea; runner registered (global/instance, on `gitea_network`,
|
||||
correct instance URL); `CI_TOKEN` secret; OpenBao `cluster/bluecap-registry`; ArgoCD Application
|
||||
applied. Deploys are now push-to-main → CI → ArgoCD, hands-off.
|
||||
|
||||
## Cluster Findings
|
||||
|
||||
|
||||
@ -5,22 +5,19 @@ type Props = {
|
||||
lead?: string;
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
const {
|
||||
eyebrow,
|
||||
title,
|
||||
lead,
|
||||
image = "/assets/live-site/img_0067.jpg",
|
||||
imageAlt = "Water resources and coastal landscape",
|
||||
} = Astro.props;
|
||||
const { eyebrow, title, lead, image, imageAlt = "Water resources and coastal landscape", icon } = Astro.props;
|
||||
const hasPhoto = Boolean(image);
|
||||
---
|
||||
|
||||
<section class="page-hero">
|
||||
<section class:list={["page-hero", { "page-hero--compact": !hasPhoto }]}>
|
||||
<div class="hero-copy">
|
||||
{icon && <img class="hero-icon" src={icon} alt="" aria-hidden="true" width="64" height="64" loading="eager" />}
|
||||
{eyebrow && <p class="eyebrow">{eyebrow}</p>}
|
||||
<h1>{title}</h1>
|
||||
{lead && <p>{lead}</p>}
|
||||
</div>
|
||||
<img src={image} alt={imageAlt} width="1280" height="854" loading="eager" />
|
||||
{hasPhoto && <img src={image} alt={imageAlt} width="1280" height="854" loading="eager" />}
|
||||
</section>
|
||||
|
||||
@ -21,7 +21,9 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { entry, kind, parentTitle, parentHref } = Astro.props;
|
||||
const { title, description, image } = entry.data;
|
||||
// Detail pages get a clean text hero (eyebrow + title) — the originals had no hero image, and a
|
||||
// tiny 64px icon reads as orphaned filler. Icons still live on the /services/ overview cards.
|
||||
const { title, description } = entry.data;
|
||||
const pathname = `/${entry.slug}/`;
|
||||
const isService = kind === "Service";
|
||||
const { Content } = await entry.render();
|
||||
@ -38,7 +40,7 @@ const jsonLd = [
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description} pathname={pathname} jsonLd={jsonLd}>
|
||||
<PageHero eyebrow={kind} title={title} image={image ?? "/assets/live-site/img_0070.jpg"} />
|
||||
<PageHero eyebrow={kind} title={title} />
|
||||
<section class="section">
|
||||
<div class="detail-layout">
|
||||
<div class="article-body">
|
||||
|
||||
@ -419,6 +419,23 @@ p {
|
||||
padding: 3.5rem 0;
|
||||
}
|
||||
|
||||
/* Detail pages (services/focus): no hero photo — single column, with an optional icon accent. */
|
||||
.page-hero--compact {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
/* More specific than `.page-hero img` so the 64px icon isn't stretched to full width. */
|
||||
.page-hero .hero-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
aspect-ratio: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user