Compare commits

...

11 Commits

Author SHA1 Message Date
Andrew Stoltz
eaba7cd171 fc-desktop: add phase 1 capacity guards 2026-05-20 15:49:20 -05:00
Andrew Stoltz
65aa1e6104 fix(monitoring): point probe-printweb at /health (Q-MR-90)
Root path requires API key auth — `/` returned 401 to the blackbox
probe, firing PrintWebDown despite `/health` reporting Healthy.
Pattern: feedback_k8s_probes_behind_auth_middleware.

Mirrors FlowerCore.Notes scripts/monitoring/prometheus.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 14:52:02 -05:00
Andrew Stoltz
7f2a3b76b4 feat(github-runner): bake Ruby 3.3 into Linux self-hosted runner image (Q-MR-81) 2026-05-20 11:45:43 -05:00
ea73f00461 fix(fc-devicemgmt): remove self-referential Application resource (Q-MR-79)
ApplicationSet already creates infra-fc-devicemgmt; removing the in-repo Application child clears the self-reference drift.
2026-05-20 16:20:01 +00:00
Andrew Stoltz
25ace30a03 fix(fc-devicemgmt): remove self-referential Application resource (Q-MR-79) 2026-05-20 11:18:25 -05:00
Andrew Stoltz
ca574c2280 brochure: delete apps/brochure/ — full prune per operator decision 2026-05-19
Removes the apps/brochure/ directory entirely from the bluejay-infra
ApplicationSet glob. ArgoCD will:

  1. See infra-brochure has no git source -> mark for delete
  2. Prune the brochure namespace + Deployment + Service + Certificate
     + Secret + IngressRoute (all generated from the now-gone
     apps/brochure/brochure.yaml)
  3. Remove the infra-brochure Application from argocd ns

Operator decision 2026-05-19 (follow-up to 09387f9 ARCHIVED banner
commit): "Yes, prune argo for brochure. Probably fully deleted there."

The brochure subdomain project was a planning-chain misinterpretation
of "make TtsReader + AI Station production-ready" — see
memory/project_brochure_split_misinterpretation_archived_2026_05_19.md
in FlowerCore.Notes for the full decision record.

Reusable artifacts that were the operator's archive concern stay alive
in their actual homes:

- FlowerCore.Intranet.Web PR #8 content-NuGet carve-out: still in
  Intranet's master, may transfer to TtsReader / AI Station prod work
- Sprint 32 Cl-5 substrate (public-twin design ideas): SUPERSEDED banner
  in-place in FlowerCore.Notes docs/standards/, history preserved
- magpie-doc-writer + wren-walkthrough skill output: unchanged in
  Intranet's flowercore-whats-new/walkthroughs/galleries directories

Companion Notes-side commit updates the "scaled to 0 + ARCHIVED banner"
language in mvp-readiness.html + fleet-roadmap-2026-05-19-sprint36-v2.md
+ memory record to reflect full deletion instead.

Wrong-codebase image localhost/fc-brochure-web:v20260524-sprint32 is
being removed from rke2-server / rke2-agent1 / rke2-agent2 in a
follow-up step (reclaims ~800MB per node).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:42:30 -05:00
Andrew Stoltz
09387f90e1 brochure: ARCHIVED 2026-05-19 — was a misinterpretation, do not re-enable
The brochure split project was a misinterpretation of an operator request
to make TtsReader + AI Station production-ready. Somewhere in the planning
chain it spun up into a separate "showcase brochure product" with its own
host, repo, NuGet, and Codex pack — none of which the operator actually
wanted. The project itself is pointless and a waste of credits.

Archive (not delete) per operator decision 2026-05-19, because some work
shipped under the misinterpretation may still have reusable value:

- FlowerCore.Intranet.Web PR #8 (merged) introduced FlowerCore.Brochure.Content
  content-NuGet carve-out — pattern may apply to TtsReader/AiStation production
  polish.
- Sprint 32 Cl-5 substrate has design ideas for public-twin vs operator-host
  separation that may transfer.
- magpie-doc-writer / wren-walkthrough skills still author useful Intranet
  content — those skills stay active.

