Skip to content

Commit 1b10ed9

Browse files
[8.19] (backport #9322) Enhancement/5235 handle insufficient disk space errors in artifact unpack (#9819)
* Enhancement/5235 handle insufficient disk space errors in artifact unpack (#9322) * enhancement(5235): added stdlib wrappers in step_unpack * enhancement(5235): wrap errors in step_unpack * enhancement(5235): removed nolint:gosec comment from step_unpack * enhancement(5235): refactored unpack step to abstract the unpack function from upgrader. abstracted unzip and untar to be able to test unpack function. updated unpack step tests to test for stdlib failures in unzip and untar and added tests for unpack * enhancement(5235}: added unpackHandler interface to abstract the unpack function from upgrader * enhancement(5235): added getPackageMetadata into unpackHandler interface for testability * enhancement(5235): using unpacker.getPackageMetadata * enhancement(5235): added abstractions in upgrader for testability * enhancement(5235): removed unpack specific test from upgrade tests, added test cases in the upgrade error handling test * enhancement(5235): using formatted test assertion api (cherry picked from commit f70ff02) # Conflicts: # internal/pkg/agent/application/upgrade/step_unpack.go # internal/pkg/agent/application/upgrade/step_unpack_test.go # internal/pkg/agent/application/upgrade/upgrade.go * resolve conflicts --------- Co-authored-by: Kaan Yalti <[email protected]>
1 parent c8d311c commit 1b10ed9

File tree

4 files changed

+333
-55
lines changed

4 files changed

+333
-55
lines changed

internal/pkg/agent/application/upgrade/step_unpack.go

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,43 @@ type UnpackResult struct {
3333
VersionedHome string `json:"versioned-home" yaml:"versioned-home"`
3434
}
3535

36+
type copyFunc func(dst io.Writer, src io.Reader) (written int64, err error)
37+
type mkdirAllFunc func(name string, perm fs.FileMode) error
38+
type openFileFunc func(name string, flag int, perm fs.FileMode) (*os.File, error)
39+
type unarchiveFunc func(log *logger.Logger, archivePath, dataDir string, copy copyFunc, mkdirAll mkdirAllFunc, openFile openFileFunc) (UnpackResult, error)
40+
41+
type unpacker struct {
42+
log *logger.Logger
43+
// Abstractsions for testability
44+
unzip unarchiveFunc
45+
untar unarchiveFunc
46+
// stdlib abstractions for testability
47+
copy copyFunc
48+
mkdirAll mkdirAllFunc
49+
openFile openFileFunc
50+
}
51+
52+
func newUnpacker(log *logger.Logger) *unpacker {
53+
return &unpacker{
54+
log: log,
55+
unzip: unzip,
56+
untar: untar,
57+
copy: io.Copy,
58+
mkdirAll: os.MkdirAll,
59+
openFile: os.OpenFile,
60+
}
61+
}
62+
3663
// unpack unpacks archive correctly, skips root (symlink, config...) unpacks data/*
37-
func (u *Upgrader) unpack(version, archivePath, dataDir string) (UnpackResult, error) {
64+
func (u *unpacker) unpack(version, archivePath, dataDir string) (UnpackResult, error) {
3865
// unpack must occur in directory that holds the installation directory
3966
// or the extraction will be double nested
4067
var unpackRes UnpackResult
4168
var err error
4269
if runtime.GOOS == windows {
43-
unpackRes, err = unzip(u.log, archivePath, dataDir)
70+
unpackRes, err = u.unzip(u.log, archivePath, dataDir, u.copy, u.mkdirAll, u.openFile)
4471
} else {
45-
unpackRes, err = untar(u.log, archivePath, dataDir)
72+
unpackRes, err = u.untar(u.log, archivePath, dataDir, u.copy, u.mkdirAll, u.openFile)
4673
}
4774

4875
if err != nil {
@@ -59,7 +86,7 @@ type packageMetadata struct {
5986
hash string
6087
}
6188

62-
func (u *Upgrader) getPackageMetadata(archivePath string) (packageMetadata, error) {
89+
func (u *unpacker) getPackageMetadata(archivePath string) (packageMetadata, error) {
6390
ext := filepath.Ext(archivePath)
6491
if ext == ".gz" {
6592
// if we got gzip extension we need another extension before last
@@ -76,7 +103,8 @@ func (u *Upgrader) getPackageMetadata(archivePath string) (packageMetadata, erro
76103
}
77104
}
78105

79-
func unzip(log *logger.Logger, archivePath, dataDir string) (UnpackResult, error) {
106+
// injecting copy, mkdirAll and openFile for testability
107+
func unzip(log *logger.Logger, archivePath, dataDir string, copy copyFunc, mkdirAll mkdirAllFunc, openFile openFileFunc) (UnpackResult, error) {
80108
var hash, rootDir string
81109
r, err := zip.OpenReader(archivePath)
82110
if err != nil {
@@ -136,8 +164,10 @@ func unzip(log *logger.Logger, archivePath, dataDir string) (UnpackResult, error
136164
// check if the directory already exists
137165
_, err = os.Stat(dstPath)
138166
if errors.Is(err, fs.ErrNotExist) {
139-
// the directory does not exist, create it and any non-existing parent directory with the same permissions
140-
if err := os.MkdirAll(dstPath, f.Mode().Perm()&0770); err != nil {
167+
// the directory does not exist, create it and any non-existing
168+
// parent directory with the same permissions.
169+
// Using mkdirAll instead of os.MkdirAll so that we can mock it in tests.
170+
if err := mkdirAll(dstPath, f.Mode().Perm()&0770); err != nil {
141171
return fmt.Errorf("creating directory %q: %w", dstPath, err)
142172
}
143173
} else if err != nil {
@@ -150,13 +180,23 @@ func unzip(log *logger.Logger, archivePath, dataDir string) (UnpackResult, error
150180
}
151181
}
152182

153-
_ = os.MkdirAll(dstPath, f.Mode()&0770)
183+
// Using mkdirAll instead of os.MkdirAll so that we can mock it in tests.
184+
err = mkdirAll(dstPath, f.Mode()&0770)
185+
if err != nil {
186+
return fmt.Errorf("creating directory %q: %w", dstPath, err)
187+
}
154188
} else {
155189
log.Debugw("Unpacking file", "archive", "zip", "file.path", dstPath)
156190
// create non-existing containing folders with 0770 permissions right now, we'll fix the permission of each
157-
// directory as we come across them while processing the other package entries
158-
_ = os.MkdirAll(filepath.Dir(dstPath), 0770)
159-
f, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()&0770)
191+
// directory as we come across them while processing the other
192+
// package entries
193+
// Using mkdirAll instead of os.MkdirAll so that we can mock it in tests.
194+
err = mkdirAll(filepath.Dir(dstPath), 0770)
195+
if err != nil {
196+
return fmt.Errorf("creating directory %q: %w", dstPath, err)
197+
}
198+
// Using openFile instead of os.OpenFile so that we can mock it in tests.
199+
f, err := openFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()&0770)
160200
if err != nil {
161201
return err
162202
}
@@ -166,7 +206,9 @@ func unzip(log *logger.Logger, archivePath, dataDir string) (UnpackResult, error
166206
}
167207
}()
168208

169-
if _, err = io.Copy(f, rc); err != nil { //nolint:gosec // legacy
209+
// Using copy instead of io.Copy so that we can
210+
// mock it in tests.
211+
if _, err = copy(f, rc); err != nil {
170212
return err
171213
}
172214
}
@@ -240,8 +282,8 @@ func getPackageMetadataFromZipReader(r *zip.ReadCloser, fileNamePrefix string) (
240282
return ret, nil
241283
}
242284

243-
func untar(log *logger.Logger, archivePath, dataDir string) (UnpackResult, error) {
244-
285+
// injecting copy, mkdirAll and openFile for testability
286+
func untar(log *logger.Logger, archivePath, dataDir string, copy copyFunc, mkdirAll mkdirAllFunc, openFile openFileFunc) (UnpackResult, error) {
245287
var versionedHome string
246288
var rootDir string
247289
var hash string
@@ -330,17 +372,23 @@ func untar(log *logger.Logger, archivePath, dataDir string) (UnpackResult, error
330372
log.Debugw("Unpacking file", "archive", "tar", "file.path", abs)
331373
// create non-existing containing folders with 0750 permissions right now, we'll fix the permission of each
332374
// directory as we come across them while processing the other package entries
333-
if err = os.MkdirAll(filepath.Dir(abs), 0750); err != nil {
334-
return UnpackResult{}, errors.New(err, "TarInstaller: creating directory for file "+abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs))
375+
// Using mkdirAll instead of os.MkdirAll so that we can
376+
// mock it in tests.
377+
if err = mkdirAll(filepath.Dir(abs), 0750); err != nil {
378+
return UnpackResult{}, goerrors.Join(err, errors.New("TarInstaller: creating directory for file "+abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs)))
335379
}
336380

337381
// remove any world permissions from the file
338-
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()&0770)
382+
// Using openFile instead of os.OpenFile so that we can
383+
// mock it in tests.
384+
wf, err := openFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()&0770)
339385
if err != nil {
340-
return UnpackResult{}, errors.New(err, "TarInstaller: creating file "+abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs))
386+
return UnpackResult{}, goerrors.Join(err, errors.New("TarInstaller: creating file "+abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs)))
341387
}
342388

343-
_, err = io.Copy(wf, tr) //nolint:gosec // legacy
389+
// Using copy instead of io.Copy so that we can
390+
// mock it in tests.
391+
_, err = copy(wf, tr)
344392
if closeErr := wf.Close(); closeErr != nil && err == nil {
345393
err = closeErr
346394
}
@@ -352,17 +400,20 @@ func untar(log *logger.Logger, archivePath, dataDir string) (UnpackResult, error
352400
// check if the directory already exists
353401
_, err = os.Stat(abs)
354402
if errors.Is(err, fs.ErrNotExist) {
355-
// the directory does not exist, create it and any non-existing parent directory with the same permissions
356-
if err := os.MkdirAll(abs, mode.Perm()&0770); err != nil {
357-
return UnpackResult{}, errors.New(err, "TarInstaller: creating directory for file "+abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs))
403+
// the directory does not exist, create it and any non-existing
404+
// parent directory with the same permissions.
405+
// Using mkdirAll instead of os.MkdirAll so that we can
406+
// mock it in tests.
407+
if err := mkdirAll(abs, mode.Perm()&0770); err != nil {
408+
return UnpackResult{}, goerrors.Join(err, errors.New("TarInstaller: creating directory for file "+abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs)))
358409
}
359410
} else if err != nil {
360411
return UnpackResult{}, errors.New(err, "TarInstaller: stat() directory for file "+abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs))
361412
} else {
362413
// directory already exists, set the appropriate permissions
363414
err = os.Chmod(abs, mode.Perm()&0770)
364415
if err != nil {
365-
return UnpackResult{}, errors.New(err, fmt.Sprintf("TarInstaller: setting permissions %O for directory %q", mode.Perm()&0770, abs), errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs))
416+
return UnpackResult{}, goerrors.Join(err, errors.New("TarInstaller: setting permissions %O for directory %q", mode.Perm()&0770, abs, errors.TypeFilesystem, errors.M(errors.MetaKeyPath, abs)))
366417
}
367418
}
368419
default:

0 commit comments

Comments
 (0)