certsync: pin the bucket side of the settled stamp and the wildcard↔mirror wiring
Mutation probe on round 2: forcing the stamp's bucket side off survived — an entry settled in sync that later left the bucket would never republish, #291 by another door. Now tested. The edge.go wiring of the wildcard manager's parent set into the mirror was also uncovered (every backfill test set it by hand); it is now attachWildcardToMirror, one place with its own test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 files changed,
+63
−6
internal/edge/certbackfill_test.go+49 −0internal/edge/certsync.go+13 −0internal/edge/edge.go+1 −6
diff --git a/internal/edge/certbackfill_test.go b/internal/edge/certbackfill_test.go| index 9cb8e2f7f..30f7e6e6e 100644 |
| --- a/internal/edge/certbackfill_test.go |
| +++ b/internal/edge/certbackfill_test.go |
| @@ -7,6 +7,7 @@ import ( |
| "log" |
| "os" |
| "path/filepath" |
| + "slices" |
| "strings" |
| "sync" |
| "testing" |
| @@ -455,3 +456,51 @@ func TestCertMirrorLoopBackfillsOnTheIssuer(t *testing.T) { |
| close(stop) |
| <-done |
| } |
| + |
| +// The bucket side is part of the stamp: an entry settled as in sync that |
| +// later leaves the bucket (a prune, a mis-delete) republishes on the next |
| +// pass even though the file has not moved — otherwise #291's failure mode |
| +// returns by a different door. |
| +func TestBackfillRepublishesAnEntryThatLeftTheBucket(t *testing.T) { |
| + log.SetOutput(io.Discard) |
| + t.Cleanup(func() { log.SetOutput(os.Stderr) }) |
| + s := release.NewDirStore(t.TempDir()) |
| + local := autocert.DirCache(t.TempDir()) |
| + putLocal(t, local, "a.example.com", hostCertPEM(t, "a.example.com", time.Now().Add(time.Hour))) |
| + ctx := context.Background() |
| + m := issuerMirror(t, s, local, true) |
| + m.backfill(ctx) |
| + if err := m.Refresh(ctx); err != nil { |
| + t.Fatal(err) |
| + } |
| + m.backfill(ctx) // settles in sync |
| + if err := s.Delete(ctx, certKey("a.example.com")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := m.Refresh(ctx); err != nil { |
| + t.Fatal(err) |
| + } |
| + m.backfill(ctx) |
| + if _, ok := bucketHas(t, s, "a.example.com"); !ok { |
| + t.Fatal("an entry that left the bucket was never republished") |
| + } |
| +} |
| + |
| +// The production wiring: the wildcard manager's parent set reaches the |
| +// mirror, or every wildcard PEM would silently never backfill. |
| +func TestAttachWildcardToMirrorWiresParentsAndWake(t *testing.T) { |
| + w := &wildcardManager{parents: []string{"acct.example.com"}} |
| + m := newCertMirror(release.NewDirStore(t.TempDir()), nil) |
| + attachWildcardToMirror(m, w) |
| + if m.wildcardParents == nil || !slices.Equal(m.wildcardParents(), []string{"acct.example.com"}) { |
| + t.Fatal("wildcardParents not wired to the manager's current set") |
| + } |
| + if m.wake == nil { |
| + t.Fatal("wake not wired") |
| + } |
| + attachWildcardToMirror(nil, w) // no mirror: no panic |
| + attachWildcardToMirror(m, nil) // no manager: leaves the wiring alone |
| + if m.wildcardParents == nil { |
| + t.Fatal("a nil manager cleared the wiring") |
| + } |
| +} |
diff --git a/internal/edge/certsync.go b/internal/edge/certsync.go| index cf85421be..dcb41069a 100644 |
| --- a/internal/edge/certsync.go |
| +++ b/internal/edge/certsync.go |
| @@ -534,6 +534,19 @@ func (e *Edge) newCertMirror(local autocert.Cache) *certMirror { |
| return m |
| } |
| +// attachWildcardToMirror is the mirror↔wildcard-manager wiring, in one place |
| +// so a test can see it: a wildcard the ISSUER minted reaches this box's |
| +// handshakes on the next poll rather than on the manager's 12h tick (wake), |
| +// and the manager's current parent set bounds which wildcard PEMs the |
| +// issuer's backfill may publish. Either side nil is a no-op. |
| +func attachWildcardToMirror(m *certMirror, w *wildcardManager) { |
| + if m == nil || w == nil { |
| + return |
| + } |
| + m.wake = w.refresh |
| + m.wildcardParents = w.currentParents |
| +} |
| + |
| func newCertMirror(s release.Store, local autocert.Cache) *certMirror { |
| return &certMirror{ |
| s: s, |
diff --git a/internal/edge/edge.go b/internal/edge/edge.go| index c0fa82507..3e4e35d7a 100644 |
| --- a/internal/edge/edge.go |
| +++ b/internal/edge/edge.go |
| @@ -2380,12 +2380,7 @@ func RunWithActivatorReady(args []string, act Activator, cache *Cache, ready fun |
| if w := newWildcardFromEnv(e, certCache); w != nil { |
| getCert = wrapGetCertificate(w, getCert) |
| e.OnReload(w.refresh) |
| - if mirror != nil { |
| - // A wildcard the ISSUER minted reaches this box's handshakes on the |
| - // next poll rather than on this manager's 12h tick. |
| - mirror.wake = w.refresh |
| - mirror.wildcardParents = w.currentParents |
| - } |
| + attachWildcardToMirror(mirror, w) |
| w.refresh() |
| go w.run() |
| log.Printf("edge: wildcard certs on (static %v, apps domain %q, delegation zone %q)", |