Phase 3 lane 1 of FlowerCore.Shared.Indexing rollout — wires the new search consumer in FlowerCore.Intranet.Web to live infrastructure. Manifest changes: - Image bump: localhost/fc-intranet-web:latest -> :v202604240040search. Built from FlowerCore.Intranet.Web@feat/shared-indexing-search and imported into all three RKE2 nodes (rke2-server, rke2-agent1, rke2-agent2) via ctr import. Both :latest and :v202604240040search tags are present. - New PersistentVolumeClaim intranet-vector-store (1Gi, ReadWriteOnce, Longhorn) mounted at /data for the SQLite vector store (intranet-vectors.db). - New emptyDir volume notes-corpus (1Gi sizeLimit) shared between the init container and main container, mounted at /srv/flowercore-notes (read-only in the main container). - New init container clone-notes-corpus (alpine/git) that shallow-clones https://github.com/astoltz/FlowerCore.Notes.git (codex/notes-pimanager-live-drift) into /srv/flowercore-notes on every pod start. Re-clone is cheap (depth=1) and re-runs of git fetch + reset --hard are idempotent. - Strategy switched to Recreate for the deployment, since the new RWO PVC blocks rolling updates — see CLAUDE.md memory "RWO PVC blocks K8s rolling updates". - Resource bumps: memory 128Mi -> 256Mi req, 512Mi -> 1Gi limit; CPU 500m -> 1000m limit. The DocsCorpusIndexer + Ollama HTTP calls add measurable load during the initial index build. - initialDelaySeconds bumps on both probes (10s -> 30s liveness, 5s -> 10s readiness) to account for startup-time Ollama probing and the slightly larger image. The DocsCorpusIndexer waits 15s after host startup before its first indexing pass, then loops every RescanInterval (default 1h). Its first run will: 1. Embed all *.md under /srv/flowercore-notes/docs against nomic-embed-text on edge1 (10.0.57.17:11434). 2. Embed all *.html under /srv/flowercore-notes/docs/dashboards. 3. Persist chunks + embeddings to /data/intranet-vectors.db. Verify after rollout: - kubectl -n intranet logs deploy/intranet-web -c clone-notes-corpus (init container should show the docs/ listing). - kubectl -n intranet logs deploy/intranet-web -f (DocsCorpusIndexer should log "Indexing docs root 'notes-md'..." then "Docs root 'notes-md' indexed: N files, M chunks, M stored"). - curl -sk https://intranet.iamworkin.lan/api/search/indexes -> ["notes-html","notes-md"] - curl -sk 'https://intranet.iamworkin.lan/api/search?q=guacamole+single+host&topK=3' -> hits from docs/infrastructure/guacamole-customization-plan.md Companion source on FlowerCore.Intranet.Web@feat/shared-indexing-search. Depends on FlowerCore.Common@feat/shared-indexing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bluejay-infra
Infrastructure manifests for ArgoCD. An ApplicationSet in argocd namespace watches the apps/* directories in this repo and creates one Application per subdir (prefixed infra-<name>).
Adding a new service to the cluster
Follow these steps in order. Step 1 must run before step 3 — if you skip it, cert-manager HTTP-01 will silently fail for ~2h per cert (exponential backoff) until someone diagnoses the DNS.
1. Create or verify the FlowerCore.DNS A record (REQUIRED for current HTTP-01 manifests)
step-ca (the ACME CA on noc1) runs in a Podman container with host networking. Its container resolver uses pfSense Unbound (10.0.56.1), not cluster CoreDNS. So even though CoreDNS has a wildcard *.iamworkin.lan → 10.0.56.200 for in-cluster lookups, step-ca cannot see it. Every new public hostname needs an explicit pfSense host override.
The management path is now FlowerCore.DNS, not FlowerCore.Notes/scripts/pfsense-add-dns-overrides.py. Add or verify the public A record there before you apply the manifest:
curl -sk https://dns.iamworkin.lan/api/v1/servers
# Find the pfSense serverId, then create the record using the host label only.
# Example: for foo.iamworkin.lan, use "name":"foo".
curl -sk -X POST https://dns.iamworkin.lan/api/v1/servers/<serverId>/zones/iamworkin.lan/records \
-H "Content-Type: application/json" \
-d '{"name":"<yourservice>","type":"A","data":"10.0.56.200","ttl":300}'
Verify all referenced iamworkin.lan hosts resolve (run from anywhere on LAN):
python scripts/check-pfsense-dns.py
# Historical filename retained. The script now calls
# https://dns.iamworkin.lan/api/v1/zones/iamworkin.lan/resolve-preflight
# for every Certificate dnsName and Traefik Host(...) rule it finds.
python scripts/check-pfsense-dns.py --live
# Optional stronger pass when kubectl access is available; also checks
# live-cluster Certificates and IngressRoutes for drift outside manifests.
Symptom if you skip this: the Certificate resource stays Ready: False with status.reason: unexpected non-ACME API error: context deadline exceeded. Recovery requires kubectl -n <ns> delete order <order-name> after adding the DNS to bypass cert-manager's backoff.
2. Create the app manifest
Create apps/<name>/<name>.yaml containing the Namespace, Deployment, Service, Certificate, and IngressRoute. Reference an existing directory (e.g. apps/fc-messageboard/) for the canonical shape.
Conventions:
Namespacehas labelapp.kubernetes.io/part-of: bluejay-infraDeployment.spec.selector.matchLabelsandService.spec.selectorMUST use the same label key. The historical convention here isapp: <name>(notapp.kubernetes.io/name) — don't mix.- Image:
localhost/<name>:v<YYYYMMDD><HHMM>,imagePullPolicy: Never. Import the image to every RKE2 node (server + both agents) viactr images importbefore applying — pods schedule anywhere. - If the app persists local state (SQLite, uploads), declare the
PersistentVolumeClaimhere withstorageClassName: longhornandaccessModes: [ReadWriteOnce]. Addstrategy.type: Recreateto the Deployment — RWO PVC blocks rolling updates. - Probes: use
tcpSocketif the app has middleware that intercepts unauth requests (returns 404/401 for/health). Otherwise preferhttpGetagainst whatever the app exposes (verify the path isn't gated by auth). - Certificate:
issuerRef.name: step-ca-acme,issuerRef.kind: ClusterIssuer.dnsNamesmust match the hostname you created in FlowerCore.DNS in step 1.
3. Commit & push
git add apps/<name>/
git commit -m "<name>: initial deployment"
git push
ArgoCD's ApplicationSet picks up the new directory within ~3 minutes and creates infra-<name> with auto-sync + self-heal enabled.
4. Verify
# From noc1
fcadmin_ssh noc1 '
kubectl -n argocd get application infra-<name>
kubectl -n <ns> get certificate,pod
curl -sk -m 8 -o /dev/null -w "HTTP %{http_code}\n" https://<name>.iamworkin.lan/
'
Certificate should be Ready: True within ~60s. If it stalls False for >2m, the pfSense DNS step got skipped — go back to step 1, then kubectl -n <ns> delete order <order-name> to bust the backoff.
Pre-merge gate
Before git push, always run:
python scripts/check-pfsense-dns.py
It's a quick service-backed check that would have caught the entire 2026-04-22 cert-manager outage. Consider wiring it into a pre-commit hook or a Gitea Actions workflow.
Retiring a service
kubectl -n argocd delete application infra-<name>(cascade deletes the K8s resources via ArgoCD finalizers)git rm -r apps/<name>/and push- Remove the FlowerCore.DNS record through the UI or API, for example:
curl -sk https://dns.iamworkin.lan/api/v1/servers
curl -sk -X DELETE https://dns.iamworkin.lan/api/v1/servers/<serverId>/zones/iamworkin.lan/records/<yourservice>
Known gotchas
- CoreDNS template + ndots:5 collision: inside pods,
<svc>.<ns>.svc.cluster.localwith <5 dots gets search-expanded throughiamworkin.lanFIRST and hits the wildcard template → resolves to Traefik VIP, not the real ClusterIP. Use short service names (<svc>) in K8s manifests. See memoryfeedback_coredns_ndots_template_collision.md. - Image not on node: pods stuck
ErrImageNeverPullmeans the image wasn't imported to the node Kubernetes scheduled the pod onto.ctr images importon all of rke2-server, rke2-agent1, rke2-agent2. - StatefulSet PVC drift:
volumeClaimTemplatesneeds explicitvolumeMode: Filesystemor ArgoCD SSA self-heals forever. See memoryfeedback_argocd_statefulset_pvc_drift.md. - ArgoCD must use internal Gitea URL:
http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git, not the external HTTPS URL (step-ca cert isn't trusted by ArgoCD). TheApplicationSetand any hand-createdApplicationmust both use the internal URL.
References
- Cert-manager recovery playbook:
FlowerCore.Notes/memory/project_cert_manager_recovery_2026_04_22.md - Why pfSense DNS is required:
FlowerCore.Notes/memory/feedback_pfsense_dns_required_for_acme.md - Public DNS operator host:
https://dns.iamworkin.lan - Canonical credential helper:
FlowerCore.Notes/scripts/credential-helper.sh - pfSense admin automation:
FlowerCore.Notes/memory/feedback_pfsense_automation.md