Skip to content

Commit 90decac

Browse files
author
Alexandr Stefurishin
committed
replace tar (cli) with code-only packing
Signed-off-by: Alexandr Stefurishin <[email protected]>
1 parent 49ec91f commit 90decac

File tree

9 files changed

+705
-4
lines changed

9 files changed

+705
-4
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ require (
9696
go.uber.org/zap v1.19.0 // indirect
9797
golang.org/x/crypto v0.30.0 // indirect
9898
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
99+
golang.org/x/mod v0.22.0
99100
golang.org/x/oauth2 v0.23.0 // indirect
100101
golang.org/x/sync v0.10.0 // indirect
101102
golang.org/x/sys v0.28.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,8 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
451451
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
452452
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
453453
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
454+
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
455+
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
454456
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
455457
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
456458
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

pkg/nfs/controllerserver.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,10 +404,11 @@ func (cs *ControllerServer) CreateSnapshot(ctx context.Context, req *csi.CreateS
404404

405405
srcPath := getInternalVolumePath(cs.Driver.workingMountDir, srcVol)
406406
dstPath := filepath.Join(snapInternalVolPath, snapshot.archiveName())
407+
407408
klog.V(2).Infof("tar %v -> %v", srcPath, dstPath)
408-
out, err := exec.Command("tar", "-C", srcPath, "-czvf", dstPath, ".").CombinedOutput()
409+
err = TarPack(srcPath, dstPath, true)
409410
if err != nil {
410-
return nil, status.Errorf(codes.Internal, "failed to create archive for snapshot: %v: %v", err, string(out))
411+
return nil, status.Errorf(codes.Internal, "failed to create archive for snapshot: %v", err)
411412
}
412413
klog.V(2).Infof("tar %s -> %s complete", srcPath, dstPath)
413414

@@ -571,9 +572,10 @@ func (cs *ControllerServer) copyFromSnapshot(ctx context.Context, req *csi.Creat
571572
snapPath := filepath.Join(getInternalVolumePath(cs.Driver.workingMountDir, snapVol), snap.archiveName())
572573
dstPath := getInternalVolumePath(cs.Driver.workingMountDir, dstVol)
573574
klog.V(2).Infof("copy volume from snapshot %v -> %v", snapPath, dstPath)
574-
out, err := exec.Command("tar", "-xzvf", snapPath, "-C", dstPath).CombinedOutput()
575+
576+
err = TarUnpack(snapPath, dstPath, true)
575577
if err != nil {
576-
return status.Errorf(codes.Internal, "failed to copy volume for snapshot: %v: %v", err, string(out))
578+
return status.Errorf(codes.Internal, "failed to copy volume for snapshot: %v", err)
577579
}
578580
klog.V(2).Infof("volume copied from snapshot %v -> %v", snapPath, dstPath)
579581
return nil

pkg/nfs/tar.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package nfs
18+
19+
import (
20+
"archive/tar"
21+
"compress/gzip"
22+
"errors"
23+
"fmt"
24+
"io"
25+
"io/fs"
26+
"os"
27+
"path"
28+
"path/filepath"
29+
"strings"
30+
)
31+
32+
func TarPack(srcDirPath string, dstPath string, enableCompression bool) error {
33+
// normalize all paths to be absolute and clean
34+
dstPath, err := filepath.Abs(dstPath)
35+
if err != nil {
36+
return fmt.Errorf("normalizing destination path: %w", err)
37+
}
38+
39+
srcDirPath, err = filepath.Abs(srcDirPath)
40+
if err != nil {
41+
return fmt.Errorf("normalizing source path: %w", err)
42+
}
43+
44+
if strings.Index(path.Dir(dstPath), srcDirPath) == 0 {
45+
return fmt.Errorf("destination file %s cannot be under source directory %s", dstPath, srcDirPath)
46+
}
47+
48+
tarFile, err := os.Create(dstPath)
49+
if err != nil {
50+
return fmt.Errorf("creating destination file: %w", err)
51+
}
52+
defer func() {
53+
err = errors.Join(err, closeAndWrapErr(tarFile, "closing destination file %s: %w", dstPath))
54+
}()
55+
56+
var tarDst io.Writer = tarFile
57+
if enableCompression {
58+
gzipWriter := gzip.NewWriter(tarFile)
59+
defer func() {
60+
err = errors.Join(err, closeAndWrapErr(gzipWriter, "closing gzip writer"))
61+
}()
62+
tarDst = gzipWriter
63+
}
64+
65+
tarWriter := tar.NewWriter(tarDst)
66+
defer func() {
67+
err = errors.Join(err, closeAndWrapErr(tarWriter, "closing tar writer"))
68+
}()
69+
70+
// recursively visit every file and write it
71+
if err = filepath.Walk(
72+
srcDirPath,
73+
func(srcSubPath string, fileInfo fs.FileInfo, walkErr error) error {
74+
return tarVisitFileToPack(tarWriter, srcDirPath, srcSubPath, fileInfo, walkErr)
75+
},
76+
); err != nil {
77+
return fmt.Errorf("walking source directory: %w", err)
78+
}
79+
80+
return nil
81+
}
82+
83+
func tarVisitFileToPack(
84+
tarWriter *tar.Writer,
85+
srcPath string,
86+
srcSubPath string,
87+
fileInfo os.FileInfo,
88+
walkErr error,
89+
) (err error) {
90+
if walkErr != nil {
91+
return walkErr
92+
}
93+
94+
linkTarget := ""
95+
if fileInfo.Mode()&fs.ModeSymlink != 0 {
96+
linkTarget, err = os.Readlink(srcSubPath)
97+
if err != nil {
98+
return fmt.Errorf("reading link %s: %w", srcSubPath, err)
99+
}
100+
}
101+
102+
tarHeader, err := tar.FileInfoHeader(fileInfo, linkTarget)
103+
if err != nil {
104+
return fmt.Errorf("creating tar header for %s: %w", srcSubPath, err)
105+
}
106+
107+
// srcSubPath always starts with srcPath and both are absolute
108+
tarHeader.Name, err = filepath.Rel(srcPath, srcSubPath)
109+
if err != nil {
110+
return fmt.Errorf("making tar header name for file %s: %w", srcSubPath, err)
111+
}
112+
113+
if err = tarWriter.WriteHeader(tarHeader); err != nil {
114+
return fmt.Errorf("writing tar header for file %s: %w", srcSubPath, err)
115+
}
116+
117+
if !fileInfo.Mode().IsRegular() {
118+
return nil
119+
}
120+
121+
srcFile, err := os.Open(srcSubPath)
122+
if err != nil {
123+
return fmt.Errorf("opening file being packed %s: %w", srcSubPath, err)
124+
}
125+
defer func() {
126+
err = errors.Join(err, closeAndWrapErr(srcFile, "closing file being packed %s: %w", srcSubPath))
127+
}()
128+
_, err = io.Copy(tarWriter, srcFile)
129+
if err != nil {
130+
return fmt.Errorf("packing file %s: %w", srcSubPath, err)
131+
}
132+
return nil
133+
}
134+
135+
func TarUnpack(srcPath, dstDirPath string, enableCompression bool) (err error) {
136+
// normalize all paths to be absolute and clean
137+
srcPath, err = filepath.Abs(srcPath)
138+
if err != nil {
139+
return fmt.Errorf("normalizing archive path: %w", err)
140+
}
141+
142+
dstDirPath, err = filepath.Abs(dstDirPath)
143+
if err != nil {
144+
return fmt.Errorf("normalizing archive destination path: %w", err)
145+
}
146+
147+
tarFile, err := os.Open(srcPath)
148+
if err != nil {
149+
return fmt.Errorf("opening archive %s: %w", srcPath, err)
150+
}
151+
defer func() {
152+
err = errors.Join(err, closeAndWrapErr(tarFile, "closing archive %s: %w", srcPath))
153+
}()
154+
155+
var tarDst io.Reader = tarFile
156+
if enableCompression {
157+
var gzipReader *gzip.Reader
158+
gzipReader, err = gzip.NewReader(tarFile)
159+
if err != nil {
160+
return fmt.Errorf("creating gzip reader: %w", err)
161+
}
162+
defer func() {
163+
err = errors.Join(err, closeAndWrapErr(gzipReader, "closing gzip reader: %w"))
164+
}()
165+
166+
tarDst = gzipReader
167+
}
168+
169+
tarReader := tar.NewReader(tarDst)
170+
171+
for {
172+
var tarHeader *tar.Header
173+
tarHeader, err = tarReader.Next()
174+
if err == io.EOF {
175+
break
176+
}
177+
if err != nil {
178+
return fmt.Errorf("reading tar header of %s: %w", srcPath, err)
179+
}
180+
181+
fileInfo := tarHeader.FileInfo()
182+
183+
filePath := filepath.Join(dstDirPath, tarHeader.Name)
184+
185+
// protect against "Zip Slip"
186+
if !strings.HasPrefix(filePath, dstDirPath) {
187+
// mimic standard error, which will be returned in future versions of Go by default
188+
// more info can be found by "tarinsecurepath" variable name
189+
return tar.ErrInsecurePath
190+
}
191+
192+
fileDirPath := filePath
193+
if !fileInfo.Mode().IsDir() {
194+
fileDirPath = filepath.Dir(fileDirPath)
195+
}
196+
197+
if err = os.MkdirAll(fileDirPath, 0755); err != nil {
198+
return fmt.Errorf("making dirs for path %s: %w", fileDirPath, err)
199+
}
200+
201+
if fileInfo.Mode().IsDir() {
202+
continue
203+
}
204+
205+
err = tarUnpackFile(filePath, tarReader, fileInfo)
206+
if err != nil {
207+
return fmt.Errorf("unpacking archive %s: %w", filePath, err)
208+
}
209+
}
210+
return nil
211+
}
212+
213+
func tarUnpackFile(dstFileName string, src io.Reader, srcFileInfo fs.FileInfo) (err error) {
214+
var dstFile *os.File
215+
dstFile, err = os.OpenFile(dstFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcFileInfo.Mode().Perm())
216+
if err != nil {
217+
return fmt.Errorf("opening destination file %s: %w", dstFileName, err)
218+
}
219+
defer func() {
220+
err = errors.Join(err, closeAndWrapErr(dstFile, "closing destination file %s: %w", dstFile))
221+
}()
222+
223+
n, err := io.Copy(dstFile, src)
224+
if err != nil {
225+
return fmt.Errorf("copying to destination file %s: %w", dstFileName, err)
226+
}
227+
228+
if srcFileInfo.Mode().IsRegular() && n != srcFileInfo.Size() {
229+
return fmt.Errorf("written size check failed for %s: wrote %d, want %d", dstFileName, n, srcFileInfo.Size())
230+
}
231+
232+
return nil
233+
}
234+
235+
func closeAndWrapErr(closer io.Closer, errFormat string, a ...any) error {
236+
if err := closer.Close(); err != nil {
237+
a = append(a, err)
238+
return fmt.Errorf(errFormat, a...)
239+
}
240+
return nil
241+
}

0 commit comments

Comments
 (0)