Skip to content

Commit 3f15d4d

Browse files
committed
replace tar (cli) with code-only packing
1 parent 49ec91f commit 3f15d4d

File tree

9 files changed

+792
-4
lines changed

9 files changed

+792
-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: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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/filepath"
28+
"strings"
29+
)
30+
31+
func TarPack(srcDirPath string, dstPath string, enableCompression bool) error {
32+
// normalize all paths to be absolute and clean
33+
dstPath, err := filepath.Abs(dstPath)
34+
if err != nil {
35+
return fmt.Errorf("normalizing destination path: %w", err)
36+
}
37+
38+
srcDirPath, err = filepath.Abs(srcDirPath)
39+
if err != nil {
40+
return fmt.Errorf("normalizing source path: %w", err)
41+
}
42+
43+
if strings.HasPrefix(filepath.Dir(dstPath), srcDirPath) {
44+
return fmt.Errorf("destination file %s cannot be under source directory %s", dstPath, srcDirPath)
45+
}
46+
47+
tarFile, err := os.Create(dstPath)
48+
if err != nil {
49+
return fmt.Errorf("creating destination file: %w", err)
50+
}
51+
defer func() {
52+
err = errors.Join(err, closeAndWrapErr(tarFile, "closing destination file %s: %w", dstPath))
53+
}()
54+
55+
var tarDst io.Writer = tarFile
56+
if enableCompression {
57+
gzipWriter := gzip.NewWriter(tarFile)
58+
defer func() {
59+
err = errors.Join(err, closeAndWrapErr(gzipWriter, "closing gzip writer"))
60+
}()
61+
tarDst = gzipWriter
62+
}
63+
64+
tarWriter := tar.NewWriter(tarDst)
65+
defer func() {
66+
err = errors.Join(err, closeAndWrapErr(tarWriter, "closing tar writer"))
67+
}()
68+
69+
// recursively visit every file and write it
70+
if err = filepath.Walk(
71+
srcDirPath,
72+
func(srcSubPath string, fileInfo fs.FileInfo, walkErr error) error {
73+
return tarVisitFileToPack(tarWriter, srcDirPath, srcSubPath, fileInfo, walkErr)
74+
},
75+
); err != nil {
76+
return fmt.Errorf("walking source directory: %w", err)
77+
}
78+
79+
return nil
80+
}
81+
82+
func tarVisitFileToPack(
83+
tarWriter *tar.Writer,
84+
srcPath string,
85+
srcSubPath string,
86+
fileInfo os.FileInfo,
87+
walkErr error,
88+
) (err error) {
89+
if walkErr != nil {
90+
return walkErr
91+
}
92+
93+
linkTarget := ""
94+
if fileInfo.Mode()&fs.ModeSymlink != 0 {
95+
linkTarget, err = os.Readlink(srcSubPath)
96+
if err != nil {
97+
return fmt.Errorf("reading link %s: %w", srcSubPath, err)
98+
}
99+
}
100+
101+
tarHeader, err := tar.FileInfoHeader(fileInfo, linkTarget)
102+
if err != nil {
103+
return fmt.Errorf("creating tar header for %s: %w", srcSubPath, err)
104+
}
105+
106+
// srcSubPath always starts with srcPath and both are absolute
107+
tarHeader.Name, err = filepath.Rel(srcPath, srcSubPath)
108+
if err != nil {
109+
return fmt.Errorf("making tar header name for file %s: %w", srcSubPath, err)
110+
}
111+
112+
if err = tarWriter.WriteHeader(tarHeader); err != nil {
113+
return fmt.Errorf("writing tar header for file %s: %w", srcSubPath, err)
114+
}
115+
116+
if !fileInfo.Mode().IsRegular() {
117+
return nil
118+
}
119+
120+
srcFile, err := os.Open(srcSubPath)
121+
if err != nil {
122+
return fmt.Errorf("opening file being packed %s: %w", srcSubPath, err)
123+
}
124+
defer func() {
125+
err = errors.Join(err, closeAndWrapErr(srcFile, "closing file being packed %s: %w", srcSubPath))
126+
}()
127+
_, err = io.Copy(tarWriter, srcFile)
128+
if err != nil {
129+
return fmt.Errorf("packing file %s: %w", srcSubPath, err)
130+
}
131+
return nil
132+
}
133+
134+
func TarUnpack(srcPath, dstDirPath string, enableCompression bool) (err error) {
135+
// normalize all paths to be absolute and clean
136+
srcPath, err = filepath.Abs(srcPath)
137+
if err != nil {
138+
return fmt.Errorf("normalizing archive path: %w", err)
139+
}
140+
141+
dstDirPath, err = filepath.Abs(dstDirPath)
142+
if err != nil {
143+
return fmt.Errorf("normalizing archive destination path: %w", err)
144+
}
145+
146+
tarFile, err := os.Open(srcPath)
147+
if err != nil {
148+
return fmt.Errorf("opening archive %s: %w", srcPath, err)
149+
}
150+
defer func() {
151+
err = errors.Join(err, closeAndWrapErr(tarFile, "closing archive %s: %w", srcPath))
152+
}()
153+
154+
var tarDst io.Reader = tarFile
155+
if enableCompression {
156+
var gzipReader *gzip.Reader
157+
gzipReader, err = gzip.NewReader(tarFile)
158+
if err != nil {
159+
return fmt.Errorf("creating gzip reader: %w", err)
160+
}
161+
defer func() {
162+
err = errors.Join(err, closeAndWrapErr(gzipReader, "closing gzip reader: %w"))
163+
}()
164+
165+
tarDst = gzipReader
166+
}
167+
168+
tarReader := tar.NewReader(tarDst)
169+
170+
for {
171+
var tarHeader *tar.Header
172+
tarHeader, err = tarReader.Next()
173+
if err == io.EOF {
174+
break
175+
}
176+
if err != nil {
177+
return fmt.Errorf("reading tar header of %s: %w", srcPath, err)
178+
}
179+
180+
fileInfo := tarHeader.FileInfo()
181+
182+
filePath := filepath.Join(dstDirPath, tarHeader.Name)
183+
184+
// protect against "Zip Slip"
185+
if !strings.HasPrefix(filePath, dstDirPath) {
186+
// mimic standard error, which will be returned in future versions of Go by default
187+
// more info can be found by "tarinsecurepath" variable name
188+
return tar.ErrInsecurePath
189+
}
190+
191+
fileDirPath := filePath
192+
if !fileInfo.Mode().IsDir() {
193+
fileDirPath = filepath.Dir(fileDirPath)
194+
}
195+
196+
if err = os.MkdirAll(fileDirPath, 0755); err != nil {
197+
return fmt.Errorf("making dirs for path %s: %w", fileDirPath, err)
198+
}
199+
200+
if fileInfo.Mode().IsDir() {
201+
continue
202+
}
203+
204+
if fileInfo.Mode()&fs.ModeSymlink != 0 {
205+
if err := os.Symlink(tarHeader.Linkname, filePath); err != nil {
206+
return fmt.Errorf("creating symlink %s: %w", filePath, err)
207+
}
208+
continue
209+
}
210+
211+
if err = tarUnpackFile(filePath, tarReader, fileInfo); err != nil {
212+
return fmt.Errorf("unpacking file %s: %w", filePath, err)
213+
}
214+
}
215+
return nil
216+
}
217+
218+
func tarUnpackFile(dstFileName string, src io.Reader, srcFileInfo fs.FileInfo) (err error) {
219+
var dstFile *os.File
220+
dstFile, err = os.OpenFile(dstFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcFileInfo.Mode().Perm())
221+
if err != nil {
222+
return fmt.Errorf("opening destination file %s: %w", dstFileName, err)
223+
}
224+
defer func() {
225+
err = errors.Join(err, closeAndWrapErr(dstFile, "closing destination file %s: %w", dstFile))
226+
}()
227+
228+
n, err := io.Copy(dstFile, src)
229+
if err != nil {
230+
return fmt.Errorf("copying to destination file %s: %w", dstFileName, err)
231+
}
232+
233+
if srcFileInfo.Mode().IsRegular() && n != srcFileInfo.Size() {
234+
return fmt.Errorf("written size check failed for %s: wrote %d, want %d", dstFileName, n, srcFileInfo.Size())
235+
}
236+
237+
return nil
238+
}
239+
240+
func closeAndWrapErr(closer io.Closer, errFormat string, a ...any) error {
241+
if err := closer.Close(); err != nil {
242+
a = append(a, err)
243+
return fmt.Errorf(errFormat, a...)
244+
}
245+
return nil
246+
}

0 commit comments

Comments
 (0)