Skip to content

Commit f531b5b

Browse files
committed
Extend NodeUnpublish test to verify cleanup of TargetPath (#336)
The CSI spec states that the SP MUST delete the file or directory that it created as part of NodeUnpublishVolume. This adds a new scenario to the sanity test to verify that NodeUnpublishVolume removes TargetPath.
1 parent bb5ec31 commit f531b5b

File tree

4 files changed

+172
-2
lines changed

4 files changed

+172
-2
lines changed

cmd/csi-sanity/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ func main() {
8181
stringVar(&config.RemoveTargetPathCmd, "removemountpathcmd", "Command to run for target path removal")
8282
stringVar(&config.RemoveStagingPathCmd, "removestagingpathcmd", "Command to run for staging path removal")
8383
durationVar(&config.RemovePathCmdTimeout, "removepathcmdtimeout", "Timeout for the commands to remove target and staging paths, in seconds")
84+
stringVar(&config.CheckPathCmd, "checkpathcmd", "Command to run to check a given path. It must print 'file', 'directory', 'not_found', or 'other' on stdout.")
85+
durationVar(&config.CheckPathCmdTimeout, "checkpathcmdtimeout", "Timeout for the command to check a given path, in seconds")
8486
stringVar(&config.SecretsFile, "secrets", "CSI secrets file")
8587
stringVar(&config.TestVolumeAccessType, "testvolumeaccesstype", "Volume capability access type, valid values are mount or block")
8688
int64Var(&config.TestVolumeSize, "testvolumesize", "Base volume size used for provisioned volumes")

hack/e2e.sh

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,23 @@ echo $targetpath
101101
rm -rf $@
102102
' > custompathremoval.bash
103103

104-
chmod +x custompathcreation.bash custompathremoval.bash
104+
# Create a script for custom target path check.
105+
echo '#!/bin/bash
106+
if [ -f "$1" ]; then
107+
echo "file"
108+
elif [ -d "$1" ]; then
109+
echo "directory"
110+
elif [ -e "$1" ]; then
111+
echo "other"
112+
else
113+
echo "not_found"
114+
fi
115+
' > custompathcheck.bash
116+
105117
local creationscriptpath="$PWD/custompathcreation.bash"
106118
local removalscriptpath="$PWD/custompathremoval.bash"
119+
local checkscriptpath="$PWD/custompathcheck.bash"
120+
chmod +x $creationscriptpath $removalscriptpath $checkscriptpath
107121

108122
CSI_ENDPOINT=$1 ./bin/mock-driver &
109123
local pid=$!
@@ -116,7 +130,8 @@ rm -rf $@
116130
--csi.createmountpathcmd=$creationscriptpath \
117131
--csi.createstagingpathcmd=$creationscriptpath \
118132
--csi.removemountpathcmd=$removalscriptpath \
119-
--csi.removestagingpathcmd=$removalscriptpath
133+
--csi.removestagingpathcmd=$removalscriptpath \
134+
--csi.checkpathcmd=$checkscriptpath \
120135
)
121136

122137
make

pkg/sanity/node.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,64 @@ var _ = DescribeSanity("Node Service", func(sc *TestContext) {
449449
Expect(ok).To(BeTrue())
450450
Expect(serverError.Code()).To(Equal(codes.InvalidArgument), "unexpected error: %s", serverError.Message())
451451
})
452+
453+
It("should remove target path", func() {
454+
// This test may break for consumers that are using
455+
// custom target path functions if they have not yet
456+
// implemented similar functionality to check if the
457+
// path exists. Skip this test if there is a custom
458+
// command or function provided to create the path,
459+
// but not yet provided to check the path.
460+
if sc.Config.CreateTargetPathCmd != "" && sc.Config.CheckPathCmd == "" {
461+
Skip("CreateTargetPathCmd was set, but CheckPathCmd was not. Please update your testing configuration to enable CheckPathCmd.")
462+
}
463+
if sc.Config.CreateTargetDir != nil && sc.Config.CheckPath == nil {
464+
Skip("CreateTargetDir was set, but CheckPath was not. Please update your testing configuration to enable CheckPath.")
465+
}
466+
467+
name := UniqueString("sanity-node-unpublish-volume")
468+
vol := createVolume(name)
469+
volid := vol.GetVolume().GetVolumeId()
470+
volpath := sc.TargetPath + "/target"
471+
472+
By("Getting a node id")
473+
nid, err := r.NodeGetInfo(
474+
context.Background(),
475+
&csi.NodeGetInfoRequest{})
476+
Expect(err).NotTo(HaveOccurred())
477+
Expect(nid).NotTo(BeNil())
478+
Expect(nid.GetNodeId()).NotTo(BeEmpty())
479+
480+
By("Staging and publishing a volume")
481+
conpubvol := controllerPublishVolume(name, vol, nid)
482+
_ = nodeStageVolume(name, vol, conpubvol)
483+
_ = nodePublishVolume(name, vol, conpubvol)
484+
485+
// Verify that the path exists before calling
486+
// NodeUnpublishVolume.
487+
By("Checking the target path exists")
488+
pa, err := CheckPath(volpath, sc.Config)
489+
Expect(err).NotTo(HaveOccurred(), "checking path %q", volpath)
490+
Expect(pa).NotTo(Equal(PathIsNotFound), "path %q should have been created by CSI driver and the test config should be enabling testing for that path", volpath)
491+
492+
By("Unpublishing the volume")
493+
_, err = r.NodeUnpublishVolume(
494+
context.Background(),
495+
&csi.NodeUnpublishVolumeRequest{
496+
VolumeId: volid,
497+
TargetPath: volpath,
498+
},
499+
)
500+
Expect(err).NotTo(HaveOccurred())
501+
502+
// The CSI spec states that the SP MUST delete
503+
// the file or directory it created at this path
504+
// as part of NodeUnpublishVolume.
505+
By("Checking the target path was removed")
506+
pa, err = CheckPath(volpath, sc.Config)
507+
Expect(err).NotTo(HaveOccurred(), "checking path %q", volpath)
508+
Expect(pa).To(Equal(PathIsNotFound), "path %q should have been removed by the CSI driver during NodeUnpublishVolume", volpath)
509+
})
452510
})
453511

