detached bbc425b

fix: make registry push cleanup per-call to prevent loss on repeated pushes

The cleanup callback stored on the Pusher was cleared after each Push call,
causing user-provided cleanup (WithImageCleanup) to be lost on subsequent
pushes. Changed the internal imageSource to return cleanup per-call as a
local variable, keeping the user cleanup immutable on the receiver.
commit
bbc425bc31fcd58ce9bfcaf38360d8daf1af8fbf
author
brice · · 2026-03-15 07:07 UTC
parent
18c86d4
signature
unsigned
2 files · +17 -17
M pkg/registrypush/push.go +11 -14
@@ -39,7 +39,7 @@ type Pusher struct {
3939 timeout time.Duration
4040 transport http.RoundTripper
4141 insecureTLS bool
42- imageSource ImageSource
42+ imageSource func(ctx context.Context, ref string) (v1.Image, func(), error)
4343 cleanupFn func()
4444 progress io.Writer
4545 }
@@ -70,7 +70,12 @@ func WithInsecureTLS(insecure bool) Option {
7070
7171 // WithImageSource overrides the default daemon-based image reader.
7272 func WithImageSource(src ImageSource) Option {
73- return func(p *Pusher) { p.imageSource = src }
73+ return func(p *Pusher) {
74+ p.imageSource = func(ctx context.Context, ref string) (v1.Image, func(), error) {
75+ img, err := src(ctx, ref)
76+ return img, nil, err
77+ }
78+ }
7479 }
7580
7681 // WithImageCleanup sets a function called after Push completes to clean up
@@ -98,13 +103,8 @@ func New(opts ...Option) *Pusher {
98103 }
99104 }
100105 if p.imageSource == nil {
101- p.imageSource = func(ctx context.Context, ref string) (v1.Image, error) {
102- img, cleanup, err := defaultImageSourceWithCleanup(ctx, ref)
103- if err != nil {
104- return nil, err
105- }
106- p.cleanupFn = cleanup
107- return img, nil
106+ p.imageSource = func(ctx context.Context, ref string) (v1.Image, func(), error) {
107+ return defaultImageSourceWithCleanup(ctx, ref)
108108 }
109109 }
110110 return p
@@ -125,19 +125,16 @@ func (p *Pusher) Push(ctx context.Context, ref string) error {
125125 return fmt.Errorf("image ref %s must include a tag", ref)
126126 }
127127
128- userCleanup := p.cleanupFn
129- p.cleanupFn = nil
130-
131128 authHeader, err := resolveAuthHeader(parsedRef)
132129 if err != nil {
133130 return err
134131 }
135132
136- img, err := p.imageSource(ctx, ref)
133+ img, sourceCleanup, err := p.imageSource(ctx, ref)
137134 if err != nil {
138135 return err
139136 }
140- sourceCleanup := p.cleanupFn
137+ userCleanup := p.cleanupFn
141138 switch {
142139 case sourceCleanup != nil && userCleanup != nil:
143140 defer func() {
@@ -39,7 +39,7 @@ type Pusher struct {
39 timeout time.Duration39 timeout time.Duration
40 transport http.RoundTripper40 transport http.RoundTripper
41 insecureTLS bool41 insecureTLS bool
42- imageSource ImageSource42+ imageSource func(ctx context.Context, ref string) (v1.Image, func(), error)
43 cleanupFn func()43 cleanupFn func()
44 progress io.Writer44 progress io.Writer
45 }45 }
@@ -70,7 +70,12 @@ func WithInsecureTLS(insecure bool) Option {
70 70
71 // WithImageSource overrides the default daemon-based image reader.71 // WithImageSource overrides the default daemon-based image reader.
72 func WithImageSource(src ImageSource) Option {72 func WithImageSource(src ImageSource) Option {
73- return func(p *Pusher) { p.imageSource = src }73+ return func(p *Pusher) {
74+ p.imageSource = func(ctx context.Context, ref string) (v1.Image, func(), error) {
75+ img, err := src(ctx, ref)
76+ return img, nil, err
77+ }
78+ }
74 }79 }
75 80
76 // WithImageCleanup sets a function called after Push completes to clean up81 // WithImageCleanup sets a function called after Push completes to clean up
@@ -98,13 +103,8 @@ func New(opts ...Option) *Pusher {
98 }103 }
99 }104 }
100 if p.imageSource == nil {105 if p.imageSource == nil {
101- p.imageSource = func(ctx context.Context, ref string) (v1.Image, error) {106+ p.imageSource = func(ctx context.Context, ref string) (v1.Image, func(), error) {
102- img, cleanup, err := defaultImageSourceWithCleanup(ctx, ref)107+ return defaultImageSourceWithCleanup(ctx, ref)
103- if err != nil {
104- return nil, err
105- }
106- p.cleanupFn = cleanup
107- return img, nil
108 }108 }
109 }109 }
110 return p110 return p
@@ -125,19 +125,16 @@ func (p *Pusher) Push(ctx context.Context, ref string) error {
125 return fmt.Errorf("image ref %s must include a tag", ref)125 return fmt.Errorf("image ref %s must include a tag", ref)
126 }126 }
127 127
128- userCleanup := p.cleanupFn
129- p.cleanupFn = nil
130-
131 authHeader, err := resolveAuthHeader(parsedRef)128 authHeader, err := resolveAuthHeader(parsedRef)
132 if err != nil {129 if err != nil {
133 return err130 return err
134 }131 }
135 132
136- img, err := p.imageSource(ctx, ref)133+ img, sourceCleanup, err := p.imageSource(ctx, ref)
137 if err != nil {134 if err != nil {
138 return err135 return err
139 }136 }
140- sourceCleanup := p.cleanupFn137+ userCleanup := p.cleanupFn
141 switch {138 switch {
142 case sourceCleanup != nil && userCleanup != nil:139 case sourceCleanup != nil && userCleanup != nil:
143 defer func() {140 defer func() {
M pkg/registrypush/push_test.go +6 -3
@@ -219,7 +219,7 @@ func TestPusher_Push(t *testing.T) {
219219 }
220220
221221 func TestPusher_Push_InvokesCleanup(t *testing.T) {
222- var cleanupCalled bool
222+ var cleanupCount int
223223 img, err := random.Image(1024, 1)
224224 require.NoError(t, err)
225225
@@ -232,15 +232,18 @@ func TestPusher_Push_InvokesCleanup(t *testing.T) {
232232 return img, nil
233233 }),
234234 registrypush.WithImageCleanup(func() {
235- cleanupCalled = true
235+ cleanupCount++
236236 }),
237237 )
238238
239239 registryHost := strings.TrimPrefix(registry.URL, "http://")
240240 err = p.Push(context.Background(), registryHost+"/demo/app:v1")
241+ require.NoError(t, err)
242+ assert.Equal(t, 1, cleanupCount, "cleanup callback should have been invoked after first push")
241243
244+ err = p.Push(context.Background(), registryHost+"/demo/app:v1")
242245 require.NoError(t, err)
243- assert.True(t, cleanupCalled, "cleanup callback should have been invoked after push")
246+ assert.Equal(t, 2, cleanupCount, "cleanup callback should have been invoked after second push")
244247 }
245248
246249 func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {
@@ -219,7 +219,7 @@ func TestPusher_Push(t *testing.T) {
219 }219 }
220 220
221 func TestPusher_Push_InvokesCleanup(t *testing.T) {221 func TestPusher_Push_InvokesCleanup(t *testing.T) {
222- var cleanupCalled bool222+ var cleanupCount int
223 img, err := random.Image(1024, 1)223 img, err := random.Image(1024, 1)
224 require.NoError(t, err)224 require.NoError(t, err)
225 225
@@ -232,15 +232,18 @@ func TestPusher_Push_InvokesCleanup(t *testing.T) {
232 return img, nil232 return img, nil
233 }),233 }),
234 registrypush.WithImageCleanup(func() {234 registrypush.WithImageCleanup(func() {
235- cleanupCalled = true235+ cleanupCount++
236 }),236 }),
237 )237 )
238 238
239 registryHost := strings.TrimPrefix(registry.URL, "http://")239 registryHost := strings.TrimPrefix(registry.URL, "http://")
240 err = p.Push(context.Background(), registryHost+"/demo/app:v1")240 err = p.Push(context.Background(), registryHost+"/demo/app:v1")
241+ require.NoError(t, err)
242+ assert.Equal(t, 1, cleanupCount, "cleanup callback should have been invoked after first push")
241 243
244+ err = p.Push(context.Background(), registryHost+"/demo/app:v1")
242 require.NoError(t, err)245 require.NoError(t, err)
243- assert.True(t, cleanupCalled, "cleanup callback should have been invoked after push")246+ assert.Equal(t, 2, cleanupCount, "cleanup callback should have been invoked after second push")
244 }247 }
245 248
246 func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {249 func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close