Skip to content

Commit d4baffe

Browse files
committed
Allow ignoring unreadable files when snapshotting volumes
Snapshots are implemented using `tar` for file system volumes. With this patch, users can set the `ignoreFailedRead` parameter in the VolumeSnapshotClass parameter to `true`, which results in the `--ignore-failed-read` option to be passed to `tar`. This enhancement is particularly useful for end-to-end testing of applications that may delete files initially included when the tar process started and retrieved the file list. By ignoring unreadable files, the snapshot process can finish successfully. Signed-off-by: Leonardo Cecchi <[email protected]>
1 parent 428b523 commit d4baffe

File tree

7 files changed

+228
-11
lines changed

7 files changed

+228
-11
lines changed

pkg/hostpath/README.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,25 @@ $ csc identity plugin-info --endpoint tcp://127.0.0.1:10000
2121
"csi-hostpath" "0.1.0"
2222
```
2323

24-
#### Create a volume
24+
#### Create a block volume
2525
```
26-
$ csc controller new --endpoint tcp://127.0.0.1:10000 --cap 1,block CSIVolumeName
26+
$ csc controller new --endpoint tcp://127.0.0.1:10000 --cap 1,block --req-bytes 1048576 --lim-bytes 1048576 CSIVolumeName
2727
CSIVolumeID
2828
```
2929

30+
#### Create mounted volume
31+
```
32+
$ csc controller new --endpoint tcp://127.0.0.1:10000 --cap MULTI_NODE_MULTI_WRITER,mount,xfs,uid=500,gid=500 CSIVolumeName
33+
CSIVolumeID
34+
```
35+
36+
#### List volumes
37+
```
38+
csc controller list-volumes --endpoint tcp://127.0.0.1:10000
39+
CSIVolumeID 0
40+
CSIVolumeID 0
41+
```
42+
3043
#### Delete a volume
3144
```
3245
$ csc controller del --endpoint tcp://127.0.0.1:10000 CSIVolumeID
@@ -56,3 +69,19 @@ CSIVolumeID
5669
$ csc node get-info --endpoint tcp://127.0.0.1:10000
5770
CSINode
5871
```
72+
73+
### Create snapshot
74+
```
75+
$ csc controller create-snapshot --endpoint tcp://127.0.0.1:10000 --params ignoreFailedRead=true --source-volume CSIVolumeID CSISnapshotName
76+
CSISnapshotID
77+
```
78+
79+
### Delete snapshot
80+
```
81+
csc controller delete-snapshot --endpoint tcp://127.0.0.1:10000 CSISnapshotID
82+
```
83+
84+
### List snapshots
85+
```
86+
csc controller list-snapshots --endpoint tcp://127.0.0.1:10000
87+
```

pkg/hostpath/controllerserver.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,8 +586,12 @@ func (hp *hostPath) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotR
586586
snapshotID := uuid.NewUUID().String()
587587
creationTime := ptypes.TimestampNow()
588588
file := hp.getSnapshotPath(snapshotID)
589+
opts, err := optionsFromParameters(hostPathVolume, req.Parameters)
590+
if err != nil {
591+
return nil, status.Errorf(codes.InvalidArgument, "invalid volume snapshot class parameters: %s", err.Error())
592+
}
589593

590-
if err := hp.createSnapshotFromVolume(hostPathVolume, file); err != nil {
594+
if err := hp.createSnapshotFromVolume(hostPathVolume, file, opts...); err != nil {
591595
return nil, err
592596
}
593597

pkg/hostpath/groupcontrollerserver.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,15 @@ func (hp *hostPath) CreateVolumeGroupSnapshot(ctx context.Context, req *csi.Crea
122122
return nil, err
123123
}
124124

125+
opts, err := optionsFromParameters(hostPathVolume, req.Parameters)
126+
if err != nil {
127+
return nil, status.Errorf(codes.InvalidArgument, "invalid volume group snapshot class parameters: %s", err.Error())
128+
}
129+
125130
snapshotID := uuid.NewUUID().String()
126131
file := hp.getSnapshotPath(snapshotID)
127132

128-
if err := hp.createSnapshotFromVolume(hostPathVolume, file); err != nil {
133+
if err := hp.createSnapshotFromVolume(hostPathVolume, file, opts...); err != nil {
129134
return nil, err
130135
}
131136

pkg/hostpath/hostpath.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -380,17 +380,25 @@ func (hp *hostPath) getAttachCount() int64 {
380380
return count
381381
}
382382

383-
func (hp *hostPath) createSnapshotFromVolume(vol state.Volume, file string) error {
384-
var cmd []string
383+
func (hp *hostPath) createSnapshotFromVolume(vol state.Volume, file string, opts ...string) error {
384+
var args []string
385+
var cmdName string
385386
if vol.VolAccessType == state.BlockAccess {
386387
klog.V(4).Infof("Creating snapshot of Raw Block Mode Volume")
387-
cmd = []string{"cp", vol.VolPath, file}
388+
cmdName = "cp"
389+
args = []string{vol.VolPath, file}
388390
} else {
389-
klog.V(4).Infof("Creating snapshot of Filsystem Mode Volume")
390-
cmd = []string{"tar", "czf", file, "-C", vol.VolPath, "."}
391+
klog.V(4).Infof("Creating snapshot of Filesystem Mode Volume")
392+
cmdName = "tar"
393+
opts = append(
394+
[]string{"czf", file, "-C", vol.VolPath},
395+
opts...,
396+
)
397+
args = []string{"."}
391398
}
392399
executor := utilexec.New()
393-
out, err := executor.Command(cmd[0], cmd[1:]...).CombinedOutput()
400+
optsAndArgs := append(opts, args...)
401+
out, err := executor.Command(cmdName, optsAndArgs...).CombinedOutput()
394402
if err != nil {
395403
return fmt.Errorf("failed create snapshot: %w: %s", err, out)
396404
}

pkg/hostpath/options.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 hostpath
18+
19+
import (
20+
"fmt"
21+
"strconv"
22+
23+
"github.com/kubernetes-csi/csi-driver-host-path/pkg/state"
24+
)
25+
26+
// ignoreFailedReadParameterName is a parameter that, when set to true,
27+
// causes the `--ignore-failed-read` option to be passed to `tar`.
28+
const ignoreFailedReadParameterName = "ignoreFailedRead"
29+
30+
func optionsFromParameters(vol state.Volume, parameters map[string]string) ([]string, error) {
31+
// We do not support options for snapshots of block volumes
32+
if vol.VolAccessType == state.BlockAccess {
33+
return nil, nil
34+
}
35+
36+
ignoreFailedReadString := parameters[ignoreFailedReadParameterName]
37+
if len(ignoreFailedReadString) == 0 {
38+
return nil, nil
39+
}
40+
41+
if ok, err := strconv.ParseBool(ignoreFailedReadString); err != nil {
42+
return nil, fmt.Errorf(
43+
"invalid value for %q, expected boolean but was %q",
44+
ignoreFailedReadParameterName,
45+
ignoreFailedReadString,
46+
)
47+
} else if ok {
48+
return []string{"--ignore-failed-read"}, nil
49+
}
50+
51+
return nil, nil
52+
}

pkg/hostpath/options_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 hostpath
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
23+
"github.com/kubernetes-csi/csi-driver-host-path/pkg/state"
24+
)
25+
26+
func TestOptionsFromParameters(t *testing.T) {
27+
mountedVolume := state.Volume{
28+
VolAccessType: state.MountAccess,
29+
}
30+
blockVolume := state.Volume{
31+
VolAccessType: state.BlockAccess,
32+
}
33+
34+
cases := []struct {
35+
testName string
36+
volume state.Volume
37+
params map[string]string
38+
result []string
39+
success bool
40+
}{
41+
{
42+
testName: "no options passed (mounted)",
43+
params: nil,
44+
result: nil,
45+
success: true,
46+
volume: mountedVolume,
47+
},
48+
{
49+
testName: "no options passed (block)",
50+
params: nil,
51+
result: nil,
52+
success: true,
53+
volume: blockVolume,
54+
},
55+
{
56+
testName: "ignoreFailedRead=false (mounted)",
57+
params: map[string]string{
58+
ignoreFailedReadParameterName: "false",
59+
},
60+
result: nil,
61+
success: true,
62+
volume: mountedVolume,
63+
},
64+
{
65+
testName: "ignoreFailedRead=true (mounted)",
66+
params: map[string]string{
67+
ignoreFailedReadParameterName: "true",
68+
},
69+
result: []string{
70+
"--ignore-failed-read",
71+
},
72+
success: true,
73+
volume: mountedVolume,
74+
},
75+
{
76+
testName: "invalid ignoreFailedRead (mounted)",
77+
params: map[string]string{
78+
ignoreFailedReadParameterName: "ABC",
79+
},
80+
result: nil,
81+
success: false,
82+
volume: mountedVolume,
83+
},
84+
{
85+
testName: "ignoreFailedRead=true (block)",
86+
params: map[string]string{
87+
ignoreFailedReadParameterName: "true",
88+
},
89+
result: nil,
90+
success: true,
91+
volume: blockVolume,
92+
},
93+
{
94+
testName: "invalid ignoreFailedRead (block)",
95+
params: map[string]string{
96+
ignoreFailedReadParameterName: "ABC",
97+
},
98+
result: nil,
99+
success: true,
100+
volume: blockVolume,
101+
},
102+
}
103+
104+
for _, tt := range cases {
105+
t.Run(tt.testName, func(t *testing.T) {
106+
result, err := optionsFromParameters(tt.volume, tt.params)
107+
if tt.success && err != nil {
108+
t.Fatalf("expected success, found error: %s", err.Error())
109+
}
110+
if !tt.success && err == nil {
111+
t.Fatalf("expected failure, but succeeded with %q", result)
112+
}
113+
114+
if tt.success && !reflect.DeepEqual(tt.result, result) {
115+
t.Fatalf("expected %q but received %q", tt.result, result)
116+
}
117+
})
118+
}
119+
}

pkg/state/strings.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (s *Strings) Empty() bool {
4040
return len(*s) == 0
4141
}
4242

43-
// Remove removes the first occurence of the string, if present.
43+
// Remove removes the first occurrence of the string, if present.
4444
func (s *Strings) Remove(str string) {
4545
for i, str2 := range *s {
4646
if str == str2 {

0 commit comments

Comments
 (0)