Loading internal/registrygate/gate.go +4 −0 Changes for internal/registrygate/gate.go: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -95,6 +95,10 @@ func New(upstream *url.URL, tokens map[string]string, log *slog.Logger) *Gate { g.health = &http.Client{Timeout: 3 * time.Second} g.maint.now = time.Now g.maint.quiet = DefaultQuiet // A gate that just started cannot tell whether a push was mid-way through the // previous one, and the installer pushes right after the registry rolls out: // count the start as a write, so the first window waits for quiet too. g.maint.lastWrite = g.maint.now() return g } Loading internal/registrygate/maint_test.go +19 −0 Changes for internal/registrygate/maint_test.go: 19 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ func newMaintGate(t *testing.T) (*Gate, *httptest.Server, *httptest.Server, *ups }, nil) clk := &clock{t: time.Date(2026, 9, 24, 3, 0, 0, 0, time.UTC)} g.maint.now = clk.now g.maint.lastWrite = time.Time{} gs := httptest.NewServer(g) t.Cleanup(gs.Close) ms := httptest.NewServer(g.MaintHandler()) Loading Loading @@ -276,3 +277,21 @@ func TestManifestIndexListsUntaggedRevisions(t *testing.T) { } } } // A gate that just started grants no window before a quiet period has passed: // the installer pushes right after the registry rolls out. func TestReadOnlyWindowWaitsAfterStart(t *testing.T) { up := httptest.NewServer(http.NotFoundHandler()) t.Cleanup(up.Close) target, _ := url.Parse(up.URL) g := New(target, nil, nil) ms := httptest.NewServer(g.MaintHandler()) t.Cleanup(ms.Close) if code := post(t, ms, "/readonly?lease=60"); code != http.StatusConflict { t.Fatalf("read-only window right after start = %d, want 409", code) } g.SetQuiet(0) if code := post(t, ms, "/readonly?lease=60"); code != http.StatusOK { t.Fatalf("read-only window once quiet = %d, want 200", code) } } Loading
internal/registrygate/gate.go +4 −0 Changes for internal/registrygate/gate.go: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -95,6 +95,10 @@ func New(upstream *url.URL, tokens map[string]string, log *slog.Logger) *Gate { g.health = &http.Client{Timeout: 3 * time.Second} g.maint.now = time.Now g.maint.quiet = DefaultQuiet // A gate that just started cannot tell whether a push was mid-way through the // previous one, and the installer pushes right after the registry rolls out: // count the start as a write, so the first window waits for quiet too. g.maint.lastWrite = g.maint.now() return g } Loading
internal/registrygate/maint_test.go +19 −0 Changes for internal/registrygate/maint_test.go: 19 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ func newMaintGate(t *testing.T) (*Gate, *httptest.Server, *httptest.Server, *ups }, nil) clk := &clock{t: time.Date(2026, 9, 24, 3, 0, 0, 0, time.UTC)} g.maint.now = clk.now g.maint.lastWrite = time.Time{} gs := httptest.NewServer(g) t.Cleanup(gs.Close) ms := httptest.NewServer(g.MaintHandler()) Loading Loading @@ -276,3 +277,21 @@ func TestManifestIndexListsUntaggedRevisions(t *testing.T) { } } } // A gate that just started grants no window before a quiet period has passed: // the installer pushes right after the registry rolls out. func TestReadOnlyWindowWaitsAfterStart(t *testing.T) { up := httptest.NewServer(http.NotFoundHandler()) t.Cleanup(up.Close) target, _ := url.Parse(up.URL) g := New(target, nil, nil) ms := httptest.NewServer(g.MaintHandler()) t.Cleanup(ms.Close) if code := post(t, ms, "/readonly?lease=60"); code != http.StatusConflict { t.Fatalf("read-only window right after start = %d, want 409", code) } g.SetQuiet(0) if code := post(t, ms, "/readonly?lease=60"); code != http.StatusOK { t.Fatalf("read-only window once quiet = %d, want 200", code) } }