/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package volume import ( "fmt" "hash/fnv" "reflect" "strings" "testing" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/util/slice" ) type testcase struct { // Input of the test name string existingPod *v1.Pod createPod *v1.Pod // eventSequence is list of events that are simulated during recycling. It // can be either event generated by a recycler pod or a state change of // the pod. (see newPodEvent and newEvent below). eventSequence []watch.Event // Expected output. // expectedEvents is list of events that were sent to the volume that was // recycled. expectedEvents []mockEvent expectedError string } func newPodEvent(eventtype watch.EventType, name string, phase v1.PodPhase, message string) watch.Event { return watch.Event{ Type: eventtype, Object: newPod(name, phase, message), } } func newEvent(eventtype, message string) watch.Event { return watch.Event{ Type: watch.Added, Object: &v1.Event{ ObjectMeta: metav1.ObjectMeta{ Namespace: metav1.NamespaceDefault, }, Reason: "MockEvent", Message: message, Type: eventtype, }, } } func newPod(name string, phase v1.PodPhase, message string) *v1.Pod { return &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: metav1.NamespaceDefault, Name: name, }, Status: v1.PodStatus{ Phase: phase, Message: message, }, } } func TestRecyclerPod(t *testing.T) { tests := []testcase{ { // Test recycler success with some events name: "RecyclerSuccess", createPod: newPod("podRecyclerSuccess", v1.PodPending, ""), eventSequence: []watch.Event{ // Pod gets Running and Succeeded newPodEvent(watch.Added, "podRecyclerSuccess", v1.PodPending, ""), newEvent(v1.EventTypeNormal, "Successfully assigned recycler-for-podRecyclerSuccess to 127.0.0.1"), newEvent(v1.EventTypeNormal, "pulling image \"gcr.io/google_containers/busybox\""), newEvent(v1.EventTypeNormal, "Successfully pulled image \"gcr.io/google_containers/busybox\""), newEvent(v1.EventTypeNormal, "Created container with docker id 83d929aeac82"), newEvent(v1.EventTypeNormal, "Started container with docker id 83d929aeac82"), newPodEvent(watch.Modified, "podRecyclerSuccess", v1.PodRunning, ""), newPodEvent(watch.Modified, "podRecyclerSuccess", v1.PodSucceeded, ""), }, expectedEvents: []mockEvent{ {v1.EventTypeNormal, "Successfully assigned recycler-for-podRecyclerSuccess to 127.0.0.1"}, {v1.EventTypeNormal, "pulling image \"gcr.io/google_containers/busybox\""}, {v1.EventTypeNormal, "Successfully pulled image \"gcr.io/google_containers/busybox\""}, {v1.EventTypeNormal, "Created container with docker id 83d929aeac82"}, {v1.EventTypeNormal, "Started container with docker id 83d929aeac82"}, }, expectedError: "", }, { // Test recycler failure with some events name: "RecyclerFailure", createPod: newPod("podRecyclerFailure", v1.PodPending, ""), eventSequence: []watch.Event{ // Pod gets Running and Succeeded newPodEvent(watch.Added, "podRecyclerFailure", v1.PodPending, ""), newEvent(v1.EventTypeNormal, "Successfully assigned recycler-for-podRecyclerFailure to 127.0.0.1"), newEvent(v1.EventTypeWarning, "Unable to mount volumes for pod \"recycler-for-podRecyclerFailure_default(3c9809e5-347c-11e6-a79b-3c970e965218)\": timeout expired waiting for volumes to attach/mount"), newEvent(v1.EventTypeWarning, "Error syncing pod, skipping: timeout expired waiting for volumes to attach/mount for pod \"default\"/\"recycler-for-podRecyclerFailure\". list of unattached/unmounted"), newPodEvent(watch.Modified, "podRecyclerFailure", v1.PodRunning, ""), newPodEvent(watch.Modified, "podRecyclerFailure", v1.PodFailed, "Pod was active on the node longer than specified deadline"), }, expectedEvents: []mockEvent{ {v1.EventTypeNormal, "Successfully assigned recycler-for-podRecyclerFailure to 127.0.0.1"}, {v1.EventTypeWarning, "Unable to mount volumes for pod \"recycler-for-podRecyclerFailure_default(3c9809e5-347c-11e6-a79b-3c970e965218)\": timeout expired waiting for volumes to attach/mount"}, {v1.EventTypeWarning, "Error syncing pod, skipping: timeout expired waiting for volumes to attach/mount for pod \"default\"/\"recycler-for-podRecyclerFailure\". list of unattached/unmounted"}, }, expectedError: "failed to recycle volume: Pod was active on the node longer than specified deadline", }, { // Recycler pod gets deleted name: "RecyclerDeleted", createPod: newPod("podRecyclerDeleted", v1.PodPending, ""), eventSequence: []watch.Event{ // Pod gets Running and Succeeded newPodEvent(watch.Added, "podRecyclerDeleted", v1.PodPending, ""), newEvent(v1.EventTypeNormal, "Successfully assigned recycler-for-podRecyclerDeleted to 127.0.0.1"), newPodEvent(watch.Deleted, "podRecyclerDeleted", v1.PodPending, ""), }, expectedEvents: []mockEvent{ {v1.EventTypeNormal, "Successfully assigned recycler-for-podRecyclerDeleted to 127.0.0.1"}, }, expectedError: "failed to recycle volume: recycler pod was deleted", }, { // Another recycler pod is already running name: "RecyclerRunning", existingPod: newPod("podOldRecycler", v1.PodRunning, ""), createPod: newPod("podNewRecycler", v1.PodFailed, "mock message"), eventSequence: []watch.Event{}, expectedError: "old recycler pod found, will retry later", }, } for _, test := range tests { t.Logf("Test %q", test.name) client := &mockRecyclerClient{ events: test.eventSequence, pod: test.existingPod, } err := internalRecycleVolumeByWatchingPodUntilCompletion(test.createPod.Name, test.createPod, client) receivedError := "" if err != nil { receivedError = err.Error() } if receivedError != test.expectedError { t.Errorf("Test %q failed, expected error %q, got %q", test.name, test.expectedError, receivedError) continue } if !client.deletedCalled { t.Errorf("Test %q failed, expected deferred client.Delete to be called on recycler pod", test.name) continue } for i, expectedEvent := range test.expectedEvents { if len(client.receivedEvents) <= i { t.Errorf("Test %q failed, expected event %d: %q not received", test.name, i, expectedEvent.message) continue } receivedEvent := client.receivedEvents[i] if expectedEvent.eventtype != receivedEvent.eventtype { t.Errorf("Test %q failed, event %d does not match: expected eventtype %q, got %q", test.name, i, expectedEvent.eventtype, receivedEvent.eventtype) } if expectedEvent.message != receivedEvent.message { t.Errorf("Test %q failed, event %d does not match: expected message %q, got %q", test.name, i, expectedEvent.message, receivedEvent.message) } } for i := len(test.expectedEvents); i < len(client.receivedEvents); i++ { t.Errorf("Test %q failed, unexpected event received: %s, %q", test.name, client.receivedEvents[i].eventtype, client.receivedEvents[i].message) } } } type mockRecyclerClient struct { pod *v1.Pod deletedCalled bool receivedEvents []mockEvent events []watch.Event } type mockEvent struct { eventtype, message string } func (c *mockRecyclerClient) CreatePod(pod *v1.Pod) (*v1.Pod, error) { if c.pod == nil { c.pod = pod return c.pod, nil } // Simulate "already exists" error return nil, errors.NewAlreadyExists(api.Resource("pods"), pod.Name) } func (c *mockRecyclerClient) GetPod(name, namespace string) (*v1.Pod, error) { if c.pod != nil { return c.pod, nil } else { return nil, fmt.Errorf("pod does not exist") } } func (c *mockRecyclerClient) DeletePod(name, namespace string) error { c.deletedCalled = true return nil } func (c *mockRecyclerClient) WatchPod(name, namespace string, stopChannel chan struct{}) (<-chan watch.Event, error) { eventCh := make(chan watch.Event, 0) go func() { for _, e := range c.events { eventCh <- e } }() return eventCh, nil } func (c *mockRecyclerClient) Event(eventtype, message string) { c.receivedEvents = append(c.receivedEvents, mockEvent{eventtype, message}) } func TestCalculateTimeoutForVolume(t *testing.T) { pv := &v1.PersistentVolume{ Spec: v1.PersistentVolumeSpec{ Capacity: v1.ResourceList{ v1.ResourceName(v1.ResourceStorage): resource.MustParse("500M"), }, }, } timeout := CalculateTimeoutForVolume(50, 30, pv) if timeout != 50 { t.Errorf("Expected 50 for timeout but got %v", timeout) } pv.Spec.Capacity[v1.ResourceStorage] = resource.MustParse("2Gi") timeout = CalculateTimeoutForVolume(50, 30, pv) if timeout != 60 { t.Errorf("Expected 60 for timeout but got %v", timeout) } pv.Spec.Capacity[v1.ResourceStorage] = resource.MustParse("150Gi") timeout = CalculateTimeoutForVolume(50, 30, pv) if timeout != 4500 { t.Errorf("Expected 4500 for timeout but got %v", timeout) } } func TestGenerateVolumeName(t *testing.T) { // Normal operation, no truncate v1 := GenerateVolumeName("kubernetes", "pv-cinder-abcde", 255) if v1 != "kubernetes-dynamic-pv-cinder-abcde" { t.Errorf("Expected kubernetes-dynamic-pv-cinder-abcde, got %s", v1) } // Truncate trailing "6789-dynamic" prefix := strings.Repeat("0123456789", 9) // 90 characters prefix + 8 chars. of "-dynamic" v2 := GenerateVolumeName(prefix, "pv-cinder-abcde", 100) expect := prefix[:84] + "-pv-cinder-abcde" if v2 != expect { t.Errorf("Expected %s, got %s", expect, v2) } // Truncate really long cluster name prefix = strings.Repeat("0123456789", 1000) // 10000 characters prefix v3 := GenerateVolumeName(prefix, "pv-cinder-abcde", 100) if v3 != expect { t.Errorf("Expected %s, got %s", expect, v3) } } func TestMountOptionFromSpec(t *testing.T) { scenarios := map[string]struct { volume *Spec expectedMountList []string systemOptions []string }{ "volume-with-mount-options": { volume: createVolumeSpecWithMountOption("good-mount-opts", "ro,nfsvers=3", v1.PersistentVolumeSpec{ PersistentVolumeSource: v1.PersistentVolumeSource{ NFS: &v1.NFSVolumeSource{Server: "localhost", Path: "/srv", ReadOnly: false}, }, }), expectedMountList: []string{"ro", "nfsvers=3"}, systemOptions: nil, }, "volume-with-bad-mount-options": { volume: createVolumeSpecWithMountOption("good-mount-opts", "", v1.PersistentVolumeSpec{ PersistentVolumeSource: v1.PersistentVolumeSource{ NFS: &v1.NFSVolumeSource{Server: "localhost", Path: "/srv", ReadOnly: false}, }, }), expectedMountList: []string{}, systemOptions: nil, }, "vol-with-sys-opts": { volume: createVolumeSpecWithMountOption("good-mount-opts", "ro,nfsvers=3", v1.PersistentVolumeSpec{ PersistentVolumeSource: v1.PersistentVolumeSource{ NFS: &v1.NFSVolumeSource{Server: "localhost", Path: "/srv", ReadOnly: false}, }, }), expectedMountList: []string{"ro", "nfsvers=3", "fsid=100", "hard"}, systemOptions: []string{"fsid=100", "hard"}, }, "vol-with-sys-opts-with-dup": { volume: createVolumeSpecWithMountOption("good-mount-opts", "ro,nfsvers=3", v1.PersistentVolumeSpec{ PersistentVolumeSource: v1.PersistentVolumeSource{ NFS: &v1.NFSVolumeSource{Server: "localhost", Path: "/srv", ReadOnly: false}, }, }), expectedMountList: []string{"ro", "nfsvers=3", "fsid=100"}, systemOptions: []string{"fsid=100", "ro"}, }, } for name, scenario := range scenarios { mountOptions := MountOptionFromSpec(scenario.volume, scenario.systemOptions...) if !reflect.DeepEqual(slice.SortStrings(mountOptions), slice.SortStrings(scenario.expectedMountList)) { t.Errorf("for %s expected mount options : %v got %v", name, scenario.expectedMountList, mountOptions) } } } func createVolumeSpecWithMountOption(name string, mountOptions string, spec v1.PersistentVolumeSpec) *Spec { annotations := map[string]string{ v1.MountOptionAnnotation: mountOptions, } objMeta := metav1.ObjectMeta{ Name: name, Annotations: annotations, } pv := &v1.PersistentVolume{ ObjectMeta: objMeta, Spec: spec, } return &Spec{PersistentVolume: pv} } func checkFnv32(t *testing.T, s string, expected int) { h := fnv.New32() h.Write([]byte(s)) h.Sum32() if int(h.Sum32()) != expected { t.Fatalf("hash of %q was %v, expected %v", s, h.Sum32(), expected) } } func TestChooseZoneForVolume(t *testing.T) { checkFnv32(t, "henley", 1180403676) // 1180403676 mod 3 == 0, so the offset from "henley" is 0, which makes it easier to verify this by inspection // A few others checkFnv32(t, "henley-", 2652299129) checkFnv32(t, "henley-a", 1459735322) checkFnv32(t, "", 2166136261) tests := []struct { Zones sets.String VolumeName string Expected string }{ // Test for PVC names that don't have a dash { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley", Expected: "a", // hash("henley") == 0 }, // Tests for PVC names that end in - number, but don't look like statefulset PVCs { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-0", Expected: "a", // hash("henley") == 0 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-1", Expected: "b", // hash("henley") + 1 == 1 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-2", Expected: "c", // hash("henley") + 2 == 2 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-3", Expected: "a", // hash("henley") + 3 == 3 === 0 mod 3 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-4", Expected: "b", // hash("henley") + 4 == 4 === 1 mod 3 }, // Tests for PVC names that are edge cases { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-", Expected: "c", // hash("henley-") = 2652299129 === 2 mod 3 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-a", Expected: "c", // hash("henley-a") = 1459735322 === 2 mod 3 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium--1", Expected: "c", // hash("") + 1 == 2166136261 + 1 === 2 mod 3 }, // Tests for PVC names for simple StatefulSet cases { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-1", Expected: "b", // hash("henley") + 1 == 1 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "loud-henley-1", Expected: "b", // hash("henley") + 1 == 1 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "quiet-henley-2", Expected: "c", // hash("henley") + 2 == 2 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-2", Expected: "c", // hash("henley") + 2 == 2 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-3", Expected: "a", // hash("henley") + 3 == 3 === 0 mod 3 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-4", Expected: "b", // hash("henley") + 4 == 4 === 1 mod 3 }, // Tests for statefulsets (or claims) with dashes in the names { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-alpha-henley-2", Expected: "c", // hash("henley") + 2 == 2 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-beta-henley-3", Expected: "a", // hash("henley") + 3 == 3 === 0 mod 3 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-gamma-henley-4", Expected: "b", // hash("henley") + 4 == 4 === 1 mod 3 }, // Tests for statefulsets name ending in - { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--2", Expected: "a", // hash("") + 2 == 0 mod 3 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--3", Expected: "b", // hash("") + 3 == 1 mod 3 }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--4", Expected: "c", // hash("") + 4 == 2 mod 3 }, } for _, test := range tests { actual := ChooseZoneForVolume(test.Zones, test.VolumeName) if actual != test.Expected { t.Errorf("Test %v failed, expected zone %q, actual %q", test, test.Expected, actual) } } } func TestChooseZonesForVolume(t *testing.T) { checkFnv32(t, "henley", 1180403676) // 1180403676 mod 3 == 0, so the offset from "henley" is 0, which makes it easier to verify this by inspection // A few others checkFnv32(t, "henley-", 2652299129) checkFnv32(t, "henley-a", 1459735322) checkFnv32(t, "", 2166136261) tests := []struct { Zones sets.String VolumeName string NumZones uint32 Expected sets.String }{ // Test for PVC names that don't have a dash { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley", NumZones: 1, Expected: sets.NewString("a" /* hash("henley") == 0 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley", NumZones: 2, Expected: sets.NewString("a" /* hash("henley") == 0 */, "b"), }, // Tests for PVC names that end in - number, but don't look like statefulset PVCs { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-0", NumZones: 1, Expected: sets.NewString("a" /* hash("henley") == 0 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-0", NumZones: 2, Expected: sets.NewString("a" /* hash("henley") == 0 */, "b"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-1", NumZones: 1, Expected: sets.NewString("b" /* hash("henley") + 1 == 1 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-1", NumZones: 2, Expected: sets.NewString("c" /* hash("henley") + 1 + 1(startingIndex) == 2 */, "a"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-2", NumZones: 1, Expected: sets.NewString("c" /* hash("henley") + 2 == 2 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-2", NumZones: 2, Expected: sets.NewString("b" /* hash("henley") + 2 + 2(startingIndex) == 4 */, "c"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-3", NumZones: 1, Expected: sets.NewString("a" /* hash("henley") + 3 == 3 === 0 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-3", NumZones: 2, Expected: sets.NewString("a" /* hash("henley") + 3 + 3(startingIndex) == 6 */, "b"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-4", NumZones: 1, Expected: sets.NewString("b" /* hash("henley") + 4 == 4 === 1 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-4", NumZones: 2, Expected: sets.NewString("c" /* hash("henley") + 4 + 4(startingIndex) == 8 */, "a"), }, // Tests for PVC names that are edge cases { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-", NumZones: 1, Expected: sets.NewString("c" /* hash("henley-") = 2652299129 === 2 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-", NumZones: 2, Expected: sets.NewString("c" /* hash("henley-") = 2652299129 === 2 mod 3 = 2 */, "a"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-a", NumZones: 1, Expected: sets.NewString("c" /* hash("henley-a") = 1459735322 === 2 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "henley-a", NumZones: 2, Expected: sets.NewString("c" /* hash("henley-a") = 1459735322 === 2 mod 3 = 2 */, "a"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium--1", NumZones: 1, Expected: sets.NewString("c" /* hash("") + 1 == 2166136261 + 1 === 2 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium--1", NumZones: 2, Expected: sets.NewString("a" /* hash("") + 1 + 1(startingIndex) == 2166136261 + 1 + 1 === 3 mod 3 = 0 */, "b"), }, // Tests for PVC names for simple StatefulSet cases { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-1", NumZones: 1, Expected: sets.NewString("b" /* hash("henley") + 1 == 1 */), }, // Tests for PVC names for simple StatefulSet cases { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-1", NumZones: 2, Expected: sets.NewString("c" /* hash("henley") + 1 + 1(startingIndex) == 2 */, "a"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "loud-henley-1", NumZones: 1, Expected: sets.NewString("b" /* hash("henley") + 1 == 1 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "loud-henley-1", NumZones: 2, Expected: sets.NewString("c" /* hash("henley") + 1 + 1(startingIndex) == 2 */, "a"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "quiet-henley-2", NumZones: 1, Expected: sets.NewString("c" /* hash("henley") + 2 == 2 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "quiet-henley-2", NumZones: 2, Expected: sets.NewString("b" /* hash("henley") + 2 + 2(startingIndex) == 4 */, "c"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-2", NumZones: 1, Expected: sets.NewString("c" /* hash("henley") + 2 == 2 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-2", NumZones: 2, Expected: sets.NewString("b" /* hash("henley") + 2 + 2(startingIndex) == 4 */, "c"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-3", NumZones: 1, Expected: sets.NewString("a" /* hash("henley") + 3 == 3 === 0 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-3", NumZones: 2, Expected: sets.NewString("a" /* hash("henley") + 3 + 3(startingIndex) == 6 === 6 mod 3 = 0 */, "b"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-4", NumZones: 1, Expected: sets.NewString("b" /* hash("henley") + 4 == 4 === 1 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley-4", NumZones: 2, Expected: sets.NewString("c" /* hash("henley") + 4 + 4(startingIndex) == 8 === 2 mod 3 */, "a"), }, // Tests for statefulsets (or claims) with dashes in the names { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-alpha-henley-2", NumZones: 1, Expected: sets.NewString("c" /* hash("henley") + 2 == 2 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-alpha-henley-2", NumZones: 2, Expected: sets.NewString("b" /* hash("henley") + 2 + 2(startingIndex) == 4 */, "c"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-beta-henley-3", NumZones: 1, Expected: sets.NewString("a" /* hash("henley") + 3 == 3 === 0 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-beta-henley-3", NumZones: 2, Expected: sets.NewString("a" /* hash("henley") + 3 + 3(startingIndex) == 6 === 0 mod 3 */, "b"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-gamma-henley-4", NumZones: 1, Expected: sets.NewString("b" /* hash("henley") + 4 == 4 === 1 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-gamma-henley-4", NumZones: 2, Expected: sets.NewString("c" /* hash("henley") + 4 + 4(startingIndex) == 8 === 2 mod 3 */, "a"), }, // Tests for statefulsets name ending in - { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--2", NumZones: 1, Expected: sets.NewString("a" /* hash("") + 2 == 0 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--2", NumZones: 2, Expected: sets.NewString("c" /* hash("") + 2 + 2(startingIndex) == 2 mod 3 */, "a"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--3", NumZones: 1, Expected: sets.NewString("b" /* hash("") + 3 == 1 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--3", NumZones: 2, Expected: sets.NewString("b" /* hash("") + 3 + 3(startingIndex) == 1 mod 3 */, "c"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--4", NumZones: 1, Expected: sets.NewString("c" /* hash("") + 4 == 2 mod 3 */), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--4", NumZones: 2, Expected: sets.NewString("a" /* hash("") + 4 + 4(startingIndex) == 0 mod 3 */, "b"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--4", NumZones: 3, Expected: sets.NewString("c" /* hash("") + 4 == 2 mod 3 */, "a", "b"), }, { Zones: sets.NewString("a", "b", "c"), VolumeName: "medium-henley--4", NumZones: 4, Expected: sets.NewString("c" /* hash("") + 4 + 9(startingIndex) == 2 mod 3 */, "a", "b", "c"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-0", NumZones: 2, Expected: sets.NewString("a" /* hash("henley") == 0 */, "b"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-1", NumZones: 2, Expected: sets.NewString("c" /* hash("henley") == 0 + 2 */, "d"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-2", NumZones: 2, Expected: sets.NewString("e" /* hash("henley") == 0 + 2 + 2(startingIndex) */, "f"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-3", NumZones: 2, Expected: sets.NewString("g" /* hash("henley") == 0 + 2 + 4(startingIndex) */, "h"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-0", NumZones: 3, Expected: sets.NewString("a" /* hash("henley") == 0 */, "b", "c"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-1", NumZones: 3, Expected: sets.NewString("d" /* hash("henley") == 0 + 1 + 2(startingIndex) */, "e", "f"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-2", NumZones: 3, Expected: sets.NewString("g" /* hash("henley") == 0 + 2 + 4(startingIndex) */, "h", "i"), }, { Zones: sets.NewString("a", "b", "c", "d", "e", "f", "g", "h", "i"), VolumeName: "henley-3", NumZones: 3, Expected: sets.NewString("a" /* hash("henley") == 0 + 3 + 6(startingIndex) */, "b", "c"), }, } for _, test := range tests { actual := ChooseZonesForVolume(test.Zones, test.VolumeName, test.NumZones) if !actual.Equal(test.Expected) { t.Errorf("Test %v failed, expected zone %#v, actual %#v", test, test.Expected, actual) } } } func TestValidateZone(t *testing.T) { functionUnderTest := "ValidateZone" // First part: want an error errCases := []string{"", " "} for _, errCase := range errCases { if got := ValidateZone(errCase); got == nil { t.Errorf("%v(%v) returned (%v), want (%v)", functionUnderTest, errCase, got, "an error") } } // Second part: want no error succCases := []string{" us-east-1a "} for _, succCase := range succCases { if got := ValidateZone(succCase); got != nil { t.Errorf("%v(%v) returned (%v), want (%v)", functionUnderTest, succCase, got, nil) } } } func TestGetWindowsPath(t *testing.T) { tests := []struct { path string expectedPath string }{ { path: `/var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4/volumes/kubernetes.io~disk`, expectedPath: `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~disk`, }, { path: `\var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~disk`, expectedPath: `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~disk`, }, { path: `/`, expectedPath: `c:\`, }, { path: ``, expectedPath: ``, }, } for _, test := range tests { result := GetWindowsPath(test.path) if result != test.expectedPath { t.Errorf("GetWindowsPath(%v) returned (%v), want (%v)", test.path, result, test.expectedPath) } } }