@@ -23,7 +23,6 @@ import (
2323 "errors"
2424 "fmt"
2525 "io"
26- "os"
2726 "path/filepath"
2827 "regexp"
2928 "strings"
@@ -35,7 +34,7 @@ import (
3534 "github.com/deckhouse/deckhouse/pkg/registry/client"
3635
3736 "github.com/deckhouse/deckhouse-cli/internal"
38- "github.com/deckhouse/deckhouse-cli/internal/mirror/chunked "
37+ "github.com/deckhouse/deckhouse-cli/internal/mirror/pack "
3938 "github.com/deckhouse/deckhouse-cli/internal/mirror/puller"
4039 "github.com/deckhouse/deckhouse-cli/pkg/libmirror/bundle"
4140 "github.com/deckhouse/deckhouse-cli/pkg/libmirror/layouts"
@@ -237,11 +236,27 @@ func (svc *Service) pullModules(ctx context.Context) error {
237236 processName = "Pull Extra Images"
238237 }
239238
239+ // pullCancelled is set when the user interrupts mid-pull. In that case we
240+ // still want the post-processing + packing phase to finalize whatever was
241+ // successfully downloaded so far, rather than throw it all away and leave
242+ // the user with nothing for a multi-hour pull.
243+ pullCancelled := false
240244 err = logger .Process (processName , func () error {
241245 for i , module := range filteredModules {
246+ if err := ctx .Err (); err != nil {
247+ logger .Warnf ("Pull cancelled; %d/%d modules attempted, will pack already-downloaded modules" , i , len (filteredModules ))
248+ pullCancelled = true
249+ return nil
250+ }
251+
242252 logger .Infof ("[%d/%d] Processing module: %s" , i + 1 , len (filteredModules ), module .name )
243253
244254 if err := svc .pullSingleModule (ctx , module ); err != nil {
255+ if isContextErr (err ) {
256+ logger .Warnf ("Pull of module %s cancelled, will pack already-downloaded modules" , module .name )
257+ pullCancelled = true
258+ return nil
259+ }
245260 return fmt .Errorf ("pull module %s: %w" , module .name , err )
246261 }
247262 }
@@ -256,6 +271,16 @@ func (svc *Service) pullModules(ctx context.Context) error {
256271 return nil
257272 }
258273
274+ // Decouple the post-pull phase from the cancellation context so that the
275+ // last bit of packing finalizes for the modules that *did* download.
276+ // Without this, a Ctrl+C during pull would still surface as ctx.Canceled
277+ // inside bundle.PackWithPrefix and we would discard a multi-GB download
278+ // that was already on disk.
279+ postCtx := ctx
280+ if pullCancelled {
281+ postCtx = context .WithoutCancel (ctx )
282+ }
283+
259284 err = logger .Process ("Processing modules image indexes" , func () error {
260285 for _ , l := range svc .layout .AsList () {
261286 err = layouts .SortIndexManifests (l )
@@ -279,13 +304,19 @@ func (svc *Service) pullModules(ctx context.Context) error {
279304 }
280305
281306 // Pack each module into separate tar
282- if err := svc .packModules (filteredModules ); err != nil {
307+ if err := svc .packModules (postCtx , filteredModules ); err != nil {
283308 return err
284309 }
285310
286311 return nil
287312}
288313
314+ // isContextErr reports whether err is one of the context cancellation errors,
315+ // either directly or wrapped.
316+ func isContextErr (err error ) bool {
317+ return errors .Is (err , context .Canceled ) || errors .Is (err , context .DeadlineExceeded )
318+ }
319+
289320func (svc * Service ) pullSingleModule (ctx context.Context , module moduleData ) error {
290321 downloadList := NewImageDownloadList (filepath .Join (svc .rootURL , "modules" , module .name ))
291322 svc .modulesDownloadList .list [module .name ] = downloadList
@@ -879,13 +910,19 @@ func (svc *Service) applyChannelAliases(moduleName string) error {
879910 return nil
880911}
881912
882- func (svc * Service ) packModules (modules []moduleData ) error {
913+ func (svc * Service ) packModules (ctx context. Context , modules []moduleData ) error {
883914 logger := svc .userLogger
884915
885916 bundleDir := svc .options .BundleDir
886917 bundleChunkSize := svc .options .BundleChunkSize
887918
888919 for _ , module := range modules {
920+ // Honor cancellation between modules so a Ctrl+C during the pack
921+ // phase doesn't keep producing more (potentially useless) tars.
922+ if err := ctx .Err (); err != nil {
923+ return err
924+ }
925+
889926 pkgName := "module-" + module .name + ".tar"
890927
891928 if err := logger .Process (fmt .Sprintf ("Pack %s" , pkgName ), func () error {
@@ -899,24 +936,14 @@ func (svc *Service) packModules(modules []moduleData) error {
899936 return nil
900937 }
901938
902- var pkg io.Writer = chunked .NewChunkedFileWriter (bundleChunkSize , bundleDir , pkgName )
903- if bundleChunkSize == 0 {
904- f , err := os .Create (filepath .Join (bundleDir , pkgName ))
905- if err != nil {
906- return fmt .Errorf ("create %s: %w" , pkgName , err )
907- }
908- pkg = f
909- }
910-
911939 // Pack from the module's working directory with prefix to create correct registry structure.
912940 // This ensures the tar contains paths like "modules/<name>/index.json" instead of just "index.json".
913941 moduleDir := filepath .Join (svc .layout .workingDir , module .name )
914942 tarPrefix := filepath .Join ("modules" , module .name )
915- if err := bundle .PackWithPrefix (context .Background (), moduleDir , tarPrefix , pkg ); err != nil {
916- return fmt .Errorf ("pack module %s: %w" , pkgName , err )
917- }
918943
919- return nil
944+ return pack .Bundle (ctx , bundleDir , pkgName , bundleChunkSize , func (w io.Writer ) error {
945+ return bundle .PackWithPrefix (ctx , moduleDir , tarPrefix , w )
946+ })
920947 }); err != nil {
921948 return err
922949 }
0 commit comments