fix(security): clean up temp tarballs after registry push
Add WithImageCleanup option and wire it into Push() via defer. defaultImageSource now produces a cleanup callback that removes the temp tar file. Previously, temp tarballs containing full image contents were left in /tmp indefinitely. The public ImageSource type signature is unchanged for backward compatibility.
- commit
- 658f480e01cfacfb9c7b0ee986a920100a0aabe1
- author
- brice · · 2026-03-14 22:15 UTC
- parent
- 92dab38
- signature
- unsigned
2 files · +65 -16
M pkg/registrypush/push.go +40 -16
| @@ -36,6 +36,7 @@ type Pusher struct { | |||
| 36 | 36 | transport http.RoundTripper | |
| 37 | 37 | insecureTLS bool | |
| 38 | 38 | imageSource ImageSource | |
| 39 | + | cleanupFn func() | |
| 39 | 40 | progress io.Writer | |
| 40 | 41 | } | |
| 41 | 42 | ||
| @@ -63,6 +64,12 @@ func WithImageSource(src ImageSource) Option { | |||
| 63 | 64 | return func(p *Pusher) { p.imageSource = src } | |
| 64 | 65 | } | |
| 65 | 66 | ||
| 67 | + | // WithImageCleanup sets a function called after Push completes to clean up | |
| 68 | + | // resources created by the image source (e.g., temp tar files). | |
| 69 | + | func WithImageCleanup(fn func()) Option { | |
| 70 | + | return func(p *Pusher) { p.cleanupFn = fn } | |
| 71 | + | } | |
| 72 | + | ||
| 66 | 73 | // WithProgress sets a writer for progress messages. | |
| 67 | 74 | func WithProgress(w io.Writer) Option { | |
| 68 | 75 | return func(p *Pusher) { p.progress = w } | |
| @@ -82,7 +89,14 @@ func New(opts ...Option) *Pusher { | |||
| 82 | 89 | } | |
| 83 | 90 | } | |
| 84 | 91 | if p.imageSource == nil { | |
| 85 | - | p.imageSource = defaultImageSource | |
| 92 | + | p.imageSource = func(ctx context.Context, ref string) (v1.Image, error) { | |
| 93 | + | img, cleanup, err := defaultImageSourceWithCleanup(ctx, ref) | |
| 94 | + | if err != nil { | |
| 95 | + | return nil, err | |
| 96 | + | } | |
| 97 | + | p.cleanupFn = cleanup | |
| 98 | + | return img, nil | |
| 99 | + | } | |
| 86 | 100 | } | |
| 87 | 101 | return p | |
| 88 | 102 | } | |
| @@ -102,6 +116,9 @@ func (p *Pusher) Push(ctx context.Context, ref string) error { | |||
| 102 | 116 | return fmt.Errorf("image ref %s must include a tag", ref) | |
| 103 | 117 | } | |
| 104 | 118 | ||
| 119 | + | cleanup := p.cleanupFn | |
| 120 | + | p.cleanupFn = nil | |
| 121 | + | ||
| 105 | 122 | authHeader, err := resolveAuthHeader(parsedRef) | |
| 106 | 123 | if err != nil { | |
| 107 | 124 | return err | |
| @@ -111,6 +128,12 @@ func (p *Pusher) Push(ctx context.Context, ref string) error { | |||
| 111 | 128 | if err != nil { | |
| 112 | 129 | return err | |
| 113 | 130 | } | |
| 131 | + | if p.cleanupFn != nil { | |
| 132 | + | cleanup = p.cleanupFn | |
| 133 | + | } | |
| 134 | + | if cleanup != nil { | |
| 135 | + | defer cleanup() | |
| 136 | + | } | |
| 114 | 137 | ||
| 115 | 138 | baseURL := registryBaseURL(parsedRef.Context().RegistryStr()) | |
| 116 | 139 | repo := parsedRef.Context().RepositoryStr() | |
| @@ -155,30 +178,30 @@ func (p *Pusher) UploadBlob(ctx context.Context, baseURL, repo, digest string, s | |||
| 155 | 178 | return p.finalizeBlobUpload(ctx, baseURL, uploadURL, digest, auth) | |
| 156 | 179 | } | |
| 157 | 180 | ||
| 158 | - | // defaultImageSource reads an image from the local Docker/Podman daemon. | |
| 159 | - | func defaultImageSource(ctx context.Context, ref string) (v1.Image, error) { | |
| 181 | + | // defaultImageSourceWithCleanup reads an image from the local Docker/Podman daemon. | |
| 182 | + | func defaultImageSourceWithCleanup(ctx context.Context, ref string) (v1.Image, func(), error) { | |
| 160 | 183 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| 161 | 184 | if err != nil { | |
| 162 | - | return nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 185 | + | return nil, nil, fmt.Errorf("failed to create Docker client: %w", err) | |
| 163 | 186 | } | |
| 164 | 187 | defer cli.Close() | |
| 165 | 188 | ||
| 166 | 189 | tag, err := name.NewTag(ref) | |
| 167 | 190 | if err != nil { | |
| 168 | - | return nil, fmt.Errorf("failed to parse image ref %s: %w", ref, err) | |
| 191 | + | return nil, nil, fmt.Errorf("failed to parse image ref %s: %w", ref, err) | |
| 169 | 192 | } | |
| 170 | 193 | ||
| 171 | 194 | rc, err := cli.ImageSave(ctx, []string{ref}) | |
| 172 | 195 | if err != nil { | |
| 173 | - | return nil, fmt.Errorf("failed to export image %s: %w", ref, err) | |
| 196 | + | return nil, nil, fmt.Errorf("failed to export image %s: %w", ref, err) | |
| 174 | 197 | } | |
| 175 | 198 | ||
| 176 | 199 | tmpFile, err := os.CreateTemp("", "gordon-push-*.tar") | |
| 177 | 200 | if err != nil { | |
| 178 | 201 | if closeErr := rc.Close(); closeErr != nil { | |
| 179 | - | return nil, fmt.Errorf("failed to create temp file for image export: %w (also failed to close image export stream: %v)", err, closeErr) | |
| 202 | + | return nil, nil, fmt.Errorf("failed to create temp file for image export: %w (also failed to close image export stream: %v)", err, closeErr) | |
| 180 | 203 | } | |
| 181 | - | return nil, fmt.Errorf("failed to create temp file for image export: %w", err) | |
| 204 | + | return nil, nil, fmt.Errorf("failed to create temp file for image export: %w", err) | |
| 182 | 205 | } | |
| 183 | 206 | tmpPath := tmpFile.Name() | |
| 184 | 207 | ||
| @@ -186,33 +209,34 @@ func defaultImageSource(ctx context.Context, ref string) (v1.Image, error) { | |||
| 186 | 209 | if closeErr := rc.Close(); closeErr != nil { | |
| 187 | 210 | _ = tmpFile.Close() | |
| 188 | 211 | _ = os.Remove(tmpPath) | |
| 189 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close image export stream: %v)", err, closeErr) | |
| 212 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close image export stream: %v)", err, closeErr) | |
| 190 | 213 | } | |
| 191 | 214 | if closeErr := tmpFile.Close(); closeErr != nil { | |
| 192 | 215 | _ = os.Remove(tmpPath) | |
| 193 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close temp file: %v)", err, closeErr) | |
| 216 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close temp file: %v)", err, closeErr) | |
| 194 | 217 | } | |
| 195 | 218 | if removeErr := os.Remove(tmpPath); removeErr != nil { | |
| 196 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to remove temp file %s: %v)", err, tmpPath, removeErr) | |
| 219 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to remove temp file %s: %v)", err, tmpPath, removeErr) | |
| 197 | 220 | } | |
| 198 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w", err) | |
| 221 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w", err) | |
| 199 | 222 | } | |
| 200 | 223 | if err := rc.Close(); err != nil { | |
| 201 | 224 | _ = tmpFile.Close() | |
| 202 | 225 | _ = os.Remove(tmpPath) | |
| 203 | - | return nil, fmt.Errorf("failed to close image export stream: %w", err) | |
| 226 | + | return nil, nil, fmt.Errorf("failed to close image export stream: %w", err) | |
| 204 | 227 | } | |
| 205 | 228 | if err := tmpFile.Close(); err != nil { | |
| 206 | 229 | _ = os.Remove(tmpPath) | |
| 207 | - | return nil, fmt.Errorf("failed to close temp file %s: %w", tmpPath, err) | |
| 230 | + | return nil, nil, fmt.Errorf("failed to close temp file %s: %w", tmpPath, err) | |
| 208 | 231 | } | |
| 209 | 232 | ||
| 210 | 233 | img, err := tarball.ImageFromPath(tmpPath, &tag) | |
| 211 | 234 | if err != nil { | |
| 212 | 235 | _ = os.Remove(tmpPath) | |
| 213 | - | return nil, fmt.Errorf("failed to parse image tar for %s: %w", ref, err) | |
| 236 | + | return nil, nil, fmt.Errorf("failed to parse image tar for %s: %w", ref, err) | |
| 214 | 237 | } | |
| 215 | - | return img, nil | |
| 238 | + | cleanup := func() { _ = os.Remove(tmpPath) } | |
| 239 | + | return img, cleanup, nil | |
| 216 | 240 | } | |
| 217 | 241 | ||
| 218 | 242 | func (p *Pusher) uploadImageLayers(ctx context.Context, img v1.Image, baseURL, repo, auth string) error { | |
| @@ -36,6 +36,7 @@ type Pusher struct { | |||||
| 36 | transport http.RoundTripper | 36 | transport http.RoundTripper | ||
| 37 | insecureTLS bool | 37 | insecureTLS bool | ||
| 38 | imageSource ImageSource | 38 | imageSource ImageSource | ||
| 39 | + | cleanupFn func() | |||
| 39 | progress io.Writer | 40 | progress io.Writer | ||
| 40 | } | 41 | } | ||
| 41 | 42 | ||||
| @@ -63,6 +64,12 @@ func WithImageSource(src ImageSource) Option { | |||||
| 63 | return func(p *Pusher) { p.imageSource = src } | 64 | return func(p *Pusher) { p.imageSource = src } | ||
| 64 | } | 65 | } | ||
| 65 | 66 | ||||
| 67 | + | // WithImageCleanup sets a function called after Push completes to clean up | |||
| 68 | + | // resources created by the image source (e.g., temp tar files). | |||
| 69 | + | func WithImageCleanup(fn func()) Option { | |||
| 70 | + | return func(p *Pusher) { p.cleanupFn = fn } | |||
| 71 | + | } | |||
| 72 | + | ||||
| 66 | // WithProgress sets a writer for progress messages. | 73 | // WithProgress sets a writer for progress messages. | ||
| 67 | func WithProgress(w io.Writer) Option { | 74 | func WithProgress(w io.Writer) Option { | ||
| 68 | return func(p *Pusher) { p.progress = w } | 75 | return func(p *Pusher) { p.progress = w } | ||
| @@ -82,7 +89,14 @@ func New(opts ...Option) *Pusher { | |||||
| 82 | } | 89 | } | ||
| 83 | } | 90 | } | ||
| 84 | if p.imageSource == nil { | 91 | if p.imageSource == nil { | ||
| 85 | - | p.imageSource = defaultImageSource | 92 | + | p.imageSource = func(ctx context.Context, ref string) (v1.Image, error) { |
| 93 | + | img, cleanup, err := defaultImageSourceWithCleanup(ctx, ref) | |||
| 94 | + | if err != nil { | |||
| 95 | + | return nil, err | |||
| 96 | + | } | |||
| 97 | + | p.cleanupFn = cleanup | |||
| 98 | + | return img, nil | |||
| 99 | + | } | |||
| 86 | } | 100 | } | ||
| 87 | return p | 101 | return p | ||
| 88 | } | 102 | } | ||
| @@ -102,6 +116,9 @@ func (p *Pusher) Push(ctx context.Context, ref string) error { | |||||
| 102 | return fmt.Errorf("image ref %s must include a tag", ref) | 116 | return fmt.Errorf("image ref %s must include a tag", ref) | ||
| 103 | } | 117 | } | ||
| 104 | 118 | ||||
| 119 | + | cleanup := p.cleanupFn | |||
| 120 | + | p.cleanupFn = nil | |||
| 121 | + | ||||
| 105 | authHeader, err := resolveAuthHeader(parsedRef) | 122 | authHeader, err := resolveAuthHeader(parsedRef) | ||
| 106 | if err != nil { | 123 | if err != nil { | ||
| 107 | return err | 124 | return err | ||
| @@ -111,6 +128,12 @@ func (p *Pusher) Push(ctx context.Context, ref string) error { | |||||
| 111 | if err != nil { | 128 | if err != nil { | ||
| 112 | return err | 129 | return err | ||
| 113 | } | 130 | } | ||
| 131 | + | if p.cleanupFn != nil { | |||
| 132 | + | cleanup = p.cleanupFn | |||
| 133 | + | } | |||
| 134 | + | if cleanup != nil { | |||
| 135 | + | defer cleanup() | |||
| 136 | + | } | |||
| 114 | 137 | ||||
| 115 | baseURL := registryBaseURL(parsedRef.Context().RegistryStr()) | 138 | baseURL := registryBaseURL(parsedRef.Context().RegistryStr()) | ||
| 116 | repo := parsedRef.Context().RepositoryStr() | 139 | repo := parsedRef.Context().RepositoryStr() | ||
| @@ -155,30 +178,30 @@ func (p *Pusher) UploadBlob(ctx context.Context, baseURL, repo, digest string, s | |||||
| 155 | return p.finalizeBlobUpload(ctx, baseURL, uploadURL, digest, auth) | 178 | return p.finalizeBlobUpload(ctx, baseURL, uploadURL, digest, auth) | ||
| 156 | } | 179 | } | ||
| 157 | 180 | ||||
| 158 | - | // defaultImageSource reads an image from the local Docker/Podman daemon. | 181 | + | // defaultImageSourceWithCleanup reads an image from the local Docker/Podman daemon. |
| 159 | - | func defaultImageSource(ctx context.Context, ref string) (v1.Image, error) { | 182 | + | func defaultImageSourceWithCleanup(ctx context.Context, ref string) (v1.Image, func(), error) { |
| 160 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | 183 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | ||
| 161 | if err != nil { | 184 | if err != nil { | ||
| 162 | - | return nil, fmt.Errorf("failed to create Docker client: %w", err) | 185 | + | return nil, nil, fmt.Errorf("failed to create Docker client: %w", err) |
| 163 | } | 186 | } | ||
| 164 | defer cli.Close() | 187 | defer cli.Close() | ||
| 165 | 188 | ||||
| 166 | tag, err := name.NewTag(ref) | 189 | tag, err := name.NewTag(ref) | ||
| 167 | if err != nil { | 190 | if err != nil { | ||
| 168 | - | return nil, fmt.Errorf("failed to parse image ref %s: %w", ref, err) | 191 | + | return nil, nil, fmt.Errorf("failed to parse image ref %s: %w", ref, err) |
| 169 | } | 192 | } | ||
| 170 | 193 | ||||
| 171 | rc, err := cli.ImageSave(ctx, []string{ref}) | 194 | rc, err := cli.ImageSave(ctx, []string{ref}) | ||
| 172 | if err != nil { | 195 | if err != nil { | ||
| 173 | - | return nil, fmt.Errorf("failed to export image %s: %w", ref, err) | 196 | + | return nil, nil, fmt.Errorf("failed to export image %s: %w", ref, err) |
| 174 | } | 197 | } | ||
| 175 | 198 | ||||
| 176 | tmpFile, err := os.CreateTemp("", "gordon-push-*.tar") | 199 | tmpFile, err := os.CreateTemp("", "gordon-push-*.tar") | ||
| 177 | if err != nil { | 200 | if err != nil { | ||
| 178 | if closeErr := rc.Close(); closeErr != nil { | 201 | if closeErr := rc.Close(); closeErr != nil { | ||
| 179 | - | return nil, fmt.Errorf("failed to create temp file for image export: %w (also failed to close image export stream: %v)", err, closeErr) | 202 | + | return nil, nil, fmt.Errorf("failed to create temp file for image export: %w (also failed to close image export stream: %v)", err, closeErr) |
| 180 | } | 203 | } | ||
| 181 | - | return nil, fmt.Errorf("failed to create temp file for image export: %w", err) | 204 | + | return nil, nil, fmt.Errorf("failed to create temp file for image export: %w", err) |
| 182 | } | 205 | } | ||
| 183 | tmpPath := tmpFile.Name() | 206 | tmpPath := tmpFile.Name() | ||
| 184 | 207 | ||||
| @@ -186,33 +209,34 @@ func defaultImageSource(ctx context.Context, ref string) (v1.Image, error) { | |||||
| 186 | if closeErr := rc.Close(); closeErr != nil { | 209 | if closeErr := rc.Close(); closeErr != nil { | ||
| 187 | _ = tmpFile.Close() | 210 | _ = tmpFile.Close() | ||
| 188 | _ = os.Remove(tmpPath) | 211 | _ = os.Remove(tmpPath) | ||
| 189 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close image export stream: %v)", err, closeErr) | 212 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close image export stream: %v)", err, closeErr) |
| 190 | } | 213 | } | ||
| 191 | if closeErr := tmpFile.Close(); closeErr != nil { | 214 | if closeErr := tmpFile.Close(); closeErr != nil { | ||
| 192 | _ = os.Remove(tmpPath) | 215 | _ = os.Remove(tmpPath) | ||
| 193 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close temp file: %v)", err, closeErr) | 216 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to close temp file: %v)", err, closeErr) |
| 194 | } | 217 | } | ||
| 195 | if removeErr := os.Remove(tmpPath); removeErr != nil { | 218 | if removeErr := os.Remove(tmpPath); removeErr != nil { | ||
| 196 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to remove temp file %s: %v)", err, tmpPath, removeErr) | 219 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w (also failed to remove temp file %s: %v)", err, tmpPath, removeErr) |
| 197 | } | 220 | } | ||
| 198 | - | return nil, fmt.Errorf("failed to write image tar to temp file: %w", err) | 221 | + | return nil, nil, fmt.Errorf("failed to write image tar to temp file: %w", err) |
| 199 | } | 222 | } | ||
| 200 | if err := rc.Close(); err != nil { | 223 | if err := rc.Close(); err != nil { | ||
| 201 | _ = tmpFile.Close() | 224 | _ = tmpFile.Close() | ||
| 202 | _ = os.Remove(tmpPath) | 225 | _ = os.Remove(tmpPath) | ||
| 203 | - | return nil, fmt.Errorf("failed to close image export stream: %w", err) | 226 | + | return nil, nil, fmt.Errorf("failed to close image export stream: %w", err) |
| 204 | } | 227 | } | ||
| 205 | if err := tmpFile.Close(); err != nil { | 228 | if err := tmpFile.Close(); err != nil { | ||
| 206 | _ = os.Remove(tmpPath) | 229 | _ = os.Remove(tmpPath) | ||
| 207 | - | return nil, fmt.Errorf("failed to close temp file %s: %w", tmpPath, err) | 230 | + | return nil, nil, fmt.Errorf("failed to close temp file %s: %w", tmpPath, err) |
| 208 | } | 231 | } | ||
| 209 | 232 | ||||
| 210 | img, err := tarball.ImageFromPath(tmpPath, &tag) | 233 | img, err := tarball.ImageFromPath(tmpPath, &tag) | ||
| 211 | if err != nil { | 234 | if err != nil { | ||
| 212 | _ = os.Remove(tmpPath) | 235 | _ = os.Remove(tmpPath) | ||
| 213 | - | return nil, fmt.Errorf("failed to parse image tar for %s: %w", ref, err) | 236 | + | return nil, nil, fmt.Errorf("failed to parse image tar for %s: %w", ref, err) |
| 214 | } | 237 | } | ||
| 215 | - | return img, nil | 238 | + | cleanup := func() { _ = os.Remove(tmpPath) } |
| 239 | + | return img, cleanup, nil | |||
| 216 | } | 240 | } | ||
| 217 | 241 | ||||
| 218 | func (p *Pusher) uploadImageLayers(ctx context.Context, img v1.Image, baseURL, repo, auth string) error { | 242 | func (p *Pusher) uploadImageLayers(ctx context.Context, img v1.Image, baseURL, repo, auth string) error { | ||
M pkg/registrypush/push_test.go +25 -0
| @@ -217,6 +217,31 @@ func TestPusher_Push(t *testing.T) { | |||
| 217 | 217 | } | |
| 218 | 218 | } | |
| 219 | 219 | ||
| 220 | + | func TestPusher_Push_InvokesCleanup(t *testing.T) { | |
| 221 | + | var cleanupCalled bool | |
| 222 | + | img, err := random.Image(1024, 1) | |
| 223 | + | require.NoError(t, err) | |
| 224 | + | ||
| 225 | + | registry := httptest.NewServer(newFakePushRegistry(t, img, map[string]bool{}).handler()) | |
| 226 | + | defer registry.Close() | |
| 227 | + | ||
| 228 | + | p := registrypush.New( | |
| 229 | + | registrypush.WithChunkSize(1024), | |
| 230 | + | registrypush.WithImageSource(func(ctx context.Context, ref string) (v1.Image, error) { | |
| 231 | + | return img, nil | |
| 232 | + | }), | |
| 233 | + | registrypush.WithImageCleanup(func() { | |
| 234 | + | cleanupCalled = true | |
| 235 | + | }), | |
| 236 | + | ) | |
| 237 | + | ||
| 238 | + | registryHost := strings.TrimPrefix(registry.URL, "http://") | |
| 239 | + | err = p.Push(context.Background(), registryHost+"/demo/app:v1") | |
| 240 | + | ||
| 241 | + | require.NoError(t, err) | |
| 242 | + | assert.True(t, cleanupCalled, "cleanup callback should have been invoked after push") | |
| 243 | + | } | |
| 244 | + | ||
| 220 | 245 | func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) { | |
| 221 | 246 | t.Parallel() | |
| 222 | 247 | ||
| @@ -217,6 +217,31 @@ func TestPusher_Push(t *testing.T) { | |||||
| 217 | } | 217 | } | ||
| 218 | } | 218 | } | ||
| 219 | 219 | ||||
| 220 | + | func TestPusher_Push_InvokesCleanup(t *testing.T) { | |||
| 221 | + | var cleanupCalled bool | |||
| 222 | + | img, err := random.Image(1024, 1) | |||
| 223 | + | require.NoError(t, err) | |||
| 224 | + | ||||
| 225 | + | registry := httptest.NewServer(newFakePushRegistry(t, img, map[string]bool{}).handler()) | |||
| 226 | + | defer registry.Close() | |||
| 227 | + | ||||
| 228 | + | p := registrypush.New( | |||
| 229 | + | registrypush.WithChunkSize(1024), | |||
| 230 | + | registrypush.WithImageSource(func(ctx context.Context, ref string) (v1.Image, error) { | |||
| 231 | + | return img, nil | |||
| 232 | + | }), | |||
| 233 | + | registrypush.WithImageCleanup(func() { | |||
| 234 | + | cleanupCalled = true | |||
| 235 | + | }), | |||
| 236 | + | ) | |||
| 237 | + | ||||
| 238 | + | registryHost := strings.TrimPrefix(registry.URL, "http://") | |||
| 239 | + | err = p.Push(context.Background(), registryHost+"/demo/app:v1") | |||
| 240 | + | ||||
| 241 | + | require.NoError(t, err) | |||
| 242 | + | assert.True(t, cleanupCalled, "cleanup callback should have been invoked after push") | |||
| 243 | + | } | |||
| 244 | + | ||||
| 220 | func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) { | 245 | func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) { | ||
| 221 | t.Parallel() | 246 | t.Parallel() | ||
| 222 | 247 | ||||