These manifests stay at replicas: 0 for ArgoCD continuity. Cleanup options
(move out of apps/* glob, or delete entirely) are documented in README.md
for an operator-explicit future call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:34:28 -05:00
Andrew Stoltz
e641ceab48 monitoring(irc-notify): criticals also batch hourly — fix per-fire spam
The first batching pass (bacac06) left critical-severity alerts on the
immediate-print path. That's still per-event spam for any persistent
critical (e.g. PrintPaperRollCritical fires every 30s Grafana evaluation
cycle when paper is <5%). Caught immediately after deploy: CUPS queue grew
0 → 8 jobs in 8 minutes from a single firing PrintPaperRollCritical.

This commit aligns with the operator's verbatim ask ("one alert an hour"):

- Critical-severity alerts now go into the digest buffer, NOT the
  immediate-print path. The digest payload already shows severity tags
  per alertname, so the operator still sees "[critical] X" in the printout.
- The explicit `alert_channel=thermal_print_immediate` label still bypasses
  batching, but only on NEW fingerprint arrival — it triggers a flush of
  the CURRENT digest (with the new alert included), then clears. Repeat
  webhooks for the same fingerprint dedupe in the buffer until the next
  hourly tick OR until the alert resolves. No fingerprint can spam.
- `add_to_digest` now returns bool (True = buffer grew, False = dedup /
  resolution / disabled) so the immediate-label path can flush only on
  state transitions.

Net effect: max 1 thermal print per BATCH_INTERVAL_MIN per alert fingerprint,
regardless of severity. Rules that genuinely need same-second paper opt in
via `alert_channel=thermal_print_immediate` (currently zero rules use this).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:22:25 -05:00
Andrew Stoltz
c263426ea5 fc-devicemgmt: operator image fix + Web scaled to 0
OPERATOR (PodCrashLoopBackOff cleared):
- Bumped image to v20260519-sp34cl3-fix (built from astoltz/FlowerCore.DeviceManagement@d9a3685
  after Sprint 34 Cl-3 stranded branch was merged via PR #19 squash).
- The v20260512-cx5 image was the broken Sprint 8 scaffold: generic Host
  builder, no kubeops, no Kestrel on :8080, no AddController chain. Readiness
  probe dial-tcp 8080 failed every restart.
- The new image ships the AddController chain for all 4 reconcilers
  (DeviceCrd / DeviceGroupCrd / DevicePolicyCrd / RemoteCommandCrd) plus
  Kestrel on :8080 and /healthz.
- Image saved + scp'd + ctr-imported on rke2-server / rke2-agent1 / rke2-agent2
  before this commit. SHA256: 2cc79ee0a2313c550268d1244f805ae41b396362148dd5603061cc15b6f7fa7e

WEB (DeploymentReplicasMismatch cleared via scale-to-0):
- Web pod cannot start. Two upstream gaps must close first:
  1) MySQL DB instance + user `fc_devicemgmt` / database `flowercore_devicemgmt`
     are not provisioned in fc-mysql. Cluster has zero MySqlInstanceCrds and
     no `mysql.fc-mysql.svc:3306` Service.
  2) 1Password vault item `IAmWorkin/FlowerCore DeviceManagement Runtime` is
     missing (5 fields: DB-Password + 4 mTLS PEMs). OnePasswordItem CRD has
     been stuck Ready=False since 2026-05-18T02:58.
- Same pattern as the brochure-web scale-to-0 in 914fed0 — make the cluster
  clean and quiet, let operator restart deploy on a real schedule.

Re-enable path is fully documented in the deployment-web.yaml header comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:11:09 -05:00
Andrew Stoltz
bacac067cf monitoring(irc-notify): hourly digest batching for thermal printer
The thermal printer drained overnight (2026-05-18/19) because the old
notify.py POSTed one print job per Grafana webhook fire. With 9
concurrently-firing alerts (zabbix-postgres + fc-devicemgmt + brochure
+ PrintPaperRollLow), every evaluation cycle stamped fresh CUPS jobs
onto the queue until the operator physically powered the printer off.

This refactor:

- Adds env-var config: THERMAL_PRINT_ENABLED (master kill switch),
  BATCH_INTERVAL_MIN (default 60), BATCH_MAX_PENDING (default 50).
- IRC delivery stays per-event (operator wants the live stream).
- Thermal routing now:
  * critical/disaster/page severity OR alert_channel=thermal_print_immediate
    -> print immediately
  * alert_channel=thermal_print -> enqueue into hourly digest
  * RESOLVED -> remove from digest buffer (no resolution-spam prints)
  * else -> IRC only, no thermal
- Background digest_loop thread flushes the buffer hourly (or sooner
  if buffer hits BATCH_MAX_PENDING). Digest payload is a single
  Print.Web /api/print/alert POST listing distinct alertnames + per-rule
  target counts.
- New POST /flush endpoint (manual operator force-flush; useful for
  testing without waiting an hour).
- GET / returns config + buffer depth + per-stat counters for observability.

Net effect: max 1 thermal print per BATCH_INTERVAL_MIN for batched
warnings, plus immediate prints for criticals. Closes the 2026-05-18/19
alert-storm incident.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:56:14 -05:00
914fed08d8 fix(brochure): scale brochure-web to 0 — wrong codebase shipped (Intranet.Web binary in fc-brochure-web image, CrashLoopBackOff 296 restarts on /data read-only). Re-enable after Sprint 34 Cx-3 rebuild per docs/ai-agents/codex-prompts/2026-05-18-fc-brochure-web-rebuild-pack.md 2026-05-19 14:45:01 +00:00
15 changed files with 988 additions and 428 deletions

View File

@@ -1,27 +0,0 @@
# FlowerCore Brochure
`apps/brochure` hosts the public brochure split from `FlowerCore.Intranet.Web`.
ArgoCD's `apps/*` ApplicationSet will create `infra-brochure` after this
directory lands on `main`.
## Runtime
- Host: `https://brochure.flowercore.io`
- Namespace: `brochure`
- Deployment: `brochure-web`
- Image: `localhost/fc-brochure-web:v20260524-sprint32`
- Port: `8080`
- Public route method allowlist: `GET` and `HEAD`
## Operator Actions
1. Publish and import `localhost/fc-brochure-web:v20260524-sprint32` to every
RKE2 node before sync, using the same podman save + `ctr images import`
flow as the Intranet deployment.
2. Create the Cloudflare DNS record for `brochure.flowercore.io` pointing at
the FlowerCore public edge.
3. Verify `infra-brochure` appears in ArgoCD, the certificate becomes Ready,
and `GET https://brochure.flowercore.io/` returns `200`.
The route intentionally does not expose `/ops/*` or `/admin/*`; the Brochure
web app returns `404` for those paths and Traefik only forwards read methods.

View File

@@ -1,131 +0,0 @@
# FlowerCore Brochure public host
#
# Thin Blazor host for public What's New, walkthrough, and gallery content
# carved out of FlowerCore.Intranet.Web. The ApplicationSet creates
# infra-brochure from this directory after merge.
---
apiVersion: v1
kind: Namespace
metadata:
name: brochure
labels:
app.kubernetes.io/part-of: flowercore
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: brochure-web
namespace: brochure
labels:
app: brochure-web
app.kubernetes.io/name: brochure-web
app.kubernetes.io/part-of: flowercore
spec:
replicas: 1
revisionHistoryLimit: 3
selector:
matchLabels:
app: brochure-web
template:
metadata:
labels:
app: brochure-web
app.kubernetes.io/name: brochure-web
app.kubernetes.io/part-of: flowercore
spec:
containers:
- name: brochure-web
image: localhost/fc-brochure-web:v20260524-sprint32
imagePullPolicy: Never
ports:
- containerPort: 8080
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
- name: ASPNETCORE_URLS
value: "http://+:8080"
resources:
requests:
cpu: "25m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
securityContext:
runAsNonRoot: true
runAsUser: 1654
runAsGroup: 1654
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: brochure-web
namespace: brochure
labels:
app: brochure-web
app.kubernetes.io/name: brochure-web
app.kubernetes.io/part-of: flowercore
spec:
type: ClusterIP
selector:
app: brochure-web
ports:
- name: http
port: 8080
targetPort: http
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: brochure-web-tls
namespace: brochure
spec:
secretName: brochure-web-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- brochure.flowercore.io
duration: 720h
renewBefore: 240h
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: brochure-web-public
namespace: brochure
spec:
entryPoints:
- websecure
routes:
- match: Host(`brochure.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
kind: Rule
services:
- name: brochure-web
port: 8080
tls:
secretName: brochure-web-tls

View File

@@ -0,0 +1,33 @@
# FlowerCore Remote Desktop - session pod resource defaults
#
# Namespace-level LimitRange for Sprint 44 Phase 1. This defends the
# fc-desktop namespace from unbounded container requests while the
# per-tenant advisory FairShareEvaluator lands in FlowerCore.RemoteDesktop.
apiVersion: v1
kind: LimitRange
metadata:
name: fc-desktop-pod-defaults
namespace: fc-desktop
labels:
app.kubernetes.io/name: fc-desktop
app.kubernetes.io/part-of: remotedesktop
app.kubernetes.io/component: capacity-guard
app.kubernetes.io/managed-by: argocd
flowercore.io/owner: infra
annotations:
flowercore.io/phase: sprint-44-cx-9-phase-a
spec:
limits:
- type: Container
default:
cpu: "1.0"
memory: "2Gi"
defaultRequest:
cpu: "500m"
memory: "1Gi"
max:
cpu: "2.0"
memory: "4Gi"
min:
cpu: "100m"
memory: "128Mi"

View File

@@ -0,0 +1,36 @@
# FlowerCore Remote Desktop - namespace ResourceQuota (GitOps-managed)
#
# Adopts the live fc-desktop-session-cap object created during the
# 2026-05-19 prewarm-cascade triage. Sprint 44 Phase 1 keeps the pod,
# CPU, and memory guard unchanged, then adds storage/PVC backstops from
# the fc-desktop CPU expansion substrate.
#
# Two-phase deploy note:
# Phase A: apply this ResourceQuota and limitrange.yaml with the current
# FlowerCore.RemoteDesktop image.
# Phase B: bump the service image only after the RemoteDesktop service
# admission/fair-share code lands in that repo.
apiVersion: v1
kind: ResourceQuota
metadata:
name: fc-desktop-session-cap
namespace: fc-desktop
labels:
app.kubernetes.io/name: fc-desktop
app.kubernetes.io/part-of: remotedesktop
app.kubernetes.io/component: capacity-guard
app.kubernetes.io/managed-by: argocd
flowercore.io/owner: infra
annotations:
flowercore.io/rationale: |
Operator-requested limit 2026-05-19: cluster CPU exhausted by RD
pool prewarm cascade. Preserve count/pods=15 plus requests.cpu=8
and requests.memory=16Gi until capacity expansion lands.
flowercore.io/phase: sprint-44-cx-9-phase-a
spec:
hard:
count/pods: "15"
requests.cpu: "8"
requests.memory: "16Gi"
requests.storage: "500Gi"
persistentvolumeclaims: "30"

View File

@@ -1,33 +0,0 @@
# Explicit ArgoCD Application shape for bootstrap/review.
#
# The live bluejay-infra ApplicationSet already discovers apps/* directories
# and creates this same Application name (`infra-fc-devicemgmt`) automatically.
# Keep repoURL on the internal Gitea ClusterIP URL; ArgoCD does not trust the
# external step-ca HTTPS endpoint.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infra-fc-devicemgmt
namespace: argocd
labels:
app.kubernetes.io/name: fc-devicemgmt
app.kubernetes.io/part-of: flowercore
app.kubernetes.io/managed-by: argocd
flowercore.io/tenant-id: system
flowercore.io/created-by: bluejay-infra
spec:
project: default
source:
repoURL: http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git
targetRevision: main
path: apps/fc-devicemgmt
destination:
server: https://kubernetes.default.svc
namespace: fc-devicemgmt
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true

View File

@@ -47,7 +47,7 @@ spec:
fsGroupChangePolicy: OnRootMismatch fsGroupChangePolicy: OnRootMismatch
containers: containers:
- name: operator - name: operator
image: localhost/fc-devicemgmt-operator:v20260512-cx5 image: localhost/fc-devicemgmt-operator:v20260519-sp34cl3-fix
imagePullPolicy: Never imagePullPolicy: Never
ports: ports:
- name: metrics - name: metrics

View File

@@ -4,6 +4,22 @@
# Sprint 9+ lane. This manifest is static-valid without requiring the image to # Sprint 9+ lane. This manifest is static-valid without requiring the image to
# exist yet; import localhost/fc-devicemgmt-web:<tag> to all schedulable RKE2 # exist yet; import localhost/fc-devicemgmt-web:<tag> to all schedulable RKE2
# nodes before letting ArgoCD sync a live rollout. # nodes before letting ArgoCD sync a live rollout.
#
# SCALED TO 0 — 2026-05-19 morning-routine cleanup.
# The Web pod cannot start until TWO upstream gaps close:
# 1. MySQL DB instance `flowercore_devicemgmt` (user `fc_devicemgmt`) is
# provisioned via fc-mysql Manager. The cluster currently has ZERO
# MySqlInstanceCrds and no `mysql.fc-mysql.svc:3306` Service, so the
# deployment-web container env `FlowerCore__Database__Host=mysql.fc-mysql.svc`
# points at nothing. Provision via the fc-mysql Manager UI/REST/MCP.
# 2. 1Password vault item `IAmWorkin/FlowerCore DeviceManagement Runtime`
# with 5 fields (DB-Password, mtls-ca.pem, mtls-client.crt, mtls-client.key,
# mtls-chain.pem) — see apps/fc-devicemgmt/1password-item.yaml. Mint mTLS
# from step-ca-agent ClusterIssuer per ADR-126; DB-Password must match the
# password configured for the MySQL user.
# Re-enable: change replicas back to 2 after both gaps close. The image tag
# in this file (v20260512-cx5) MAY also need a refresh — it predates the
# Sprint 34 Cl-3 operator fix; Web may have an analogous bug.
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -20,7 +36,7 @@ metadata:
annotations: annotations:
flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard flowercore.io/traceability-standard: k8s-pod-ownership-and-traceability-standard
spec: spec:
replicas: 2 replicas: 0
revisionHistoryLimit: 3 revisionHistoryLimit: 3
selector: selector:
matchLabels: matchLabels:

2
apps/github-runner/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
*.sh text eol=lf
Dockerfile text eol=lf

View File

@@ -0,0 +1,44 @@
FROM myoung34/github-runner:latest
ARG RUBY_VERSION=3.3.11
ARG RUBY_MINOR=3.3
ARG RUBY_BUILD_VERSION=v20260326
ARG RUNNER_UID=1001
ARG RUNNER_GID=1001
ENV RUNNER_TOOL_CACHE=/home/runner/_tool
ENV RUNNER_RUBY_TOOLCACHE=/opt/runner-toolcache
ENV PATH="/home/runner/_tool/Ruby/${RUBY_MINOR}/x64/bin:/opt/runner-toolcache/Ruby/${RUBY_MINOR}/x64/bin:${PATH}"
USER root
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
autoconf \
bison \
build-essential \
ca-certificates \
curl \
libdb-dev \
libffi-dev \
libgdbm-dev \
libgmp-dev \
libncurses-dev \
libreadline-dev \
libssl-dev \
libyaml-dev \
patch \
pkg-config \
uuid-dev \
zlib1g-dev \
&& curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/${RUBY_BUILD_VERSION}.tar.gz" -o /tmp/ruby-build.tar.gz \
&& mkdir -p /tmp/ruby-build \
&& tar -xzf /tmp/ruby-build.tar.gz --strip-components=1 -C /tmp/ruby-build \
&& /tmp/ruby-build/install.sh \
&& rm -rf /tmp/ruby-build /tmp/ruby-build.tar.gz /var/lib/apt/lists/*
COPY install-ruby-toolcache.sh /usr/local/bin/install-ruby-toolcache.sh
RUN chmod +x /usr/local/bin/install-ruby-toolcache.sh \
&& RUBY_VERSION="${RUBY_VERSION}" RUBY_MINOR="${RUBY_MINOR}" TOOLCACHE_ROOT="${RUNNER_RUBY_TOOLCACHE}" RUNNER_UID="${RUNNER_UID}" RUNNER_GID="${RUNNER_GID}" /usr/local/bin/install-ruby-toolcache.sh \
&& ruby -v

View File

@@ -7,12 +7,17 @@ Deployments with `kubectl`; update this manifest and let ArgoCD reconcile.
All repo-scoped Linux runners use: All repo-scoped Linux runners use:
- `localhost/fc-github-runner:v20260520-ruby3.3.11`, derived from
`myoung34/github-runner:latest`
- `ACCESS_TOKEN` from the `github-runner-token` Secret - `ACCESS_TOKEN` from the `github-runner-token` Secret
- `RUN_AS_ROOT=false` - `RUN_AS_ROOT=false`
- `EPHEMERAL=true` - `EPHEMERAL=true`
- `LABELS=self-hosted,linux,fc-build-linux` - `LABELS=self-hosted,linux,fc-build-linux`
- writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and - writable non-root paths under `/home/runner` for .NET, NuGet, XDG cache, and
Actions tool cache Actions tool cache
- Ruby 3.3.11 seeded into `/home/runner/_tool/Ruby/3.3/x64` from the baked
`/opt/runner-toolcache` copy so `ruby/setup-ruby@v1` can discover it on
self-hosted `ubuntu-20.04-x64` runners
`github-runner` for `FlowerCore.Common` is single-replica because it retains the `github-runner` for `FlowerCore.Common` is single-replica because it retains the
original Longhorn ReadWriteOnce NuGet PVC. Every other repo-scoped runner uses original Longhorn ReadWriteOnce NuGet PVC. Every other repo-scoped runner uses
@@ -28,6 +33,34 @@ Sprint 32 final long-tail wave adds 16 two-replica Deployments:
`FlowerCore.Provisioning`, `FlowerCore.Redis`, `FlowerCore.MessageBoard`, and `FlowerCore.Provisioning`, `FlowerCore.Redis`, `FlowerCore.MessageBoard`, and
`FlowerCore.MenuBoard`. `FlowerCore.MenuBoard`.
## Image Build
Ruby is baked with a pinned `ruby-build` release and Ruby patch version. The pod
still mounts an `emptyDir` over `/home/runner`, so the `setup-runner-home` init
container copies the baked toolcache from `/opt/runner-toolcache/Ruby` into
`/home/runner/_tool/Ruby` before the runner container starts.
```bash
cd apps/github-runner
podman build -t localhost/fc-github-runner:v20260520-ruby3.3.11 .
podman run --rm localhost/fc-github-runner:v20260520-ruby3.3.11 ruby -v
podman run --rm localhost/fc-github-runner:v20260520-ruby3.3.11 \
test -f /opt/runner-toolcache/Ruby/3.3/x64.complete
podman save localhost/fc-github-runner:v20260520-ruby3.3.11 \
-o fc-github-runner-v20260520-ruby3.3.11.tar
```
Import the saved image on every schedulable RKE2 node before ArgoCD rolls the
Deployments:
```bash
for node in rke2-server rke2-agent1 rke2-agent2; do
scp fc-github-runner-v20260520-ruby3.3.11.tar "$node:/tmp/"
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images rm localhost/fc-github-runner:v20260520-ruby3.3.11 || true'
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/fc-github-runner-v20260520-ruby3.3.11.tar'
done
```
## Post-Merge Proof ## Post-Merge Proof
After the PR is merged and ArgoCD syncs, verify the runner fleet: After the PR is merged and ArgoCD syncs, verify the runner fleet:
@@ -36,6 +69,14 @@ After the PR is merged and ArgoCD syncs, verify the runner fleet:
kubectl -n github-runner get deploy,pods,pvc kubectl -n github-runner get deploy,pods,pvc
``` ```
Verify the Ruby toolcache in a fresh pod:
```bash
kubectl -n github-runner exec deploy/github-runner-puppet -c runner -- ruby -v
kubectl -n github-runner exec deploy/github-runner-puppet -c runner -- sh -c \
'echo "$RUNNER_TOOL_CACHE" && test -f "$RUNNER_TOOL_CACHE/Ruby/3.3/x64.complete"'
```
Verify GitHub registration for the repo-scoped runners: Verify GitHub registration for the repo-scoped runners:
```bash ```bash
@@ -69,6 +110,10 @@ from GitHub Actions and verify it lands on an `rke2-linux-*` runner.
- `actions/setup-dotnet` permission error at `/usr/share/dotnet`: check that - `actions/setup-dotnet` permission error at `/usr/share/dotnet`: check that
`DOTNET_INSTALL_DIR=/home/runner/.dotnet` and related cache env vars are `DOTNET_INSTALL_DIR=/home/runner/.dotnet` and related cache env vars are
present on the runner pod. present on the runner pod.
- `ruby/setup-ruby@v1` says self-hosted runners must install Ruby in
`$RUNNER_TOOL_CACHE`: check that the init container copied
`/opt/runner-toolcache/Ruby` into `/home/runner/_tool/Ruby` and that
`/home/runner/_tool/Ruby/3.3/x64.complete` exists.
- `404` during runner registration: the fine-grained PAT is valid but missing - `404` during runner registration: the fine-grained PAT is valid but missing
repository access for that repo. Add the repo to the PAT access list; the PAT repository access for that repo. Add the repo to the PAT access list; the PAT
value does not change. value does not change.

View File

@@ -22,11 +22,16 @@
# NUGET_PACKAGES, XDG_CACHE_HOME, and RUNNER_TOOL_CACHE are all pointed at # NUGET_PACKAGES, XDG_CACHE_HOME, and RUNNER_TOOL_CACHE are all pointed at
# writable mounted paths under /home/runner so actions/setup-dotnet does not # writable mounted paths under /home/runner so actions/setup-dotnet does not
# attempt to install into /usr/share/dotnet. # attempt to install into /usr/share/dotnet.
# Ruby 3.3.11 is baked into localhost/fc-github-runner:v20260520-ruby3.3.11
# under /opt/runner-toolcache; setup-runner-home copies it into
# /home/runner/_tool because the runner-home emptyDir masks image content
# under /home/runner at runtime.
# #
# Credentials: # Credentials:
# OnePasswordItem "GitHub PAT (Runner Registration)" syncs Secret # OnePasswordItem "GitHub PAT (Runner Registration)" syncs Secret
# github-runner-token with field "credential". myoung34/github-runner uses # github-runner-token with field "credential". The custom image inherits
# ACCESS_TOKEN to mint short-lived registration tokens on pod start. # myoung34/github-runner behavior and uses ACCESS_TOKEN to mint short-lived
# registration tokens on pod start.
# #
# Security model: # Security model:
# - No ClusterRole / ClusterRoleBinding. The ServiceAccount has no K8s API # - No ClusterRole / ClusterRoleBinding. The ServiceAccount has no K8s API
@@ -152,15 +157,19 @@ spec:
# honors the deeper mount. # honors the deeper mount.
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -169,8 +178,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
# GitHub org/repo targeting. # GitHub org/repo targeting.
# Set REPO_URL for a repo-scoped runner (cheaper, simpler). # Set REPO_URL for a repo-scoped runner (cheaper, simpler).
@@ -325,15 +334,19 @@ spec:
# rather than re-applied per repo as flipped lanes land. # rather than re-applied per repo as flipped lanes land.
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -342,8 +355,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Shared.Pos" value: "https://github.com/astoltz/FlowerCore.Shared.Pos"
@@ -459,15 +472,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -476,8 +493,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Puppet" value: "https://github.com/astoltz/FlowerCore.Puppet"
@@ -587,15 +604,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -604,8 +625,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Signage" value: "https://github.com/astoltz/FlowerCore.Signage"
@@ -715,15 +736,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -732,8 +757,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.DMS" value: "https://github.com/astoltz/FlowerCore.DMS"
@@ -843,15 +868,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -860,8 +889,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Telephony" value: "https://github.com/astoltz/FlowerCore.Telephony"
@@ -971,15 +1000,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -988,8 +1021,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Print.Web" value: "https://github.com/astoltz/FlowerCore.Print.Web"
@@ -1099,15 +1132,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -1116,8 +1153,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Chat" value: "https://github.com/astoltz/FlowerCore.Chat"
@@ -1227,15 +1264,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -1244,8 +1285,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.MySQL" value: "https://github.com/astoltz/FlowerCore.MySQL"
@@ -1355,15 +1396,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -1372,8 +1417,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Kiosk.Linux" value: "https://github.com/astoltz/FlowerCore.Kiosk.Linux"
@@ -1485,15 +1530,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -1502,8 +1551,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Marquee" value: "https://github.com/astoltz/FlowerCore.Marquee"
@@ -1615,15 +1664,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -1632,8 +1685,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.TtsReader" value: "https://github.com/astoltz/FlowerCore.TtsReader"
@@ -1745,15 +1798,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -1762,8 +1819,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Knowledge" value: "https://github.com/astoltz/FlowerCore.Knowledge"
@@ -1874,15 +1931,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -1891,8 +1952,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.LlmBridge" value: "https://github.com/astoltz/FlowerCore.LlmBridge"
@@ -2003,15 +2064,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2020,8 +2085,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Media" value: "https://github.com/astoltz/FlowerCore.Media"
@@ -2132,15 +2197,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2149,8 +2218,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Presentations" value: "https://github.com/astoltz/FlowerCore.Presentations"
@@ -2261,15 +2330,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2278,8 +2351,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.RemoteDesktop" value: "https://github.com/astoltz/FlowerCore.RemoteDesktop"
@@ -2390,15 +2463,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2407,8 +2484,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.DNS" value: "https://github.com/astoltz/FlowerCore.DNS"
@@ -2519,15 +2596,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2536,8 +2617,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Distribution" value: "https://github.com/astoltz/FlowerCore.Distribution"
@@ -2648,15 +2729,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2665,8 +2750,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Scoreboard" value: "https://github.com/astoltz/FlowerCore.Scoreboard"
@@ -2777,15 +2862,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2794,8 +2883,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.SegmentDisplay" value: "https://github.com/astoltz/FlowerCore.SegmentDisplay"
@@ -2906,15 +2995,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -2923,8 +3016,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Signage.Contracts" value: "https://github.com/astoltz/FlowerCore.Signage.Contracts"
@@ -3035,15 +3128,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -3052,8 +3149,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.SignalControl" value: "https://github.com/astoltz/FlowerCore.SignalControl"
@@ -3164,15 +3261,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -3181,8 +3282,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Intranet.Web" value: "https://github.com/astoltz/FlowerCore.Intranet.Web"
@@ -3293,15 +3394,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -3310,8 +3415,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Provisioning" value: "https://github.com/astoltz/FlowerCore.Provisioning"
@@ -3422,15 +3527,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -3439,8 +3548,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.Redis" value: "https://github.com/astoltz/FlowerCore.Redis"
@@ -3551,15 +3660,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -3568,8 +3681,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.MessageBoard" value: "https://github.com/astoltz/FlowerCore.MessageBoard"
@@ -3680,15 +3793,19 @@ spec:
fsGroup: 1001 fsGroup: 1001
initContainers: initContainers:
- name: setup-runner-home - name: setup-runner-home
image: busybox:1.36 image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Never
command: command:
- sh - sh
- -c - -c
- | - |
set -e set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
runAsNonRoot: false runAsNonRoot: false
@@ -3697,8 +3814,8 @@ spec:
mountPath: /home/runner mountPath: /home/runner
containers: containers:
- name: runner - name: runner
image: myoung34/github-runner:latest image: localhost/fc-github-runner:v20260520-ruby3.3.11
imagePullPolicy: Always imagePullPolicy: Never
env: env:
- name: REPO_URL - name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.MenuBoard" value: "https://github.com/astoltz/FlowerCore.MenuBoard"

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
RUBY_VERSION="${RUBY_VERSION:-3.3.11}"
RUBY_MINOR="${RUBY_MINOR:-3.3}"
TOOLCACHE_ROOT="${TOOLCACHE_ROOT:-/opt/runner-toolcache}"
RUNNER_UID="${RUNNER_UID:-1001}"
RUNNER_GID="${RUNNER_GID:-1001}"
RUBY_PREFIX="${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64"
mkdir -p "${TOOLCACHE_ROOT}/Ruby"
RUBY_CONFIGURE_OPTS="${RUBY_CONFIGURE_OPTS:---disable-install-doc --disable-yjit}" ruby-build "${RUBY_VERSION}" "${RUBY_PREFIX}"
touch "${TOOLCACHE_ROOT}/Ruby/${RUBY_VERSION}/x64.complete"
ln -sfn "${RUBY_VERSION}" "${TOOLCACHE_ROOT}/Ruby/${RUBY_MINOR}"
"${RUBY_PREFIX}/bin/ruby" -v
chown -R "${RUNNER_UID}:${RUNNER_GID}" "${TOOLCACHE_ROOT}"
chmod -R a+rX "${TOOLCACHE_ROOT}"

View File

@@ -280,13 +280,14 @@ data:
printer_model: "NuPrint 210" printer_model: "NuPrint 210"
# Print.Web health (Blazor app on edge2:5200) # Print.Web health (Blazor app on edge2:5200)
# Target `/health` (anonymous) — root path requires API key auth and returns 401.
- job_name: "probe-printweb" - job_name: "probe-printweb"
metrics_path: /probe metrics_path: /probe
params: params:
module: [http_2xx] module: [http_2xx]
scrape_interval: 30s scrape_interval: 30s
static_configs: static_configs:
- targets: ["http://10.0.57.16:5200/"] - targets: ["http://10.0.57.16:5200/health"]
labels: labels:
instance: "print-web" instance: "print-web"
service: "print-web" service: "print-web"
@@ -1273,24 +1274,55 @@ metadata:
data: data:
notify.py: | notify.py: |
#!/usr/bin/env python3 #!/usr/bin/env python3
"""HTTP->IRC alert relay with thermal printer forwarding for Grafana webhooks. """HTTP->IRC alert relay with thermal-printer DIGEST forwarding.
Listens on :9119, posts to #alerts on UnrealIRCd via raw IRC protocol.
Alerts tagged alert_channel=thermal_print also POST to Print.Web /api/print/alert. Listens on :9119, posts to #alerts on UnrealIRCd, forwards to Print.Web
/api/print/alert. Thermal printing is BATCHED into hourly digests by
default so the printer no longer spam-fires per Grafana webhook.
Routing (per Grafana webhook alert):
- IRC: always per-event (operator likes the stream)
- Thermal printer:
* severity in {critical,disaster,page} OR
label alert_channel=thermal_print_immediate -> print NOW
* label alert_channel=thermal_print -> enqueue into hourly digest
* everything else -> IRC only
- RESOLVED webhooks remove the alert from the digest buffer
Env vars (defaults preserve old behavior on first deploy):
THERMAL_PRINT_ENABLED default "true" - master kill switch
BATCH_INTERVAL_MIN default "60" - minutes between digest prints
BATCH_MAX_PENDING default "50" - force-flush threshold
HTTP surface:
POST / - Grafana webhook entry
POST /flush - manual digest flush (idempotent)
GET / - status + config + buffer depth + stats
""" """
import json, socket, sys, time import json, os, socket, sys, threading, time
from collections import defaultdict
from datetime import datetime, timezone
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from urllib.error import URLError
IRC_HOST = "unrealircd.irc.svc" # short name: CoreDNS ndots:5 + iamworkin.lan template hijacks full .cluster.local (see memory) THERMAL_PRINT_ENABLED = os.environ.get("THERMAL_PRINT_ENABLED", "true").lower() == "true"
IRC_PORT = 6667 BATCH_INTERVAL_MIN = int(os.environ.get("BATCH_INTERVAL_MIN", "60"))
IRC_NICK = "grafana-bot" BATCH_MAX_PENDING = int(os.environ.get("BATCH_MAX_PENDING", "50"))
IRC_CHANNEL = "#alerts"
PRINT_WEB_URL = "http://10.0.57.16:5200/api/print/alert" IRC_HOST = os.environ.get("IRC_HOST", "unrealircd.irc.svc")
PRINT_ENABLED = True IRC_PORT = int(os.environ.get("IRC_PORT", "6667"))
IRC_NICK = os.environ.get("IRC_NICK", "grafana-bot")
IRC_CHANNEL = os.environ.get("IRC_CHANNEL", "#alerts")
PRINT_WEB_URL = os.environ.get("PRINT_WEB_URL", "http://10.0.57.16:5200/api/print/alert")
_buffer_lock = threading.Lock()
_buffer = {} # fingerprint -> {"alert": dict, "first_seen": float, "last_seen": float}
_last_flush_time = time.time()
_stats = {"webhooks_received": 0, "irc_sent": 0, "print_immediate": 0,
"digest_flushed": 0, "buffer_dedup": 0, "buffer_added": 0,
"buffer_resolved": 0, "started_at": time.time()}
def send_irc(message): def send_irc(message):
"""Connect, handle PING, join, send, quit."""
try: try:
sock = socket.create_connection((IRC_HOST, IRC_PORT), timeout=15) sock = socket.create_connection((IRC_HOST, IRC_PORT), timeout=15)
sock.sendall(f"NICK {IRC_NICK}\r\n".encode()) sock.sendall(f"NICK {IRC_NICK}\r\n".encode())
@@ -1323,52 +1355,137 @@ data:
time.sleep(0.5) time.sleep(0.5)
sock.sendall(b"QUIT :alert delivered\r\n") sock.sendall(b"QUIT :alert delivered\r\n")
sock.close() sock.close()
_stats["irc_sent"] += 1
return True return True
except Exception as e: except Exception as e:
print(f"[irc-notify] IRC send failed: {e}", file=sys.stderr) print(f"[irc-notify] IRC send failed: {e}", file=sys.stderr)
return False return False
def send_thermal_print(alert): def post_thermal(payload, kind):
if not PRINT_ENABLED: return if not THERMAL_PRINT_ENABLED:
labels = alert.get("labels", {}) print(f"[irc-notify] thermal disabled; skip {kind} ({payload.get('title','?')[:40]})", file=sys.stderr)
annotations = alert.get("annotations", {}) return False
status = alert.get("status", "firing").upper()
summary = annotations.get("summary", "")
description = annotations.get("description", "")
runbook = annotations.get("runbook", "")
# Build a useful message: summary + description + runbook steps
parts = []
if summary: parts.append(summary)
if description and description != summary: parts.append(description)
if runbook: parts.append("STEPS: " + runbook)
message = " | ".join(parts) if parts else labels.get("alertname", "Unknown alert")
payload = {
"title": labels.get("alertname", "Unknown"),
"severity": labels.get("severity", "warning").capitalize(),
"host": labels.get("instance", labels.get("host", "unknown")),
"message": message,
"eventId": alert.get("fingerprint", ""),
"source": "Grafana",
"status": "RESOLVED" if status == "RESOLVED" else "PROBLEM",
"acknowledged": False
}
try: try:
req = Request(PRINT_WEB_URL, data=json.dumps(payload).encode("utf-8"), req = Request(PRINT_WEB_URL, data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}, method="POST") headers={"Content-Type": "application/json"}, method="POST")
resp = urlopen(req, timeout=10) resp = urlopen(req, timeout=10)
print(f"[irc-notify] Thermal print sent: {resp.read().decode()}", file=sys.stderr) if kind == "immediate": _stats["print_immediate"] += 1
print(f"[irc-notify] thermal {kind} sent: {payload.get('title','?')[:50]}", file=sys.stderr)
return True
except Exception as e: except Exception as e:
print(f"[irc-notify] Thermal print failed: {e}", file=sys.stderr) print(f"[irc-notify] thermal {kind} failed: {e}", file=sys.stderr)
def should_print(alert):
labels = alert.get("labels", {})
if labels.get("alert_channel") == "thermal_print": return True
if labels.get("severity", "").lower() in ("critical", "disaster"): return True
if alert.get("status", "").upper() == "RESOLVED": return False
return False return False
def fingerprint_of(alert):
fp = alert.get("fingerprint", "")
if fp: return fp
labels = alert.get("labels", {})
target = labels.get("pod") or labels.get("instance") or labels.get("deployment") or labels.get("statefulset") or labels.get("namespace") or ""
return f"{labels.get('alertname','?')}/{labels.get('namespace','')}/{target}"
def is_critical(alert):
return alert.get("labels", {}).get("severity", "").lower() in ("critical", "disaster", "page")
def is_immediate_label(alert):
return alert.get("labels", {}).get("alert_channel") == "thermal_print_immediate"
def is_batched_label(alert):
return alert.get("labels", {}).get("alert_channel") == "thermal_print"
def add_to_digest(alert):
"""Add an alert to the digest buffer. Returns True if the buffer GREW
(new fingerprint), False if it was a dedup, resolution, or no-op.
"""
if not THERMAL_PRINT_ENABLED: return False
fp = fingerprint_of(alert)
status = alert.get("status", "firing").lower()
with _buffer_lock:
if status == "resolved":
if fp in _buffer:
del _buffer[fp]
_stats["buffer_resolved"] += 1
return False
if fp in _buffer:
_buffer[fp]["last_seen"] = time.time()
_buffer[fp]["alert"] = alert
_stats["buffer_dedup"] += 1
return False
_buffer[fp] = {"alert": alert, "first_seen": time.time(), "last_seen": time.time()}
_stats["buffer_added"] += 1
return True
def build_digest_payload():
with _buffer_lock:
items = list(_buffer.values())
if not items: return None
by_name = defaultdict(list)
for item in items:
labels = item["alert"].get("labels", {})
by_name[labels.get("alertname", "Unknown")].append(item)
lines = []
for name, group in sorted(by_name.items()):
targets = []
for it in group[:5]:
labels = it["alert"].get("labels", {})
t = (labels.get("pod") or labels.get("instance") or labels.get("deployment")
or labels.get("statefulset") or labels.get("namespace") or "?")
targets.append(t)
more = f" (+{len(group)-5})" if len(group) > 5 else ""
sevs = sorted({it["alert"].get("labels", {}).get("severity", "warning") for it in group})
lines.append(f"[{'/'.join(sevs)}] {name} x{len(group)}: {', '.join(targets)}{more}")
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
title = f"Alert digest: {len(items)} firing"
body = "\n".join([
f"=== {title} ===",
f"as of {now}",
"",
*lines,
"",
"Stream: #alerts (IRC) | Triage: grafana-noc1.iamworkin.lan",
"Force-flush: POST irc-notify.monitoring.svc:9119/flush",
])
return {"title": title, "severity": "Warning", "host": "monitoring",
"message": body, "eventId": f"digest-{int(time.time())}",
"source": "Grafana digest", "status": "PROBLEM", "acknowledged": False}
def flush_digest():
payload = build_digest_payload()
if payload is None:
print("[irc-notify] flush: buffer empty, no digest sent", file=sys.stderr)
return False
sent = post_thermal(payload, "digest")
with _buffer_lock:
_buffer.clear()
if sent: _stats["digest_flushed"] += 1
return sent
def digest_loop():
global _last_flush_time
while True:
try:
now = time.time()
elapsed = now - _last_flush_time
if elapsed >= BATCH_INTERVAL_MIN * 60:
print(f"[irc-notify] digest tick: interval reached ({BATCH_INTERVAL_MIN}m); buffer={len(_buffer)}", file=sys.stderr)
flush_digest()
_last_flush_time = now
elif len(_buffer) >= BATCH_MAX_PENDING:
print(f"[irc-notify] digest tick: buffer full ({len(_buffer)}); force flush", file=sys.stderr)
flush_digest()
_last_flush_time = now
time.sleep(15)
except Exception as e:
print(f"[irc-notify] digest loop error: {e}", file=sys.stderr)
time.sleep(60)
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
def do_POST(self): def do_POST(self):
if self.path == "/flush":
ok = flush_digest()
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
self.wfile.write(json.dumps({"flushed": ok, "buffer_after": len(_buffer)}).encode())
return
_stats["webhooks_received"] += 1
length = int(self.headers.get("Content-Length", 0)) length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length)) if length else {} body = json.loads(self.rfile.read(length)) if length else {}
for alert in body.get("alerts", []): for alert in body.get("alerts", []):
@@ -1383,22 +1500,56 @@ data:
msg = f"{icon}{sev_tag} {name}: {summary}" msg = f"{icon}{sev_tag} {name}: {summary}"
if desc: msg += f"\n {desc}" if desc: msg += f"\n {desc}"
send_irc(msg) send_irc(msg)
if should_print(alert): send_thermal_print(alert) # Thermal routing — EVERYTHING (including criticals) goes into
self.send_response(200) # the hourly digest. Only the explicit `alert_channel=thermal_print_immediate`
self.send_header("Content-Type", "application/json") # label bypasses, and even that flushes-the-current-digest rather
self.end_headers() # than printing a standalone job, so the same fingerprint can't
# spam the printer per webhook cycle.
if status == "RESOLVED":
add_to_digest(alert) # removes from buffer
continue
if is_immediate_label(alert):
# Explicit opt-in for "paper this NOW" — first arrival of a
# new fingerprint triggers an immediate digest flush; repeat
# webhooks for the same fingerprint dedupe in the buffer
# until the next interval or until the alert resolves.
new_in_buffer = add_to_digest(alert)
if new_in_buffer:
global _last_flush_time
flush_digest()
_last_flush_time = time.time()
elif is_critical(alert) or is_batched_label(alert):
add_to_digest(alert)
# else: IRC-only (warnings without thermal_print label)
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
self.wfile.write(b'{"status":"ok"}') self.wfile.write(b'{"status":"ok"}')
def do_GET(self): def do_GET(self):
self.send_response(200) self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
self.send_header("Content-Type", "application/json") with _buffer_lock:
self.end_headers() alertnames = sorted({it["alert"].get("labels", {}).get("alertname", "?") for it in _buffer.values()})
self.wfile.write(json.dumps({"service":"irc-notify","thermal_print":PRINT_ENABLED}).encode()) depth = len(_buffer)
info = {
"service": "irc-notify",
"config": {"thermal_print_enabled": THERMAL_PRINT_ENABLED,
"batch_interval_min": BATCH_INTERVAL_MIN,
"batch_max_pending": BATCH_MAX_PENDING,
"irc_target": f"{IRC_HOST}:{IRC_PORT} {IRC_CHANNEL}",
"print_web_url": PRINT_WEB_URL},
"buffer": {"depth": depth, "alertnames": alertnames,
"seconds_since_last_flush": int(time.time() - _last_flush_time),
"seconds_until_next_flush": max(0, int(BATCH_INTERVAL_MIN*60 - (time.time() - _last_flush_time)))},
"stats": _stats,
}
self.wfile.write(json.dumps(info, indent=2).encode())
def log_message(self, format, *args): def log_message(self, format, *args):
print(f"[irc-notify] {args[0]}", file=sys.stderr) print(f"[irc-notify] {args[0]}", file=sys.stderr)
if __name__ == "__main__": if __name__ == "__main__":
threading.Thread(target=digest_loop, daemon=True).start()
server = HTTPServer(("0.0.0.0", 9119), Handler) server = HTTPServer(("0.0.0.0", 9119), Handler)
print(f"IRC alert relay :9119 -> {IRC_HOST}:{IRC_PORT} {IRC_CHANNEL} (thermal: {PRINT_ENABLED})") print(f"[irc-notify] :9119 -> IRC {IRC_HOST}:{IRC_PORT} {IRC_CHANNEL} | thermal={'ON' if THERMAL_PRINT_ENABLED else 'OFF'} | digest={BATCH_INTERVAL_MIN}m max={BATCH_MAX_PENDING}", file=sys.stderr)
server.serve_forever() server.serve_forever()
# ============================================================================= # =============================================================================

View File

@@ -0,0 +1,285 @@
using FluentAssertions;
using YamlDotNet.RepresentationModel;
using Xunit;
namespace BluejayInfraLint.Tests;
[Trait("Category", "Unit")]
public sealed class FcDesktopCapacityPolicyTests
{
private static readonly ManifestInventory Inventory = ManifestInventory.Load();
[Fact]
public void FcDesktop_AppDirectoryMustExist()
{
Directory.Exists(Path.Combine(Inventory.BluejayRoot, "apps", "fc-desktop"))
.Should()
.BeTrue();
}
[Fact]
public void FcDesktop_MustHaveExactlyOneResourceQuota()
{
FcDesktopDocuments()
.Where(document => document.Kind == "ResourceQuota")
.Should()
.ContainSingle();
}
[Fact]
public void FcDesktop_ResourceQuotaMustAdoptLiveSessionCapObject()
{
var quota = ResourceQuota();
quota.RelativePath.Should().Be("fc-desktop/resourcequota.yaml");
quota.Name.Should().Be("fc-desktop-session-cap");
quota.Namespace.Should().Be("fc-desktop");
}
[Theory]
[InlineData("count/pods", "15")]
[InlineData("requests.cpu", "8")]
[InlineData("requests.memory", "16Gi")]
[InlineData("requests.storage", "500Gi")]
[InlineData("persistentvolumeclaims", "30")]
public void FcDesktop_ResourceQuotaMustDeclarePhaseOneHardLimits(string key, string value)
{
ResourceQuota().Scalar("spec", "hard", key).Should().Be(value);
}
[Fact]
public void FcDesktop_ResourceQuotaMustCarryTraceableLabels()
{
ResourceQuotaLabels()
.Should()
.Contain(new Dictionary<string, string>
{
["app.kubernetes.io/name"] = "fc-desktop",
["app.kubernetes.io/part-of"] = "remotedesktop",
["app.kubernetes.io/component"] = "capacity-guard",
["app.kubernetes.io/managed-by"] = "argocd",
["flowercore.io/owner"] = "infra",
});
}
[Fact]
public void FcDesktop_ResourceQuotaMustUseRequestsKeysForComputeCap()
{
var hardKeys = HardLimitKeys(ResourceQuota());
hardKeys.Should().Contain(new[] { "requests.cpu", "requests.memory" });
hardKeys.Should().NotContain(new[] { "cpu", "memory" });
}
[Fact]
public void FcDesktop_ResourceQuotaMustAvoidDestructiveArgoAnnotations()
{
var quota = ResourceQuota();
quota.Scalar("metadata", "annotations", "argocd.argoproj.io/hook").Should().BeNull();
quota.Scalar("metadata", "annotations", "argocd.argoproj.io/hook-delete-policy").Should().BeNull();
var syncOptions = quota.Scalar("metadata", "annotations", "argocd.argoproj.io/sync-options") ?? string.Empty;
syncOptions.Should().NotContain("Force=true");
syncOptions.Should().NotContain("Replace=true");
}
[Fact]
public void FcDesktop_ResourceQuotaMustRecordPhaseAInfraOnlyScope()
{
ResourceQuota().Scalar("metadata", "annotations", "flowercore.io/phase")
.Should()
.Be("sprint-44-cx-9-phase-a");
}
[Fact]
public void FcDesktop_MustHaveExactlyOneLimitRange()
{
FcDesktopDocuments()
.Where(document => document.Kind == "LimitRange")
.Should()
.ContainSingle();
}
[Fact]
public void FcDesktop_LimitRangeMustLiveBesideResourceQuota()
{
var limitRange = LimitRange();
limitRange.RelativePath.Should().Be("fc-desktop/limitrange.yaml");
limitRange.Name.Should().Be("fc-desktop-pod-defaults");
limitRange.Namespace.Should().Be("fc-desktop");
}
[Fact]
public void FcDesktop_LimitRangeMustHaveSingleContainerRule()
{
var limit = LimitRangeRule();
LimitRange().MappingSequence("spec", "limits").Should().ContainSingle();
ManifestNodeExtensions.Scalar(limit, "type").Should().Be("Container");
}
[Theory]
[InlineData("default", "cpu", "1.0")]
[InlineData("default", "memory", "2Gi")]
[InlineData("defaultRequest", "cpu", "500m")]
[InlineData("defaultRequest", "memory", "1Gi")]
[InlineData("max", "cpu", "2.0")]
[InlineData("max", "memory", "4Gi")]
[InlineData("min", "cpu", "100m")]
[InlineData("min", "memory", "128Mi")]
public void FcDesktop_LimitRangeMustDeclarePerPodShape(string section, string key, string value)
{
ManifestNodeExtensions.Scalar(LimitRangeRule(), section, key).Should().Be(value);
}
[Fact]
public void FcDesktop_LimitRangeMustCarryTraceableLabels()
{
LimitRangeLabels()
.Should()
.Contain(new Dictionary<string, string>
{
["app.kubernetes.io/name"] = "fc-desktop",
["app.kubernetes.io/part-of"] = "remotedesktop",
["app.kubernetes.io/component"] = "capacity-guard",
["app.kubernetes.io/managed-by"] = "argocd",
["flowercore.io/owner"] = "infra",
});
}
[Fact]
public void FcDesktop_LimitRangeMustAvoidDestructiveArgoAnnotations()
{
var limitRange = LimitRange();
limitRange.Scalar("metadata", "annotations", "argocd.argoproj.io/hook").Should().BeNull();
limitRange.Scalar("metadata", "annotations", "argocd.argoproj.io/hook-delete-policy").Should().BeNull();
var syncOptions = limitRange.Scalar("metadata", "annotations", "argocd.argoproj.io/sync-options") ?? string.Empty;
syncOptions.Should().NotContain("Force=true");
syncOptions.Should().NotContain("Replace=true");
}
[Fact]
public void FcDesktop_LimitRangeMustRecordPhaseAInfraOnlyScope()
{
LimitRange().Scalar("metadata", "annotations", "flowercore.io/phase")
.Should()
.Be("sprint-44-cx-9-phase-a");
}
[Fact]
public void FcDesktop_BluejayInfraMustNotOwnDeploymentOrService()
{
FcDesktopDocuments()
.Select(document => document.Kind)
.Should()
.NotContain(new[] { "Deployment", "Service" });
}
[Fact]
public void FcDesktop_BluejayInfraMustOnlyOwnInfraResourceKinds()
{
var allowedKinds = new HashSet<string>(StringComparer.Ordinal)
{
"Certificate",
"IngressRoute",
"NetworkPolicy",
"ResourceQuota",
"LimitRange",
};
FcDesktopDocuments()
.Select(document => document.Kind)
.Should()
.OnlyContain(kind => allowedKinds.Contains(kind));
}
[Fact]
public void FcDesktop_NetworkPolicySetMustRemainPresent()
{
FcDesktopDocuments()
.Where(document => document.Kind == "NetworkPolicy")
.Select(document => document.Name)
.Should()
.BeEquivalentTo(
"desktop-isolation",
"fc-desktop-default-deny",
"remotedesktop-web-isolation",
"cm-acme-http-solver-allow");
}
[Fact]
public void FcDesktop_TlsIngressMustRemainOwnedByInfra()
{
FcDesktopDocuments()
.Should()
.Contain(document => document.Kind == "Certificate" && document.Name == "remotedesktop-web-tls")
.And
.Contain(document => document.Kind == "IngressRoute" && document.Name == "remotedesktop-web");
}
private static IReadOnlyList<ManifestDocument> FcDesktopDocuments()
{
return Inventory.Documents
.Where(document => document.RelativePath.StartsWith("fc-desktop/", StringComparison.Ordinal))
.ToList();
}
private static ManifestDocument ResourceQuota()
{
return FcDesktopDocuments()
.Single(document => document.Kind == "ResourceQuota");
}
private static ManifestDocument LimitRange()
{
return FcDesktopDocuments()
.Single(document => document.Kind == "LimitRange");
}
private static YamlMappingNode LimitRangeRule()
{
return LimitRange()
.MappingSequence("spec", "limits")
.Single();
}
private static IReadOnlySet<string> HardLimitKeys(ManifestDocument document)
{
var hard = ManifestNodeExtensions.Mapping(document.Root, "spec", "hard")
?? throw new InvalidOperationException($"{document.Descriptor} is missing spec.hard.");
return hard.Children.Keys
.OfType<YamlScalarNode>()
.Select(key => key.Value)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Cast<string>()
.ToHashSet(StringComparer.Ordinal);
}
private static IReadOnlyDictionary<string, string> ResourceQuotaLabels()
{
return Labels(ResourceQuota());
}
private static IReadOnlyDictionary<string, string> LimitRangeLabels()
{
return Labels(LimitRange());
}
private static IReadOnlyDictionary<string, string> Labels(ManifestDocument document)
{
var labels = ManifestNodeExtensions.Mapping(document.Root, "metadata", "labels")
?? throw new InvalidOperationException($"{document.Descriptor} is missing metadata.labels.");
return labels.Children
.Where(entry => entry.Key is YamlScalarNode && entry.Value is YamlScalarNode)
.ToDictionary(
entry => ((YamlScalarNode)entry.Key).Value ?? string.Empty,
entry => ((YamlScalarNode)entry.Value).Value ?? string.Empty,
StringComparer.Ordinal);
}
}

View File

@@ -234,7 +234,7 @@ public sealed class FleetManifestLintTests
{ {
deployments.Should().ContainKey(expectedRunner.Key); deployments.Should().ContainKey(expectedRunner.Key);
var container = deployments[expectedRunner.Key].ContainerMappings().Should().ContainSingle().Subject; var container = RunnerContainer(deployments[expectedRunner.Key]);
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value); EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
EnvValue(container, "EPHEMERAL").Should().Be("true"); EnvValue(container, "EPHEMERAL").Should().Be("true");
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux"); EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
@@ -250,7 +250,7 @@ public sealed class FleetManifestLintTests
{ {
foreach (var deployment in GitHubRunnerDeployments().Values) foreach (var deployment in GitHubRunnerDeployments().Values)
{ {
var container = deployment.ContainerMappings().Should().ContainSingle().Subject; var container = RunnerContainer(deployment);
foreach (var expectedEnv in WritableRunnerEnv) foreach (var expectedEnv in WritableRunnerEnv)
{ {
@@ -430,7 +430,6 @@ public sealed class FleetManifestLintTests
var expectedFiles = new[] var expectedFiles = new[]
{ {
"1password-item.yaml", "1password-item.yaml",
"argocd-application.yaml",
"certificate-web.yaml", "certificate-web.yaml",
"clusterrole-operator.yaml", "clusterrole-operator.yaml",
"clusterrolebinding-operator.yaml", "clusterrolebinding-operator.yaml",
@@ -586,17 +585,15 @@ public sealed class FleetManifestLintTests
} }
[Fact] [Fact]
public void FcDeviceManagement_ArgocdApplicationMustMatchApplicationSetDiscoveryConventions() public void FcDeviceManagement_MustRelyOnApplicationSetDiscovery()
{ {
var application = FcDeviceManagementDocuments() FcDeviceManagementDocuments()
.Single(document => document.Kind == "Application" && document.Name == "infra-fc-devicemgmt");
application.Namespace.Should().Be("argocd");
application.Scalar("spec", "source", "repoURL")
.Should() .Should()
.Be("http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git"); .NotContain(document => document.Kind == "Application", "the root ApplicationSet owns apps/fc-devicemgmt discovery");
application.Scalar("spec", "source", "path").Should().Be("apps/fc-devicemgmt");
application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt"); FcDeviceManagementDocuments()
.Should()
.Contain(document => document.Kind == "Namespace" && document.Name == "fc-devicemgmt");
} }
private static IEnumerable<string> ProbeViolations( private static IEnumerable<string> ProbeViolations(
@@ -631,6 +628,12 @@ public sealed class FleetManifestLintTests
.ToDictionary(document => document.Name, StringComparer.Ordinal); .ToDictionary(document => document.Name, StringComparer.Ordinal);
} }
private static YamlMappingNode RunnerContainer(ManifestDocument deployment)
{
return deployment.ContainerMappings()
.Single(container => string.Equals(ManifestNodeExtensions.Scalar(container, "name"), "runner", StringComparison.Ordinal));
}
private static int ReplicaCount(ManifestDocument document) private static int ReplicaCount(ManifestDocument document)
{ {
return int.TryParse(document.Scalar("spec", "replicas"), out var replicas) ? replicas : 1; return int.TryParse(document.Scalar("spec", "replicas"), out var replicas) ? replicas : 1;