Make registry.bstein.dev/bstein/hermes-webui a linux/amd64 + linux/arm64 manifest list so the agent pod's `hux` sidecar (which runs the webui image) can schedule onto the amd64 node titan-22. Reuses the hermes-agent multi-arch pattern already on main. - Dockerfile.hermes-webui: repoint both FROMs to multi-arch, internal sources. The upstream WebUI base (ghcr sha256:a83a3893..., already a multi-arch OCI index) is now pulled from the in-cluster Harbor mirror; the agent base moves from the retired arm64-only leaf (81970563) to the multi-arch agent index (a68d1c4d). Kaniko selects the matching arch leaf per build node. - services/harbor/hermes-webui-base-mirror-job.yaml: new suspended, operator-run skopeo `copy --all` Job mirroring the upstream WebUI base index into Harbor's `mirror` project (modeled on hermes-agent-base-mirror-job.yaml; reuses the generic ensure-project helper). Wired into the harbor kustomization. - Jenkinsfile.hermes-webui-image: arm64 leg (titan-20) + amd64 leg (titan-24, hostname+arch pin, toleration Exists, resource-capped, own checkout scm) + Combine multi-arch index stage; per-arch evidence archived alongside the index. - hermes_multiarch_combine.py: generalize the destination pattern/component to serve both hermes-agent and hermes-webui (fail-closed to just those two). - Tests updated to the two-arch topology (two legs, combine, both FROM bases, the mirror Job, twelve archived evidence files). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
153 lines
6.5 KiB
YAML
153 lines
6.5 KiB
YAML
# services/harbor/hermes-webui-base-mirror-job.yaml
|
|
#
|
|
# One-shot mirror of the upstream Hermes WebUI base image INDEX from ghcr.io into
|
|
# the in-cluster Harbor "mirror" project, so the reviewed image build
|
|
# (dockerfiles/Dockerfile.hermes-webui) pulls its `AS webui` FROM base internally
|
|
# with no ghcr.io fallback (ghcr/docker.io egress is IPv6-broken/flaky from build
|
|
# pods, which is why the agent base was mirrored the same way).
|
|
#
|
|
# The upstream digest sha256:a83a3893... is ALREADY a multi-arch OCI index
|
|
# (amd64 sha256:54fd4990..., arm64 sha256:9094ae6a...). `skopeo copy --all` is
|
|
# content-addressed, so it reproduces the identical index and both arch leaves in
|
|
# Harbor -- the build stays digest-pinned AND multi-arch.
|
|
#
|
|
# Kept `suspend: true` exactly like hermes-agent-base-mirror-job.yaml: it needs
|
|
# egress to ghcr.io and is only run deliberately, once per base-digest bump. To
|
|
# run it, an operator clears suspend (or `kubectl create job --from`) AFTER
|
|
# updating the digest in BOTH args below and in the Dockerfile `AS webui` FROM.
|
|
apiVersion: batch/v1
|
|
kind: Job
|
|
metadata:
|
|
name: harbor-hermes-webui-base-mirror-1
|
|
namespace: harbor
|
|
spec:
|
|
suspend: true
|
|
backoffLimit: 2
|
|
activeDeadlineSeconds: 1800
|
|
template:
|
|
metadata:
|
|
annotations:
|
|
vault.hashicorp.com/agent-inject: "true"
|
|
vault.hashicorp.com/agent-pre-populate-only: "true"
|
|
# This Job has an app init container (ensure-project) that reads the
|
|
# injected secret, so Vault's init must run FIRST — otherwise it is
|
|
# appended after ensure-project and the secret file isn't written yet.
|
|
vault.hashicorp.com/agent-init-first: "true"
|
|
vault.hashicorp.com/agent-run-as-user: "65532"
|
|
vault.hashicorp.com/agent-run-as-group: "65532"
|
|
vault.hashicorp.com/role: harbor-policy-bootstrap
|
|
vault.hashicorp.com/agent-inject-secret-harbor-admin-password: kv/data/atlas/harbor/harbor-core
|
|
vault.hashicorp.com/agent-inject-template-harbor-admin-password: |
|
|
{{- with secret "kv/data/atlas/harbor/harbor-core" -}}
|
|
{{ .Data.data.harbor_admin_password }}
|
|
{{- end -}}
|
|
spec:
|
|
serviceAccountName: harbor-policy-bootstrap
|
|
enableServiceLinks: false
|
|
restartPolicy: Never
|
|
nodeSelector:
|
|
hardware: rpi5
|
|
kubernetes.io/arch: arm64
|
|
node-role.kubernetes.io/worker: "true"
|
|
affinity:
|
|
nodeAffinity:
|
|
requiredDuringSchedulingIgnoredDuringExecution:
|
|
nodeSelectorTerms:
|
|
- matchExpressions:
|
|
- key: kubernetes.io/hostname
|
|
operator: NotIn
|
|
values: [titan-04, titan-14, titan-18, titan-19, titan-24]
|
|
securityContext:
|
|
fsGroup: 65532
|
|
fsGroupChangePolicy: OnRootMismatch
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
initContainers:
|
|
# Ensure the public "mirror" project exists before skopeo tries to push
|
|
# into it (Harbor only auto-creates repositories inside an existing
|
|
# project). Reuses the exact same generic ensure-project helper and
|
|
# Vault-injected admin password as the sibling agent base-mirror Job --
|
|
# no new credential and no new project is introduced.
|
|
- name: ensure-project
|
|
image: docker.io/library/python@sha256:efcdfa6a6b2fd2afb9c7dfa9a5b288a6f68338b5cfdebe6b637d986067d85757
|
|
imagePullPolicy: IfNotPresent
|
|
command: [python3, /scripts/harbor_mirror_project_ensure.py]
|
|
env:
|
|
- name: HARBOR_API_ORIGIN
|
|
value: http://harbor-core.harbor.svc.cluster.local/api/v2.0
|
|
- name: HARBOR_ADMIN_PASSWORD_FILE
|
|
value: /vault/secrets/harbor-admin-password
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
readOnlyRootFilesystem: true
|
|
runAsGroup: 65532
|
|
runAsNonRoot: true
|
|
runAsUser: 65532
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
volumeMounts:
|
|
- name: scripts
|
|
mountPath: /scripts
|
|
readOnly: true
|
|
- name: tmp
|
|
mountPath: /tmp
|
|
resources:
|
|
requests: {cpu: 25m, memory: 32Mi}
|
|
limits: {cpu: 250m, memory: 128Mi}
|
|
containers:
|
|
# skopeo copies the WHOLE multi-arch index (--all) straight from ghcr.io
|
|
# to the internal Harbor registry. Source is the public upstream image, so
|
|
# no source credential is needed; the digest is asserted on both ends so a
|
|
# drifted upstream tag cannot be mirrored.
|
|
- name: mirror
|
|
image: quay.io/skopeo/stable@sha256:94f5c5e26997e2e78c234ec9abf19a391c234b39eb22e6d1210d0b527c97dcc8
|
|
imagePullPolicy: IfNotPresent
|
|
command: ["/bin/sh", "-c"]
|
|
args:
|
|
- |
|
|
set -eu
|
|
src="docker://ghcr.io/nesquena/hermes-webui@sha256:a83a3893111dcb250e7aa7aa657d3d6f4570b0e2fd00d9b7569246fc5e7339b2"
|
|
# Push to the EXTERNAL Harbor endpoint (valid TLS) — the same path
|
|
# kaniko already publishes to — to avoid harbor-core's internal
|
|
# HTTP/token-realm quirks. The image lands in the same Harbor
|
|
# 'mirror' project, so kaniko still pulls it internally via harbor-core.
|
|
dst="docker://registry.bstein.dev/mirror/hermes-webui@sha256:a83a3893111dcb250e7aa7aa657d3d6f4570b0e2fd00d9b7569246fc5e7339b2"
|
|
pw="$(cat /vault/secrets/harbor-admin-password)"
|
|
exec skopeo copy --all \
|
|
--src-tls-verify=true \
|
|
--dest-tls-verify=true \
|
|
--dest-creds "admin:${pw}" \
|
|
"${src}" "${dst}"
|
|
env:
|
|
- name: HOME
|
|
value: /tmp
|
|
- name: TMPDIR
|
|
value: /tmp
|
|
- name: XDG_RUNTIME_DIR
|
|
value: /tmp
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
readOnlyRootFilesystem: true
|
|
runAsGroup: 65532
|
|
runAsNonRoot: true
|
|
runAsUser: 65532
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
volumeMounts:
|
|
- name: tmp
|
|
mountPath: /tmp
|
|
resources:
|
|
requests: {cpu: 100m, memory: 256Mi}
|
|
limits: {cpu: "1", memory: 1Gi}
|
|
volumes:
|
|
- name: scripts
|
|
configMap:
|
|
name: harbor-hermes-webui-base-mirror-script
|
|
defaultMode: 0555
|
|
- name: tmp
|
|
emptyDir: {}
|