Syncthing service on Milner #21
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner#21
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Naina:
Scope update — the annia pod is now the first consumer (2026-09-28 session)
This issue was "define the IaC code to create a pod running syncthing on Milner". That is still the core, but the pod acquires its purpose and a hard consumer, so the scope below is what the plan should cover. The body's one-line framing is superseded; the issue is not.
1. What this ships
An always-on Syncthing pod in
annia, running the official upstream image at a pinned tag, on a singlepersistent-classReadWriteOncePVC that holds one folder: the operator's~/.pi— Syncthing's folder idnaccs-5znau, labelpi. Send/receive, with the operator'sdima(macOS) and this host (fedora) as the two registered peers. That is the whole of it: a service, a claim, a folder, and the peers.2. Why it is now load-bearing
The annia pod (milner#25) mounts that same PVC at
/root/.pi, whole-volume, no subPath./root/.piis not an annia artefact — it is the operator's folder, ~1.1 GB of pi credentials, skills, extensions and session history — and the decision is that the pod gets the real thing rather than a copy or a subset. Syncthing is the transport that keeps the pod's copy and the operator's machines one folder. Concretely:node-role: workerfor both; there is one worker today, so it is free — but it is a real constraint the day a second appears, and it should be written down rather than assumed.3. Calls the plan must settle (they are this issue's, not #25's)
cert.pem/key.pemare the device ID: if they are regenerated every start, every peer has to be re-approved. So the identity (and the folder/peer configuration, i.e.config.xml) is a credential → aVaultStaticSecretsyncingsecret/syncthing/<name>into a mount the daemon reads, perAGENTS.md's Vault-first rule — with the named-path policy and kubernetes-auth role for the namespace that the rule requires. No hand-runkubectl create secret. The GUI/API password is credential material too and belongs in the same path.<ignore>entries and already carries two*.sync-conflict-*files (both session JSONLs), i.e. the conflict rate under send/receive is non-zero before a second writer exists. Decide what is ignored — the obvious candidate is anything machine-specific:agent/bin/{fd,rg}on this host are Linux aarch64 ELFs but would be mac binaries ondima, so syncing them bidirectionally is a rotation-restore problem rather than a sync. The session-log conflicts are the other candidate. Neither is #25's to decide.baselinePSA as elsewhere inannia: no added capabilities, no privileged containers, no hostPath.4. Effect on other issues
/root/.piline is this issue's PVC, so #25 cannot meet its live criteria until this ships.persistentclaim, not anix-class mount.5. Verification
A file written at
/root/.pi/<name>inside the pod appears under~/.pi/<name>on the operator's host within the sync interval, and vice versa; the device ID survives a pod restart (identity from Vault), and a restart does not re-add or re-approve either peer.Label note: with the PVC, the Vault-synced identity, the ownership decision and the folder policy in scope,
effort/lowlooks too small —effort/mediumis the honest estimate, pending the plan.Naina:
Overview
Ship an always-on Syncthing pod in
annia— upstreamsyncthing/syncthing:2.1.5, onepersistentRWO claim holding the operator's~/.pi(foldernaccs-5znau), identity +config.xml+ GUI password Vault-synced fromsecret/syncthing/config. The "IaC" is Flux GitOps manifests undergitops/clusters/milner/, not Terraform. This unblocks milner#25, whose/root/.piis this claim mounted whole-volume.Technical Approach
Add the
VaultStaticSecrettogitops/clusters/milner/secrets/annia-secrets.yaml(copyannia-secretsshape +irc-oper's rollout argument):12Widen
annia-readby one exact path indocs/vault.md§4c-ter (kv-v2 policies need thedata/segment):3The
anniakubernetes-auth role already exists (vaultauth-annia.yaml) and already carriesannia-read, so the role andset-credential.sh's single-policySNIPPET_ROLEstay untouched.56New producer script
gitops/clusters/milner/apps/annia/set-syncthing-credential.sh(sibling ofset-credential.sh,set-herald-credential.shprecedent):78Generate the identity offline (runbook
docs/syncthing.md):All three files are generated together (the local
<device>id must matchcert.pem); the Vault-held copy is the source of truth, GUI edits are transient by design.PVC
apps/annia/syncthing-pvc.yaml— mirrorapps/irc/persistentvolumeclaim.yaml:10Deployment
apps/annia/syncthing.yaml:No
hostNetwork/hostPort/hostPath— all three are banned by the namespace'sbaselinePSA anyway.13Two Services in
apps/annia/syncthing-services.yaml:Without (2) sync still works — global discovery + relays are outbound-only from the pod — but the 1.1 GB first sync and
dimatraffic ride public relays. Local discovery (21027) never crosses subnets, so nothing listens for it.Ignore policy lands before the second writer: new file
apps/annia/stignore, wired viaconfigMapGenerator(hashed name rolls the pod, irc precedent):.stignoreis per-device and never synced, sodima/fedoraget the same three lines once via their GUIs (runbook), and the two existing*.sync-conflict-*JSONLs are reviewed/removed before the pod's first sync. Session JSONLs stay synced — #25 needs session history; the existing conflicts are adima↔fedoraproblem, not this pod's.List the new resources in
apps/annia/kustomization.yamlwith the house-style comment block (placement argument + co-scheduling write-down).Tests (counts + one new behaviour test, details below).
Docs + story:
docs/syncthing.md(§19: generate → write Vault → ignore/conflict steps → verification),OPERATIONS.mdindex row,docs/tailscale.mdautoApprovers snippet gains"svc:syncthing", andUSER_STORIES.mdgains story 24 under "As the operator of the cluster":Live rollout order (runbook —
apps.yamlhaswait: true, so the Vault value must exist before the Deployment merges, the failureset-credential.sh's header documents):Implementation Details
Modify
gitops/clusters/milner/apps/annia/kustomization.yaml— 4 resources + commentgitops/clusters/milner/secrets/annia-secrets.yaml—syncthing-configVSSdocs/vault.md— §4c-terannia-readgains the exact pathtests/test_gitops.py—14 → 15intest_every_vault_static_secret_path_has_a_producerand(9, 14) → (9, 15)intest_vault_crs_match_the_vso_crd_shape1516docs/tailscale.md— autoApprovers snippet +svc:syncthingnoteOPERATIONS.md— index row;USER_STORIES.md— story 24Create
apps/annia/syncthing.yaml,syncthing-pvc.yaml,syncthing-services.yaml,stignore,set-syncthing-credential.sh,docs/syncthing.mdConstraints
syncthing/syncthing:2.1.5, multi-arch incl.linux/arm64)1718; resources on main and init containers (policy/k8s.rego covers both).secrets/stays CRs-only; VSS keysconfig.xml/cert.pem/key.pemare valid Secret keys and mount as exact filenames.baselinePSA: no added capabilities, no privileged, no hostPath/hostNetwork/hostPort.appslayer.Out-of-scope notes
:8384is the accepted state.podAffinity(mutual required affinity deadlocks) that hardens co-scheduling when a second worker appears. TodaynodeSelector: node-role: workeron both +WaitForFirstConsumerPV node-affinity is the actual guarantee, written into the manifest comments here..woodpecker/gates.yamlchanges;effort/lowshould becomeeffort/mediumper the issue's own note (scheduler's relabel).Acceptance Criteria
pytest tests/, kustomize render, kubeconform, conftest — including both count assertions deliberately moved to 15 VaultStaticSecrets.secret/syncthing/confighas a tracked producer;annia-readgrants exactlysecret/data/syncthing/config; VSO Secretsyncthing-configis Ready inanniaand the pod reaches Ready on the worker./pi/<name>in the pod appears at~/.pi/<name>ondimaandfedoraand vice versa; the device ID survives a pod restart with no peer re-approval; the three.stignorecopies and the conflict-file remediation are done before the pod's first sync.Testing Plan
test_every_vault_static_secret_path_has_a_producer— bump to 15; passes viaSYNCTHING_PATH=in the new script.test_vault_crs_match_the_vso_crd_shape— bump to(9, 15); closed-key-set check coversrolloutRestartTargets.test_syncthing_is_recreate_rwo_and_vault_seeded(renderedapps/annia, house-style long docstring): assertsstrategy: Recreate,accessModes == [ReadWriteOnce],nodeSelector: node-role: worker, init container copying exactlyconfig.xml/cert.pem/key.pem, nohostPath/hostNetwork/hostPort, pinned tag, resources on every container.test_kustomize_build,test_kubeconform,test_all_namespaces_have_psa_enforce,test_no_secrets_with_literal_data— unchanged, must stay green.Related Issues/PRs
/root/.pi; blocked by this issue.~/.pistory), unaffected — this is an ordinarypersistentclaim, not anix-class mount.Research Findings
syncthing/syncthing:2.1.5(2026-09-08, multi-arch incl.linux/arm64); the image defines noUSERand its entrypointsu-execs toPUID:PGID(default 1000) after a root start.config.xmlorcert.pem/key.pemmakes Syncthing mint a fresh device ID on first start — identity must be present before every start, yet the config dir must stay writable (csrftokens.txt, rewrites), so init-copy from the Vault Secret into anemptyDir, never a read-only Secret mount.config.xml(syncthing generate --gui-passwordhashes it);/rest/noauth/healthis unauthenticated and maps directly to the readiness probe.config.xmletc. mount as exact filenames; Syncthing never hot-reloads config, sorolloutRestartTargetsis the right rotation mechanism (andsubPathmounts would silently skip updates).data/prefix; a role takes N policies — but the smallest change keepingsecret/syncthing/<name>is one exact-path block inannia-read, per theirc-readprecedent..stignorelives at the folder root, is per-device, never synced;config.xml's<ignores>only templates auto-accepted folders, and conflicts are resolved by manual review (three-way merge is the documented technique for JSONLs).LoadBalancerService on 22000/TCP (IRC precedent), andbaselinePSA forbids hostNetwork/hostPort/hostPath outright.fsGroupdoes not chown hostPath-backed claims and user namespaces are per-pod opt-in, so the pod's uid is a deliberate choice; host-side ownership is unaffected either way because Syncthing syncs content, not ownership.persistentclass'sWaitForFirstConsumerPV node-affinity already co-locates any pod mounting the claim.