From f830325ca31aa97efe2a7111c39bed3b8634fa7c Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 9 Jul 2026 23:58:30 +0200 Subject: [PATCH] =?UTF-8?q?offsiteapply:=20include=20QuotaGB=20in=20the=20?= =?UTF-8?q?descriptor=20hash=20=E2=80=94=20quota=20changes=20re-apply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A hub-side quota raise now reaches the target: the marker hash changes and the bridge re-applies via key-auth-first (no password consumed). Test: quota-only change remaps the new quota with a panicking consumer. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6 --- .../internal/offsiteapply/offsiteapply.go | 8 +++++--- .../offsiteapply/offsiteapply_test.go | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/controller/internal/offsiteapply/offsiteapply.go b/controller/internal/offsiteapply/offsiteapply.go index a8e35f1..9e0f343 100644 --- a/controller/internal/offsiteapply/offsiteapply.go +++ b/controller/internal/offsiteapply/offsiteapply.go @@ -72,10 +72,12 @@ func (b *Bridge) logf(f string, a ...any) { } } -// descriptorHash is the applied-marker key: a hash of the identity-bearing descriptor fields. A change -// (re-provision → new host/user/fingerprint) yields a new hash → the bridge re-applies (new password). +// descriptorHash is the applied-marker key: a hash of the APPLY-RELEVANT descriptor fields. A change +// (re-provision → new host/user/fingerprint, or a quota adjustment) yields a new hash → the bridge +// re-applies. QuotaGB is included (SLICE 4) so a hub-side quota raise reaches the target — on an +// already-provisioned guest that re-apply is a cheap key-auth-first re-pin (no password consumed). func descriptorHash(o config.OffsiteConfig) string { - s := fmt.Sprintf("%s|%s|%s|%d|%s|%s", o.Type, o.Host, o.User, o.Port, o.RepoPath, o.HostFingerprint) + s := fmt.Sprintf("%s|%s|%s|%d|%s|%s|%d", o.Type, o.Host, o.User, o.Port, o.RepoPath, o.HostFingerprint, o.QuotaGB) sum := sha256.Sum256([]byte(s)) return hex.EncodeToString(sum[:]) } diff --git a/controller/internal/offsiteapply/offsiteapply_test.go b/controller/internal/offsiteapply/offsiteapply_test.go index c4a98a0..d4fc58c 100644 --- a/controller/internal/offsiteapply/offsiteapply_test.go +++ b/controller/internal/offsiteapply/offsiteapply_test.go @@ -144,6 +144,25 @@ func TestBridge_AppliesEndToEnd(t *testing.T) { } } +// SLICE 4 — a quota-only descriptor change re-applies (the hash includes QuotaGB), and with a working +// key it costs no password: key-auth-first re-pins + remaps the quota. +func TestBridge_QuotaChangeReappliesWithoutConsume(t *testing.T) { + b, cons, _, en, _ := newBridge(t, goodOffsite()) + cons.panics = true + b.Prober = &fakeProber{pem: "EXISTINGPEM", ok: true} + // marker for the OLD quota (25) already applied; the descriptor now says 50 + old := b.Cfg.Offsite + old.QuotaGB = 25 + _ = os.MkdirAll(filepath.Dir(b.MarkerPath), 0o700) + _ = os.WriteFile(b.MarkerPath, []byte(descriptorHash(old)), 0o600) + if err := b.Reconcile(context.Background()); err != nil { + t.Fatalf("quota-change reconcile: %v", err) + } + if en.calls != 1 || en.gotQuotaGB != 50 { + t.Fatalf("a quota raise must re-apply and map the NEW quota (no consume): %+v", en) + } +} + // Key-auth-first (Scenario B) — the existing key still works: NO consume, NO install; re-verify + re-pin + // reconfigure with the EXISTING key, marker updated. func TestBridge_KeyAuthFirstSkipsConsume(t *testing.T) {