1
16
17 package secret
18
19 import (
20 "fmt"
21 "io/ioutil"
22 "os"
23 "path/filepath"
24 "reflect"
25 "runtime"
26 "strings"
27 "testing"
28
29 "k8s.io/api/core/v1"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/types"
32 clientset "k8s.io/client-go/kubernetes"
33 "k8s.io/client-go/kubernetes/fake"
34 "k8s.io/kubernetes/pkg/volume"
35 "k8s.io/kubernetes/pkg/volume/emptydir"
36 volumetest "k8s.io/kubernetes/pkg/volume/testing"
37 "k8s.io/kubernetes/pkg/volume/util"
38
39 "github.com/stretchr/testify/assert"
40 )
41
42 func TestMakePayload(t *testing.T) {
43 caseMappingMode := int32(0400)
44 cases := []struct {
45 name string
46 mappings []v1.KeyToPath
47 secret *v1.Secret
48 mode int32
49 optional bool
50 payload map[string]util.FileProjection
51 success bool
52 }{
53 {
54 name: "no overrides",
55 secret: &v1.Secret{
56 Data: map[string][]byte{
57 "foo": []byte("foo"),
58 "bar": []byte("bar"),
59 },
60 },
61 mode: 0644,
62 payload: map[string]util.FileProjection{
63 "foo": {Data: []byte("foo"), Mode: 0644},
64 "bar": {Data: []byte("bar"), Mode: 0644},
65 },
66 success: true,
67 },
68 {
69 name: "basic 1",
70 mappings: []v1.KeyToPath{
71 {
72 Key: "foo",
73 Path: "path/to/foo.txt",
74 },
75 },
76 secret: &v1.Secret{
77 Data: map[string][]byte{
78 "foo": []byte("foo"),
79 "bar": []byte("bar"),
80 },
81 },
82 mode: 0644,
83 payload: map[string]util.FileProjection{
84 "path/to/foo.txt": {Data: []byte("foo"), Mode: 0644},
85 },
86 success: true,
87 },
88 {
89 name: "subdirs",
90 mappings: []v1.KeyToPath{
91 {
92 Key: "foo",
93 Path: "path/to/1/2/3/foo.txt",
94 },
95 },
96 secret: &v1.Secret{
97 Data: map[string][]byte{
98 "foo": []byte("foo"),
99 "bar": []byte("bar"),
100 },
101 },
102 mode: 0644,
103 payload: map[string]util.FileProjection{
104 "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644},
105 },
106 success: true,
107 },
108 {
109 name: "subdirs 2",
110 mappings: []v1.KeyToPath{
111 {
112 Key: "foo",
113 Path: "path/to/1/2/3/foo.txt",
114 },
115 },
116 secret: &v1.Secret{
117 Data: map[string][]byte{
118 "foo": []byte("foo"),
119 "bar": []byte("bar"),
120 },
121 },
122 mode: 0644,
123 payload: map[string]util.FileProjection{
124 "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644},
125 },
126 success: true,
127 },
128 {
129 name: "subdirs 3",
130 mappings: []v1.KeyToPath{
131 {
132 Key: "foo",
133 Path: "path/to/1/2/3/foo.txt",
134 },
135 {
136 Key: "bar",
137 Path: "another/path/to/the/esteemed/bar.bin",
138 },
139 },
140 secret: &v1.Secret{
141 Data: map[string][]byte{
142 "foo": []byte("foo"),
143 "bar": []byte("bar"),
144 },
145 },
146 mode: 0644,
147 payload: map[string]util.FileProjection{
148 "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644},
149 "another/path/to/the/esteemed/bar.bin": {Data: []byte("bar"), Mode: 0644},
150 },
151 success: true,
152 },
153 {
154 name: "non existent key",
155 mappings: []v1.KeyToPath{
156 {
157 Key: "zab",
158 Path: "path/to/foo.txt",
159 },
160 },
161 secret: &v1.Secret{
162 Data: map[string][]byte{
163 "foo": []byte("foo"),
164 "bar": []byte("bar"),
165 },
166 },
167 mode: 0644,
168 success: false,
169 },
170 {
171 name: "mapping with Mode",
172 mappings: []v1.KeyToPath{
173 {
174 Key: "foo",
175 Path: "foo.txt",
176 Mode: &caseMappingMode,
177 },
178 {
179 Key: "bar",
180 Path: "bar.bin",
181 Mode: &caseMappingMode,
182 },
183 },
184 secret: &v1.Secret{
185 Data: map[string][]byte{
186 "foo": []byte("foo"),
187 "bar": []byte("bar"),
188 },
189 },
190 mode: 0644,
191 payload: map[string]util.FileProjection{
192 "foo.txt": {Data: []byte("foo"), Mode: caseMappingMode},
193 "bar.bin": {Data: []byte("bar"), Mode: caseMappingMode},
194 },
195 success: true,
196 },
197 {
198 name: "mapping with defaultMode",
199 mappings: []v1.KeyToPath{
200 {
201 Key: "foo",
202 Path: "foo.txt",
203 },
204 {
205 Key: "bar",
206 Path: "bar.bin",
207 },
208 },
209 secret: &v1.Secret{
210 Data: map[string][]byte{
211 "foo": []byte("foo"),
212 "bar": []byte("bar"),
213 },
214 },
215 mode: 0644,
216 payload: map[string]util.FileProjection{
217 "foo.txt": {Data: []byte("foo"), Mode: 0644},
218 "bar.bin": {Data: []byte("bar"), Mode: 0644},
219 },
220 success: true,
221 },
222 {
223 name: "optional non existent key",
224 mappings: []v1.KeyToPath{
225 {
226 Key: "zab",
227 Path: "path/to/foo.txt",
228 },
229 },
230 secret: &v1.Secret{
231 Data: map[string][]byte{
232 "foo": []byte("foo"),
233 "bar": []byte("bar"),
234 },
235 },
236 mode: 0644,
237 optional: true,
238 payload: map[string]util.FileProjection{},
239 success: true,
240 },
241 }
242
243 for _, tc := range cases {
244 actualPayload, err := MakePayload(tc.mappings, tc.secret, &tc.mode, tc.optional)
245 if err != nil && tc.success {
246 t.Errorf("%v: unexpected failure making payload: %v", tc.name, err)
247 continue
248 }
249
250 if err == nil && !tc.success {
251 t.Errorf("%v: unexpected success making payload", tc.name)
252 continue
253 }
254
255 if !tc.success {
256 continue
257 }
258
259 if e, a := tc.payload, actualPayload; !reflect.DeepEqual(e, a) {
260 t.Errorf("%v: expected and actual payload do not match", tc.name)
261 }
262 }
263 }
264
265 func newTestHost(t *testing.T, clientset clientset.Interface) (string, volume.VolumeHost) {
266 tempDir, err := ioutil.TempDir("", "secret_volume_test.")
267 if err != nil {
268 t.Fatalf("can't make a temp rootdir: %v", err)
269 }
270
271 return tempDir, volumetest.NewFakeVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins())
272 }
273
274 func TestCanSupport(t *testing.T) {
275 pluginMgr := volume.VolumePluginMgr{}
276 tempDir, host := newTestHost(t, nil)
277 defer os.RemoveAll(tempDir)
278 pluginMgr.InitPlugins(ProbeVolumePlugins(), nil , host)
279
280 plugin, err := pluginMgr.FindPluginByName(secretPluginName)
281 if err != nil {
282 t.Fatal("Can't find the plugin by name")
283 }
284 if plugin.GetPluginName() != secretPluginName {
285 t.Errorf("Wrong name: %s", plugin.GetPluginName())
286 }
287 if !plugin.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{Secret: &v1.SecretVolumeSource{SecretName: ""}}}}) {
288 t.Errorf("Expected true")
289 }
290 if plugin.CanSupport(&volume.Spec{}) {
291 t.Errorf("Expected false")
292 }
293 }
294
295 func TestPlugin(t *testing.T) {
296 var (
297 testPodUID = types.UID("test_pod_uid")
298 testVolumeName = "test_volume_name"
299 testNamespace = "test_secret_namespace"
300 testName = "test_secret_name"
301
302 volumeSpec = volumeSpec(testVolumeName, testName, 0644)
303 secret = secret(testNamespace, testName)
304 client = fake.NewSimpleClientset(&secret)
305 pluginMgr = volume.VolumePluginMgr{}
306 rootDir, host = newTestHost(t, client)
307 )
308 defer os.RemoveAll(rootDir)
309 pluginMgr.InitPlugins(ProbeVolumePlugins(), nil , host)
310
311 plugin, err := pluginMgr.FindPluginByName(secretPluginName)
312 if err != nil {
313 t.Fatal("Can't find the plugin by name")
314 }
315
316 pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}}
317 mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{})
318 if err != nil {
319 t.Errorf("Failed to make a new Mounter: %v", err)
320 }
321 if mounter == nil {
322 t.Fatalf("Got a nil Mounter")
323 }
324
325 volumePath := mounter.GetPath()
326 if !hasPathSuffix(volumePath, "pods/test_pod_uid/volumes/kubernetes.io~secret/test_volume_name") {
327 t.Errorf("Got unexpected path: %s", volumePath)
328 }
329
330 err = mounter.SetUp(volume.MounterArgs{})
331 if err != nil {
332 t.Errorf("Failed to setup volume: %v", err)
333 }
334 if _, err := os.Stat(volumePath); err != nil {
335 if os.IsNotExist(err) {
336 t.Errorf("SetUp() failed, volume path not created: %s", volumePath)
337 } else {
338 t.Errorf("SetUp() failed: %v", err)
339 }
340 }
341
342
343 podWrapperMetadataDir := fmt.Sprintf("%v/pods/test_pod_uid/plugins/kubernetes.io~empty-dir/wrapped_test_volume_name", rootDir)
344
345 if _, err := os.Stat(podWrapperMetadataDir); err != nil {
346 if os.IsNotExist(err) {
347 t.Errorf("SetUp() failed, empty-dir wrapper path is not created: %s", podWrapperMetadataDir)
348 } else {
349 t.Errorf("SetUp() failed: %v", err)
350 }
351 }
352 doTestSecretDataInVolume(volumePath, secret, t)
353 defer doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t)
354
355
356 metrics, err := mounter.GetMetrics()
357 if runtime.GOOS == "linux" {
358 assert.NotEmpty(t, metrics)
359 assert.NoError(t, err)
360 } else {
361 t.Skipf("Volume metrics not supported on %s", runtime.GOOS)
362 }
363 }
364
365 func TestInvalidPathSecret(t *testing.T) {
366 var (
367 testPodUID = types.UID("test_pod_uid")
368 testVolumeName = "test_volume_name"
369 testNamespace = "test_secret_namespace"
370 testName = "test_secret_name"
371
372 volumeSpec = volumeSpec(testVolumeName, testName, 0644)
373 secret = secret(testNamespace, testName)
374 client = fake.NewSimpleClientset(&secret)
375 pluginMgr = volume.VolumePluginMgr{}
376 rootDir, host = newTestHost(t, client)
377 )
378 volumeSpec.Secret.Items = []v1.KeyToPath{
379 {Key: "missing", Path: "missing"},
380 }
381
382 defer os.RemoveAll(rootDir)
383 pluginMgr.InitPlugins(ProbeVolumePlugins(), nil , host)
384
385 plugin, err := pluginMgr.FindPluginByName(secretPluginName)
386 if err != nil {
387 t.Fatal("Can't find the plugin by name")
388 }
389
390 pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}}
391 mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{})
392 if err != nil {
393 t.Errorf("Failed to make a new Mounter: %v", err)
394 }
395 if mounter == nil {
396 t.Fatalf("Got a nil Mounter")
397 }
398
399 volumePath := mounter.GetPath()
400 if !hasPathSuffix(volumePath, "pods/test_pod_uid/volumes/kubernetes.io~secret/test_volume_name") {
401 t.Errorf("Got unexpected path: %s", volumePath)
402 }
403
404 var mounterArgs volume.MounterArgs
405 err = mounter.SetUp(mounterArgs)
406 if err == nil {
407 t.Errorf("Expected error while setting up secret")
408 }
409
410 _, err = os.Stat(volumePath)
411 if err == nil {
412 t.Errorf("Expected path %s to not exist", volumePath)
413 }
414 }
415
416
417
418
419 func TestPluginReboot(t *testing.T) {
420 var (
421 testPodUID = types.UID("test_pod_uid3")
422 testVolumeName = "test_volume_name"
423 testNamespace = "test_secret_namespace"
424 testName = "test_secret_name"
425
426 volumeSpec = volumeSpec(testVolumeName, testName, 0644)
427 secret = secret(testNamespace, testName)
428 client = fake.NewSimpleClientset(&secret)
429 pluginMgr = volume.VolumePluginMgr{}
430 rootDir, host = newTestHost(t, client)
431 )
432 defer os.RemoveAll(rootDir)
433 pluginMgr.InitPlugins(ProbeVolumePlugins(), nil , host)
434
435 plugin, err := pluginMgr.FindPluginByName(secretPluginName)
436 if err != nil {
437 t.Fatal("Can't find the plugin by name")
438 }
439
440 pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}}
441 mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{})
442 if err != nil {
443 t.Errorf("Failed to make a new Mounter: %v", err)
444 }
445 if mounter == nil {
446 t.Fatalf("Got a nil Mounter")
447 }
448
449 podMetadataDir := fmt.Sprintf("%v/pods/test_pod_uid3/plugins/kubernetes.io~secret/test_volume_name", rootDir)
450 util.SetReady(podMetadataDir)
451 volumePath := mounter.GetPath()
452 if !hasPathSuffix(volumePath, "pods/test_pod_uid3/volumes/kubernetes.io~secret/test_volume_name") {
453 t.Errorf("Got unexpected path: %s", volumePath)
454 }
455
456 err = mounter.SetUp(volume.MounterArgs{})
457 if err != nil {
458 t.Errorf("Failed to setup volume: %v", err)
459 }
460 if _, err := os.Stat(volumePath); err != nil {
461 if os.IsNotExist(err) {
462 t.Errorf("SetUp() failed, volume path not created: %s", volumePath)
463 } else {
464 t.Errorf("SetUp() failed: %v", err)
465 }
466 }
467
468 doTestSecretDataInVolume(volumePath, secret, t)
469 doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t)
470 }
471
472 func TestPluginOptional(t *testing.T) {
473 var (
474 testPodUID = types.UID("test_pod_uid")
475 testVolumeName = "test_volume_name"
476 testNamespace = "test_secret_namespace"
477 testName = "test_secret_name"
478 trueVal = true
479
480 volumeSpec = volumeSpec(testVolumeName, testName, 0644)
481 client = fake.NewSimpleClientset()
482 pluginMgr = volume.VolumePluginMgr{}
483 rootDir, host = newTestHost(t, client)
484 )
485 volumeSpec.Secret.Optional = &trueVal
486 defer os.RemoveAll(rootDir)
487 pluginMgr.InitPlugins(ProbeVolumePlugins(), nil , host)
488
489 plugin, err := pluginMgr.FindPluginByName(secretPluginName)
490 if err != nil {
491 t.Fatal("Can't find the plugin by name")
492 }
493
494 pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}}
495 mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{})
496 if err != nil {
497 t.Errorf("Failed to make a new Mounter: %v", err)
498 }
499 if mounter == nil {
500 t.Errorf("Got a nil Mounter")
501 }
502
503 volumePath := mounter.GetPath()
504 if !hasPathSuffix(volumePath, "pods/test_pod_uid/volumes/kubernetes.io~secret/test_volume_name") {
505 t.Errorf("Got unexpected path: %s", volumePath)
506 }
507
508 err = mounter.SetUp(volume.MounterArgs{})
509 if err != nil {
510 t.Errorf("Failed to setup volume: %v", err)
511 }
512 if _, err := os.Stat(volumePath); err != nil {
513 if os.IsNotExist(err) {
514 t.Errorf("SetUp() failed, volume path not created: %s", volumePath)
515 } else {
516 t.Errorf("SetUp() failed: %v", err)
517 }
518 }
519
520
521 podWrapperMetadataDir := fmt.Sprintf("%v/pods/test_pod_uid/plugins/kubernetes.io~empty-dir/wrapped_test_volume_name", rootDir)
522
523 if _, err := os.Stat(podWrapperMetadataDir); err != nil {
524 if os.IsNotExist(err) {
525 t.Errorf("SetUp() failed, empty-dir wrapper path is not created: %s", podWrapperMetadataDir)
526 } else {
527 t.Errorf("SetUp() failed: %v", err)
528 }
529 }
530
531 datadirSymlink := filepath.Join(volumePath, "..data")
532 datadir, err := os.Readlink(datadirSymlink)
533 if err != nil && os.IsNotExist(err) {
534 t.Fatalf("couldn't find volume path's data dir, %s", datadirSymlink)
535 } else if err != nil {
536 t.Fatalf("couldn't read symlink, %s", datadirSymlink)
537 }
538 datadirPath := filepath.Join(volumePath, datadir)
539
540 infos, err := ioutil.ReadDir(volumePath)
541 if err != nil {
542 t.Fatalf("couldn't find volume path, %s", volumePath)
543 }
544 if len(infos) != 0 {
545 for _, fi := range infos {
546 if fi.Name() != "..data" && fi.Name() != datadir {
547 t.Errorf("empty data volume directory, %s, is not empty. Contains: %s", datadirSymlink, fi.Name())
548 }
549 }
550 }
551
552 infos, err = ioutil.ReadDir(datadirPath)
553 if err != nil {
554 t.Fatalf("couldn't find volume data path, %s", datadirPath)
555 }
556 if len(infos) != 0 {
557 t.Errorf("empty data directory, %s, is not empty. Contains: %s", datadirSymlink, infos[0].Name())
558 }
559
560 defer doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t)
561 }
562
563 func TestPluginOptionalKeys(t *testing.T) {
564 var (
565 testPodUID = types.UID("test_pod_uid")
566 testVolumeName = "test_volume_name"
567 testNamespace = "test_secret_namespace"
568 testName = "test_secret_name"
569 trueVal = true
570
571 volumeSpec = volumeSpec(testVolumeName, testName, 0644)
572 secret = secret(testNamespace, testName)
573 client = fake.NewSimpleClientset(&secret)
574 pluginMgr = volume.VolumePluginMgr{}
575 rootDir, host = newTestHost(t, client)
576 )
577 volumeSpec.VolumeSource.Secret.Items = []v1.KeyToPath{
578 {Key: "data-1", Path: "data-1"},
579 {Key: "data-2", Path: "data-2"},
580 {Key: "data-3", Path: "data-3"},
581 {Key: "missing", Path: "missing"},
582 }
583 volumeSpec.Secret.Optional = &trueVal
584 defer os.RemoveAll(rootDir)
585 pluginMgr.InitPlugins(ProbeVolumePlugins(), nil , host)
586
587 plugin, err := pluginMgr.FindPluginByName(secretPluginName)
588 if err != nil {
589 t.Fatal("Can't find the plugin by name")
590 }
591
592 pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, UID: testPodUID}}
593 mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{})
594 if err != nil {
595 t.Errorf("Failed to make a new Mounter: %v", err)
596 }
597 if mounter == nil {
598 t.Errorf("Got a nil Mounter")
599 }
600
601 volumePath := mounter.GetPath()
602 if !hasPathSuffix(volumePath, "pods/test_pod_uid/volumes/kubernetes.io~secret/test_volume_name") {
603 t.Errorf("Got unexpected path: %s", volumePath)
604 }
605
606 err = mounter.SetUp(volume.MounterArgs{})
607 if err != nil {
608 t.Errorf("Failed to setup volume: %v", err)
609 }
610 if _, err := os.Stat(volumePath); err != nil {
611 if os.IsNotExist(err) {
612 t.Errorf("SetUp() failed, volume path not created: %s", volumePath)
613 } else {
614 t.Errorf("SetUp() failed: %v", err)
615 }
616 }
617
618
619 podWrapperMetadataDir := fmt.Sprintf("%v/pods/test_pod_uid/plugins/kubernetes.io~empty-dir/wrapped_test_volume_name", rootDir)
620
621 if _, err := os.Stat(podWrapperMetadataDir); err != nil {
622 if os.IsNotExist(err) {
623 t.Errorf("SetUp() failed, empty-dir wrapper path is not created: %s", podWrapperMetadataDir)
624 } else {
625 t.Errorf("SetUp() failed: %v", err)
626 }
627 }
628 doTestSecretDataInVolume(volumePath, secret, t)
629 defer doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t)
630
631
632 metrics, err := mounter.GetMetrics()
633 if runtime.GOOS == "linux" {
634 assert.NotEmpty(t, metrics)
635 assert.NoError(t, err)
636 } else {
637 t.Skipf("Volume metrics not supported on %s", runtime.GOOS)
638 }
639 }
640
641 func volumeSpec(volumeName, secretName string, defaultMode int32) *v1.Volume {
642 return &v1.Volume{
643 Name: volumeName,
644 VolumeSource: v1.VolumeSource{
645 Secret: &v1.SecretVolumeSource{
646 SecretName: secretName,
647 DefaultMode: &defaultMode,
648 },
649 },
650 }
651 }
652
653 func secret(namespace, name string) v1.Secret {
654 return v1.Secret{
655 ObjectMeta: metav1.ObjectMeta{
656 Namespace: namespace,
657 Name: name,
658 },
659 Data: map[string][]byte{
660 "data-1": []byte("value-1"),
661 "data-2": []byte("value-2"),
662 "data-3": []byte("value-3"),
663 },
664 }
665 }
666
667 func doTestSecretDataInVolume(volumePath string, secret v1.Secret, t *testing.T) {
668 for key, value := range secret.Data {
669 secretDataHostPath := filepath.Join(volumePath, key)
670 if _, err := os.Stat(secretDataHostPath); err != nil {
671 t.Fatalf("SetUp() failed, couldn't find secret data on disk: %v", secretDataHostPath)
672 } else {
673 actualSecretBytes, err := ioutil.ReadFile(secretDataHostPath)
674 if err != nil {
675 t.Fatalf("Couldn't read secret data from: %v", secretDataHostPath)
676 }
677
678 actualSecretValue := string(actualSecretBytes)
679 if string(value) != actualSecretValue {
680 t.Errorf("Unexpected value; expected %q, got %q", value, actualSecretValue)
681 }
682 }
683 }
684 }
685
686 func doTestCleanAndTeardown(plugin volume.VolumePlugin, podUID types.UID, testVolumeName, volumePath string, t *testing.T) {
687 unmounter, err := plugin.NewUnmounter(testVolumeName, podUID)
688 if err != nil {
689 t.Errorf("Failed to make a new Unmounter: %v", err)
690 }
691 if unmounter == nil {
692 t.Fatalf("Got a nil Unmounter")
693 }
694
695 if err := unmounter.TearDown(); err != nil {
696 t.Errorf("Expected success, got: %v", err)
697 }
698 if _, err := os.Stat(volumePath); err == nil {
699 t.Errorf("TearDown() failed, volume path still exists: %s", volumePath)
700 } else if !os.IsNotExist(err) {
701 t.Errorf("TearDown() failed: %v", err)
702 }
703 }
704
705 func hasPathSuffix(s, suffix string) bool {
706 return strings.HasSuffix(s, filepath.FromSlash(suffix))
707 }
708
View as plain text