454512
Describe("NodeStageVolume", func() {

pkg/sanity/sanity.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ type TestConfig struct {
164164
// n > 0: repeat each call n times
165165
// NewTestConfig() by default enables idempotency testing.
166166
IdempotentCount int
167+
168+
// CheckPath is a callback function to check whether the given path exists.
169+
// If this is not set, then defaultCheckPath will be used instead.
170+
CheckPath func(path string) (PathKind, error)
171+
// Command to be executed for a customized way to check a given path.
172+
CheckPathCmd string
173+
// Timeout for the executed command to check a given path.
174+
CheckPathCmdTimeout time.Duration
167175
}
168176

169177
// TestContext gets initialized by the sanity package before each test
@@ -194,6 +202,7 @@ func NewTestConfig() TestConfig {
194202
TestVolumeAccessType: "mount",
195203
IDGen: &DefaultIDGenerator{},
196204
IdempotentCount: 10,
205+
CheckPathCmdTimeout: 10 * time.Second,
197206

198207
DialOptions: []grpc.DialOption{grpc.WithInsecure()},
199208
ControllerDialOptions: []grpc.DialOption{grpc.WithInsecure()},
@@ -449,3 +458,89 @@ func PseudoUUID() string {
449458
func UniqueString(prefix string) string {
450459
return prefix + uniqueSuffix
451460
}
461+
462+
// Return codes for CheckPath
463+
type PathKind string
464+
465+
const (
466+
PathIsFile PathKind = "file"
467+
PathIsDir PathKind = "directory"
468+
PathIsNotFound PathKind = "not_found"
469+
PathIsOther PathKind = "other"
470+
)
471+
472+
// IsPathKind validates that the input string matches one of the defined
473+
// PathKind values above. If successful, it returns the corresponding
474+
// PathKind type. Otherwise, it returns an error.
475+
func IsPathKind(in string) (PathKind, error) {
476+
pk := PathKind(in)
477+
switch pk {
478+
case PathIsFile, PathIsDir, PathIsNotFound, PathIsOther:
479+
return pk, nil
480+
default:
481+
return pk, fmt.Errorf("invalid PathType: %s", pk)
482+
}
483+
}
484+
485+
// defaultCheckPath runs os.Stat against the provided path and returns
486+
// a code indicating whether it's a file, directory, not found, or other.
487+
// If an error occurs, it returns an empty string along with the error.
488+
func defaultCheckPath(path string) (PathKind, error) {
489+
var pk PathKind
490+
fi, err := os.Stat(path)
491+
if err != nil {
492+
if os.IsNotExist(err) {
493+
return PathIsNotFound, nil
494+
} else {
495+
return "", err
496+
}
497+
}
498+
switch mode := fi.Mode(); {
499+
case mode.IsRegular():
500+
pk = PathIsFile
501+
case mode.IsDir():
502+
pk = PathIsDir
503+
default:
504+
pk = PathIsOther
505+
}
506+
return pk, nil
507+
}
508+
509+
// CheckPath takes a path parameter and returns a code indicating whether
510+
// it's a file, directory, not found, or other. This can be done using a
511+
// custom command, custom function, or by the defaultCheckPath function.
512+
// If an error occurs, it returns an empty string along with the error.
513+
func CheckPath(path string, config *TestConfig) (PathKind, error) {
514+
if path == "" {
515+
return "", fmt.Errorf("path argument must not be empty")
516+
}
517+
if config == nil {
518+
return "", fmt.Errorf("config argument must not be nil")
519+
}
520+
521+
if config.CheckPathCmd != "" {
522+
// Check the provided path using the check path command.
523+
ctx, cancel := context.WithTimeout(context.Background(), config.CheckPathCmdTimeout)
524+
defer cancel()
525+
526+
cmd := exec.CommandContext(ctx, config.CheckPathCmd, path)
527+
cmd.Stderr = os.Stderr
528+
out, err := cmd.Output()
529+
if err != nil {
530+
return "", fmt.Errorf("check path command %s failed: %v", config.CheckPathCmd, err)
531+
}
532+
// The output of this command is expected to match the value for
533+
// PathIsFile, PathIsDir, PathIsNotFound, or PathIsOther.
534+
pk, err := IsPathKind(strings.TrimSpace(string(out)))
535+
if err != nil {
536+
return "", fmt.Errorf("check path command %s failed: %v", config.CheckPathCmd, err)
537+
}
538+
return pk, nil
539+
} else if config.CheckPath != nil {
540+
// Check the path using a custom callback function.
541+
return config.CheckPath(path)
542+
} else {
543+
// Use defaultCheckPath if no custom function was provided.
544+
return defaultCheckPath(path)
545+
}
546+
}

0 commit comments

Comments
 (0)