Add DynamicProvisioningScheduling support for GCE PD and RePD

Signed-off-by: Deep Debroy <ddebroy@docker.com>
pull/8/head
Deep Debroy 2018-08-24 14:38:25 -07:00
parent 99c17d89c7
commit a2de7d2d8d
5 changed files with 1063 additions and 125 deletions

View File

@ -32,6 +32,7 @@ import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
gcecloud "k8s.io/kubernetes/pkg/cloudprovider/providers/gce"
"k8s.io/kubernetes/pkg/features"
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
"k8s.io/kubernetes/pkg/util/mount"
kstrings "k8s.io/kubernetes/pkg/util/strings"
"k8s.io/kubernetes/pkg/volume"
@ -303,7 +304,7 @@ func (plugin *gcePersistentDiskPlugin) ConstructVolumeSpec(volumeName, mountPath
// Abstract interface to PD operations.
type pdManager interface {
// Creates a volume
CreateVolume(provisioner *gcePersistentDiskProvisioner) (volumeID string, volumeSizeGB int, labels map[string]string, fstype string, err error)
CreateVolume(provisioner *gcePersistentDiskProvisioner, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (volumeID string, volumeSizeGB int, labels map[string]string, fstype string, err error)
// Deletes a volume
DeleteVolume(deleter *gcePersistentDiskDeleter) error
}
@ -471,7 +472,7 @@ func (c *gcePersistentDiskProvisioner) Provision(selectedNode *v1.Node, allowedT
return nil, fmt.Errorf("invalid AccessModes %v: only AccessModes %v are supported", c.options.PVC.Spec.AccessModes, c.plugin.GetAccessModes())
}
volumeID, sizeGB, labels, fstype, err := c.manager.CreateVolume(c)
volumeID, sizeGB, labels, fstype, err := c.manager.CreateVolume(c, selectedNode, allowedTopologies)
if err != nil {
return nil, err
}
@ -519,14 +520,32 @@ func (c *gcePersistentDiskProvisioner) Provision(selectedNode *v1.Node, allowedT
pv.Spec.AccessModes = c.plugin.GetAccessModes()
}
requirements := make([]v1.NodeSelectorRequirement, 0)
if len(labels) != 0 {
if pv.Labels == nil {
pv.Labels = make(map[string]string)
}
for k, v := range labels {
pv.Labels[k] = v
var values []string
if k == kubeletapis.LabelZoneFailureDomain {
values, err = util.LabelZonesToList(v)
if err != nil {
return nil, fmt.Errorf("failed to convert label string for Zone: %s to a List: %v", v, err)
}
} else {
values = []string{v}
}
requirements = append(requirements, v1.NodeSelectorRequirement{Key: k, Operator: v1.NodeSelectorOpIn, Values: values})
}
}
if utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) && len(requirements) > 0 {
pv.Spec.NodeAffinity = new(v1.VolumeNodeAffinity)
pv.Spec.NodeAffinity.Required = new(v1.NodeSelector)
pv.Spec.NodeAffinity.Required.NodeSelectorTerms = make([]v1.NodeSelectorTerm, 1)
pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions = requirements
}
return pv, nil
}

View File

@ -20,6 +20,8 @@ import (
"fmt"
"os"
"path"
"reflect"
"sort"
"testing"
"k8s.io/api/core/v1"
@ -27,10 +29,12 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake"
utiltesting "k8s.io/client-go/util/testing"
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
"k8s.io/kubernetes/pkg/util/mount"
"k8s.io/kubernetes/pkg/volume"
volumetest "k8s.io/kubernetes/pkg/volume/testing"
"k8s.io/kubernetes/pkg/volume/util"
volumeutil "k8s.io/kubernetes/pkg/volume/util"
)
func TestCanSupport(t *testing.T) {
@ -78,9 +82,10 @@ func TestGetAccessModes(t *testing.T) {
type fakePDManager struct {
}
func (fake *fakePDManager) CreateVolume(c *gcePersistentDiskProvisioner) (volumeID string, volumeSizeGB int, labels map[string]string, fstype string, err error) {
func (fake *fakePDManager) CreateVolume(c *gcePersistentDiskProvisioner, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (volumeID string, volumeSizeGB int, labels map[string]string, fstype string, err error) {
labels = make(map[string]string)
labels["fakepdmanager"] = "yes"
labels[kubeletapis.LabelZoneFailureDomain] = "zone1__zone2"
return "test-gce-volume-name", 100, labels, "", nil
}
@ -91,6 +96,15 @@ func (fake *fakePDManager) DeleteVolume(cd *gcePersistentDiskDeleter) error {
return nil
}
func getNodeSelectorRequirementWithKey(key string, term v1.NodeSelectorTerm) (*v1.NodeSelectorRequirement, error) {
for _, r := range term.MatchExpressions {
if r.Key == key {
return &r, nil
}
}
return nil, fmt.Errorf("key %s not found", key)
}
func TestPlugin(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("gcepdTest")
if err != nil {
@ -182,7 +196,35 @@ func TestPlugin(t *testing.T) {
}
if persistentSpec.Labels["fakepdmanager"] != "yes" {
t.Errorf("Provision() returned unexpected labels: %v", persistentSpec.Labels)
t.Errorf("Provision() returned unexpected value for fakepdmanager: %v", persistentSpec.Labels["fakepdmanager"])
}
if persistentSpec.Labels[kubeletapis.LabelZoneFailureDomain] != "zone1__zone2" {
t.Errorf("Provision() returned unexpected value for %s: %v", kubeletapis.LabelZoneFailureDomain, persistentSpec.Labels[kubeletapis.LabelZoneFailureDomain])
}
if persistentSpec.Spec.NodeAffinity == nil {
t.Errorf("Unexpected nil NodeAffinity found")
}
if len(persistentSpec.Spec.NodeAffinity.Required.NodeSelectorTerms) != 1 {
t.Errorf("Unexpected number of NodeSelectorTerms")
}
term := persistentSpec.Spec.NodeAffinity.Required.NodeSelectorTerms[0]
if len(term.MatchExpressions) != 2 {
t.Errorf("Unexpected number of NodeSelectorRequirements in volume NodeAffinity: %d", len(term.MatchExpressions))
}
r, _ := getNodeSelectorRequirementWithKey("fakepdmanager", term)
if r == nil || r.Values[0] != "yes" || r.Operator != v1.NodeSelectorOpIn {
t.Errorf("NodeSelectorRequirement fakepdmanager-in-yes not found in volume NodeAffinity")
}
zones, _ := volumeutil.ZonesToSet("zone1,zone2")
r, _ = getNodeSelectorRequirementWithKey(kubeletapis.LabelZoneFailureDomain, term)
if r == nil {
t.Errorf("NodeSelectorRequirement %s-in-%v not found in volume NodeAffinity", kubeletapis.LabelZoneFailureDomain, zones)
}
sort.Strings(r.Values)
if !reflect.DeepEqual(r.Values, zones.List()) {
t.Errorf("ZoneFailureDomain elements %v does not match zone labels %v", r.Values, zones)
}
// Test Deleter

View File

@ -87,7 +87,7 @@ func (util *GCEDiskUtil) DeleteVolume(d *gcePersistentDiskDeleter) error {
// CreateVolume creates a GCE PD.
// Returns: gcePDName, volumeSizeGB, labels, fsType, error
func (gceutil *GCEDiskUtil) CreateVolume(c *gcePersistentDiskProvisioner) (string, int, map[string]string, string, error) {
func (gceutil *GCEDiskUtil) CreateVolume(c *gcePersistentDiskProvisioner, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (string, int, map[string]string, string, error) {
cloud, err := getCloudProvider(c.gcePersistentDisk.plugin.host.GetCloudProvider())
if err != nil {
return "", 0, nil, "", err
@ -104,7 +104,7 @@ func (gceutil *GCEDiskUtil) CreateVolume(c *gcePersistentDiskProvisioner) (strin
// to the cloud provider.
diskType := ""
configuredZone := ""
configuredZones := ""
var configuredZones sets.String
zonePresent := false
zonesPresent := false
replicationType := replicationTypeNone
@ -118,7 +118,10 @@ func (gceutil *GCEDiskUtil) CreateVolume(c *gcePersistentDiskProvisioner) (strin
configuredZone = v
case "zones":
zonesPresent = true
configuredZones = v
configuredZones, err = volumeutil.ZonesToSet(v)
if err != nil {
return "", 0, nil, "", err
}
case "replication-type":
if !utilfeature.DefaultFeatureGate.Enabled(features.GCERegionalPersistentDisk) {
return "", 0, nil, "",
@ -133,76 +136,49 @@ func (gceutil *GCEDiskUtil) CreateVolume(c *gcePersistentDiskProvisioner) (strin
}
}
if zonePresent && zonesPresent {
return "", 0, nil, "", fmt.Errorf("the 'zone' and 'zones' StorageClass parameters must not be used at the same time")
}
if replicationType == replicationTypeRegionalPD && zonePresent {
// If a user accidentally types 'zone' instead of 'zones', we want to throw an error
// instead of assuming that 'zones' is empty and proceed by randomly selecting zones.
return "", 0, nil, "", fmt.Errorf("the '%s' replication type does not support the 'zone' parameter; use 'zones' instead", replicationTypeRegionalPD)
}
// TODO: implement PVC.Selector parsing
if c.options.PVC.Spec.Selector != nil {
return "", 0, nil, "", fmt.Errorf("claim.Spec.Selector is not supported for dynamic provisioning on GCE")
}
var activezones sets.String
activezones, err = cloud.GetAllCurrentZones()
if err != nil {
return "", 0, nil, "", err
}
switch replicationType {
case replicationTypeRegionalPD:
err = createRegionalPD(
name,
c.options.PVC.Name,
diskType,
configuredZones,
requestGB,
c.options.CloudTags,
cloud)
selectedZones, err := volumeutil.SelectZonesForVolume(zonePresent, zonesPresent, configuredZone, configuredZones, activezones, node, allowedTopologies, c.options.PVC.Name, maxRegionalPDZones)
if err != nil {
glog.V(2).Infof("Error selecting zones for regional GCE PD volume: %v", err)
return "", 0, nil, "", err
}
if err = cloud.CreateRegionalDisk(
name,
diskType,
selectedZones,
int64(requestGB),
*c.options.CloudTags); err != nil {
glog.V(2).Infof("Error creating regional GCE PD volume: %v", err)
return "", 0, nil, "", err
}
glog.V(2).Infof("Successfully created Regional GCE PD volume %s", name)
case replicationTypeNone:
var zones sets.String
if !zonePresent && !zonesPresent {
// 00 - neither "zone" or "zones" specified
// Pick a zone randomly selected from all active zones where
// Kubernetes cluster has a node.
zones, err = cloud.GetAllCurrentZones()
if err != nil {
glog.V(2).Infof("error getting zone information from GCE: %v", err)
return "", 0, nil, "", err
}
} else if !zonePresent && zonesPresent {
// 01 - "zones" specified
// Pick a zone randomly selected from specified set.
if zones, err = volumeutil.ZonesToSet(configuredZones); err != nil {
return "", 0, nil, "", err
}
} else if zonePresent && !zonesPresent {
// 10 - "zone" specified
// Use specified zone
if err := volumeutil.ValidateZone(configuredZone); err != nil {
return "", 0, nil, "", err
}
zones = make(sets.String)
zones.Insert(configuredZone)
selectedZone, err := volumeutil.SelectZoneForVolume(zonePresent, zonesPresent, configuredZone, configuredZones, activezones, node, allowedTopologies, c.options.PVC.Name)
if err != nil {
return "", 0, nil, "", err
}
zone := volumeutil.ChooseZoneForVolume(zones, c.options.PVC.Name)
if err := cloud.CreateDisk(
name,
diskType,
zone,
selectedZone,
int64(requestGB),
*c.options.CloudTags); err != nil {
glog.V(2).Infof("Error creating single-zone GCE PD volume: %v", err)
return "", 0, nil, "", err
}
glog.V(2).Infof("Successfully created single-zone GCE PD volume %s", name)
default:
@ -218,57 +194,6 @@ func (gceutil *GCEDiskUtil) CreateVolume(c *gcePersistentDiskProvisioner) (strin
return name, int(requestGB), labels, fstype, nil
}
// Creates a Regional PD
func createRegionalPD(
diskName string,
pvcName string,
diskType string,
zonesString string,
requestGB int64,
cloudTags *map[string]string,
cloud *gcecloud.GCECloud) error {
var replicaZones sets.String
var err error
if zonesString == "" {
// Consider all zones
replicaZones, err = cloud.GetAllCurrentZones()
if err != nil {
glog.V(2).Infof("error getting zone information from GCE: %v", err)
return err
}
} else {
replicaZones, err = volumeutil.ZonesToSet(zonesString)
if err != nil {
return err
}
}
zoneCount := replicaZones.Len()
var selectedReplicaZones sets.String
if zoneCount < maxRegionalPDZones {
return fmt.Errorf("cannot specify only %d zone(s) for Regional PDs.", zoneCount)
} else if zoneCount == maxRegionalPDZones {
selectedReplicaZones = replicaZones
} else {
// Must randomly select zones
selectedReplicaZones = volumeutil.ChooseZonesForVolume(
replicaZones, pvcName, maxRegionalPDZones)
}
if err = cloud.CreateRegionalDisk(
diskName,
diskType,
selectedReplicaZones,
int64(requestGB),
*cloudTags); err != nil {
return err
}
return nil
}
// Returns the first path that exists, or empty string if none exist.
func verifyDevicePath(devicePaths []string, sdBeforeSet sets.String, diskName string) (string, error) {
if err := udevadmChangeToNewDrives(sdBeforeSet); err != nil {

View File

@ -320,62 +320,102 @@ func LoadPodFromFile(filePath string) (*v1.Pod, error) {
return pod, nil
}
// SelectZone selects a zone for a volume based on several factors:
// node.zone, allowedTopologies, zone/zones parameters from storageclass
// and zones with active nodes from the cluster
// SelectZoneForVolume is a wrapper around SelectZonesForVolume
// to select a single zone for a volume based on parameters
func SelectZoneForVolume(zoneParameterPresent, zonesParameterPresent bool, zoneParameter string, zonesParameter, zonesWithNodes sets.String, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm, pvcName string) (string, error) {
zones, err := SelectZonesForVolume(zoneParameterPresent, zonesParameterPresent, zoneParameter, zonesParameter, zonesWithNodes, node, allowedTopologies, pvcName, 1)
if err != nil {
return "", err
}
zone, ok := zones.PopAny()
if !ok {
return "", fmt.Errorf("could not determine a zone to provision volume in")
}
return zone, nil
}
// SelectZonesForVolume selects zones for a volume based on several factors:
// node.zone, allowedTopologies, zone/zones parameters from storageclass,
// zones with active nodes from the cluster. The number of zones = replicas.
func SelectZonesForVolume(zoneParameterPresent, zonesParameterPresent bool, zoneParameter string, zonesParameter, zonesWithNodes sets.String, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm, pvcName string, numReplicas uint32) (sets.String, error) {
if zoneParameterPresent && zonesParameterPresent {
return "", fmt.Errorf("both zone and zones StorageClass parameters must not be used at the same time")
return nil, fmt.Errorf("both zone and zones StorageClass parameters must not be used at the same time")
}
// pick zone from node if present
var zoneFromNode string
// pick one zone from node if present
if node != nil {
// DynamicProvisioningScheduling implicit since node is not nil
if zoneParameterPresent || zonesParameterPresent {
return "", fmt.Errorf("zone[s] cannot be specified in StorageClass if VolumeBindingMode is set to WaitForFirstConsumer. Please specify allowedTopologies in StorageClass for constraining zones.")
return nil, fmt.Errorf("zone[s] cannot be specified in StorageClass if VolumeBindingMode is set to WaitForFirstConsumer. Please specify allowedTopologies in StorageClass for constraining zones")
}
// pick node's zone and ignore any allowedTopologies (since scheduler is assumed to have accounted for it already)
z, ok := node.ObjectMeta.Labels[kubeletapis.LabelZoneFailureDomain]
// pick node's zone for one of the replicas
var ok bool
zoneFromNode, ok = node.ObjectMeta.Labels[kubeletapis.LabelZoneFailureDomain]
if !ok {
return "", fmt.Errorf("%s Label for node missing", kubeletapis.LabelZoneFailureDomain)
return nil, fmt.Errorf("%s Label for node missing", kubeletapis.LabelZoneFailureDomain)
}
// if single replica volume and node with zone found, return immediately
if numReplicas == 1 {
return sets.NewString(zoneFromNode), nil
}
return z, nil
}
// pick zone from allowedZones if specified
allowedZones, err := ZonesFromAllowedTopologies(allowedTopologies)
if err != nil {
return "", err
return nil, err
}
if (len(allowedTopologies) > 0) && (allowedZones.Len() == 0) {
return "", fmt.Errorf("no matchLabelExpressions with %s key found in allowedTopologies. Please specify matchLabelExpressions with %s key", kubeletapis.LabelZoneFailureDomain, kubeletapis.LabelZoneFailureDomain)
return nil, fmt.Errorf("no matchLabelExpressions with %s key found in allowedTopologies. Please specify matchLabelExpressions with %s key", kubeletapis.LabelZoneFailureDomain, kubeletapis.LabelZoneFailureDomain)
}
if allowedZones.Len() > 0 {
// DynamicProvisioningScheduling implicit since allowedZones present
if zoneParameterPresent || zonesParameterPresent {
return "", fmt.Errorf("zone[s] cannot be specified in StorageClass if allowedTopologies specified")
return nil, fmt.Errorf("zone[s] cannot be specified in StorageClass if allowedTopologies specified")
}
// scheduler will guarantee if node != null above, zoneFromNode is member of allowedZones.
// so if zoneFromNode != "", we can safely assume it is part of allowedZones.
if zones, err := chooseZonesForVolumeIncludingZone(allowedZones, pvcName, zoneFromNode, numReplicas); err != nil {
return nil, fmt.Errorf("cannot process zones in allowedTopologies: %v", err)
} else {
return zones, nil
}
return ChooseZoneForVolume(*allowedZones, pvcName), nil
}
// pick zone from parameters if present
if zoneParameterPresent {
return zoneParameter, nil
if numReplicas > 1 {
return nil, fmt.Errorf("zone cannot be specified if desired number of replicas for pv is greather than 1. Please specify zones or allowedTopologies to specify desired zones")
}
return sets.NewString(zoneParameter), nil
}
if zonesParameterPresent {
return ChooseZoneForVolume(zonesParameter, pvcName), nil
if uint32(zonesParameter.Len()) < numReplicas {
return nil, fmt.Errorf("not enough zones found in zones parameter to provision a volume with %d replicas. Found %d zones, need %d zones", numReplicas, zonesParameter.Len(), numReplicas)
}
// directly choose from zones parameter; no zone from node need to be considered
return ChooseZonesForVolume(zonesParameter, pvcName, numReplicas), nil
}
// pick zone from zones with nodes
return ChooseZoneForVolume(zonesWithNodes, pvcName), nil
if zonesWithNodes.Len() > 0 {
// If node != null (and thus zoneFromNode != ""), zoneFromNode will be member of zonesWithNodes
if zones, err := chooseZonesForVolumeIncludingZone(zonesWithNodes, pvcName, zoneFromNode, numReplicas); err != nil {
return nil, fmt.Errorf("cannot process zones where nodes exist in the cluster: %v", err)
} else {
return zones, nil
}
}
return nil, fmt.Errorf("cannot determine zones to provision volume in")
}
// ZonesFromAllowedTopologies returns a list of zones specified in allowedTopologies
func ZonesFromAllowedTopologies(allowedTopologies []v1.TopologySelectorTerm) (*sets.String, error) {
func ZonesFromAllowedTopologies(allowedTopologies []v1.TopologySelectorTerm) (sets.String, error) {
zones := make(sets.String)
for _, term := range allowedTopologies {
for _, exp := range term.MatchLabelExpressions {
@ -388,7 +428,7 @@ func ZonesFromAllowedTopologies(allowedTopologies []v1.TopologySelectorTerm) (*s
}
}
}
return &zones, nil
return zones, nil
}
func ZonesSetToLabelValue(strSet sets.String) string {
@ -397,7 +437,11 @@ func ZonesSetToLabelValue(strSet sets.String) string {
// ZonesToSet converts a string containing a comma separated list of zones to set
func ZonesToSet(zonesString string) (sets.String, error) {
return stringToSet(zonesString, ",")
zones, err := stringToSet(zonesString, ",")
if err != nil {
return nil, fmt.Errorf("error parsing zones %s, must be strings separated by commas: %v", zonesString, err)
}
return zones, nil
}
// LabelZonesToSet converts a PV label value from string containing a delimited list of zones to set
@ -422,6 +466,27 @@ func stringToSet(str, delimiter string) (sets.String, error) {
return zonesSet, nil
}
// LabelZonesToList converts a PV label value from string containing a delimited list of zones to list
func LabelZonesToList(labelZonesValue string) ([]string, error) {
return stringToList(labelZonesValue, kubeletapis.LabelMultiZoneDelimiter)
}
// StringToList converts a string containing list separated by specified delimiter to a list
func stringToList(str, delimiter string) ([]string, error) {
zonesSlice := make([]string, 0)
for _, zone := range strings.Split(str, delimiter) {
trimmedZone := strings.TrimSpace(zone)
if trimmedZone == "" {
return nil, fmt.Errorf(
"%q separated list (%q) must not contain an empty string",
delimiter,
str)
}
zonesSlice = append(zonesSlice, trimmedZone)
}
return zonesSlice, nil
}
// CalculateTimeoutForVolume calculates time for a Recycler pod to complete a
// recycle operation. The calculation and return value is either the
// minimumTimeout or the timeoutIncrement per Gi of storage size, whichever is
@ -539,6 +604,33 @@ func ChooseZoneForVolume(zones sets.String, pvcName string) string {
return zone
}
// chooseZonesForVolumeIncludingZone is a wrapper around ChooseZonesForVolume that ensures zoneToInclude is chosen
// zoneToInclude can either be empty in which case it is ignored. If non-empty, zoneToInclude is expected to be member of zones.
// numReplicas is expected to be > 0 and <= zones.Len()
func chooseZonesForVolumeIncludingZone(zones sets.String, pvcName, zoneToInclude string, numReplicas uint32) (sets.String, error) {
if numReplicas == 0 {
return nil, fmt.Errorf("invalid number of replicas passed")
}
if uint32(zones.Len()) < numReplicas {
return nil, fmt.Errorf("not enough zones found to provision a volume with %d replicas. Need at least %d distinct zones for a volume with %d replicas", numReplicas, numReplicas, numReplicas)
}
if zoneToInclude != "" && !zones.Has(zoneToInclude) {
return nil, fmt.Errorf("zone to be included: %s needs to be member of set: %v", zoneToInclude, zones)
}
if uint32(zones.Len()) == numReplicas {
return zones, nil
}
if zoneToInclude != "" {
zones.Delete(zoneToInclude)
numReplicas = numReplicas - 1
}
zonesChosen := ChooseZonesForVolume(zones, pvcName, numReplicas)
if zoneToInclude != "" {
zonesChosen.Insert(zoneToInclude)
}
return zonesChosen, nil
}
// ChooseZonesForVolume is identical to ChooseZoneForVolume, but selects a multiple zones, for multi-zone disks.
func ChooseZonesForVolume(zones sets.String, pvcName string, numZones uint32) sets.String {
// We create the volume in a zone determined by the name

View File

@ -1460,6 +1460,866 @@ func TestSelectZoneForVolume(t *testing.T) {
}
}
func TestSelectZonesForVolume(t *testing.T) {
nodeWithZoneLabels := &v1.Node{}
nodeWithZoneLabels.Labels = map[string]string{kubeletapis.LabelZoneFailureDomain: "zoneX"}
nodeWithNoLabels := &v1.Node{}
tests := []struct {
// Parameters passed by test to SelectZonesForVolume
Name string
ReplicaCount uint32
ZonePresent bool
Zone string
ZonesPresent bool
Zones string
ZonesWithNodes string
Node *v1.Node
AllowedTopologies []v1.TopologySelectorTerm
// Expectations around returned zones from SelectZonesForVolume
Reject bool // expect error due to validation failing
ExpectSpecificZones bool // expect set of returned zones to be equal to ExpectedZones (rather than subset of ExpectedZones)
ExpectedZones string // set of zones that is a superset of returned zones or equal to returned zones (if ExpectSpecificZones is set)
ExpectSpecificZone bool // expect set of returned zones to include ExpectedZone as a member
ExpectedZone string // zone that should be a member of returned zones
}{
// NEGATIVE TESTS
// Zone and Zones are both specified [Fail]
// [1] Node irrelevant
// [2] Zone and Zones parameters presents
// [3] AllowedTopologies irrelevant
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_with_Zone_Zones_parameters_present",
ZonePresent: true,
Zone: "zoneX",
ZonesPresent: true,
Zones: "zoneX,zoneY",
Reject: true,
},
// Node has no zone labels [Fail]
// [1] Node with no zone labels
// [2] Zone/Zones parameter irrelevant
// [3] AllowedTopologies irrelevant
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Node_with_no_Zone_labels",
Node: nodeWithNoLabels,
Reject: true,
},
// Node with Zone labels as well as Zone parameter specified [Fail]
// [1] Node with zone labels
// [2] Zone parameter specified
// [3] AllowedTopologies irrelevant
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Node_with_Zone_labels_and_Zone_parameter_present",
Node: nodeWithZoneLabels,
ZonePresent: true,
Zone: "zoneX",
Reject: true,
},
// Node with Zone labels as well as Zones parameter specified [Fail]
// [1] Node with zone labels
// [2] Zones parameter specified
// [3] AllowedTopologies irrelevant
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Node_with_Zone_labels_and_Zones_parameter_present",
Node: nodeWithZoneLabels,
ZonesPresent: true,
Zones: "zoneX,zoneY",
Reject: true,
},
// Zone parameter as well as AllowedTopologies specified [Fail]
// [1] nil Node
// [2] Zone parameter specified
// [3] AllowedTopologies specified
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_and_Zone_parameter_and_Allowed_Topology_term",
Node: nil,
ZonePresent: true,
Zone: "zoneX",
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX"},
},
},
},
},
Reject: true,
},
// Zones parameter as well as AllowedTopologies specified [Fail]
// [1] nil Node
// [2] Zones parameter specified
// [3] AllowedTopologies specified
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_and_Zones_parameter_and_Allowed_Topology_term",
Node: nil,
ZonesPresent: true,
Zones: "zoneX,zoneY",
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX"},
},
},
},
},
Reject: true,
},
// Key specified in AllowedTopologies is not LabelZoneFailureDomain [Fail]
// [1] nil Node
// [2] no Zone/Zones parameter
// [3] AllowedTopologies with invalid key specified
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_and_Invalid_Allowed_Topology_Key",
Node: nil,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: "invalid_key",
Values: []string{"zoneX"},
},
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneY"},
},
},
},
},
Reject: true,
},
// AllowedTopologies without keys specifying LabelZoneFailureDomain [Fail]
// [1] nil Node
// [2] no Zone/Zones parameter
// [3] Invalid AllowedTopologies
// [4] ReplicaCount irrelevant
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_and_Invalid_AllowedTopologies",
Node: nil,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{},
},
},
Reject: true,
},
// Zone specified with ReplicaCount > 1 [Fail]
// [1] nil Node
// [2] Zone/Zones parameter specified
// [3] AllowedTopologies irrelevant
// [4] ReplicaCount > 1
// [5] ZonesWithNodes irrelevant
{
Name: "Zone_and_Replicas_mismatched",
Node: nil,
ZonePresent: true,
Zone: "zoneX",
ReplicaCount: 2,
Reject: true,
},
// Not enough zones in Zones parameter to satisfy ReplicaCount [Fail]
// [1] nil Node
// [2] Zone/Zones parameter specified
// [3] AllowedTopologies irrelevant
// [4] ReplicaCount > zones
// [5] ZonesWithNodes irrelevant
{
Name: "Zones_and_Replicas_mismatched",
Node: nil,
ZonesPresent: true,
Zones: "zoneX",
ReplicaCount: 2,
Reject: true,
},
// Not enough zones in ZonesWithNodes to satisfy ReplicaCount [Fail]
// [1] nil Node
// [2] no Zone/Zones parameter specified
// [3] AllowedTopologies irrelevant
// [4] ReplicaCount > ZonesWithNodes
// [5] ZonesWithNodes specified but not enough
{
Name: "Zones_and_Replicas_mismatched",
Node: nil,
ZonesWithNodes: "zoneX",
ReplicaCount: 2,
Reject: true,
},
// Not enough zones in AllowedTopologies to satisfy ReplicaCount [Fail]
// [1] nil Node
// [2] Zone/Zones parameter specified
// [3] Invalid AllowedTopologies
// [4] ReplicaCount > zones
// [5] ZonesWithNodes irrelevant
{
Name: "AllowedTopologies_and_Replicas_mismatched",
Node: nil,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX"},
},
},
},
},
Reject: true,
},
// POSITIVE TESTS
// Select zones from zones parameter [Pass]
// [1] nil Node (Node irrelevant)
// [2] Zones parameter specified
// [3] no AllowedTopologies
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Zones_parameter_Dual_replica_Superset",
ZonesPresent: true,
Zones: "zoneW,zoneX,zoneY,zoneZ",
ReplicaCount: 2,
Reject: false,
ExpectedZones: "zoneW,zoneX,zoneY,zoneZ",
},
// Select zones from zones parameter [Pass]
// [1] nil Node (Node irrelevant)
// [2] Zones parameter specified
// [3] no AllowedTopologies
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Zones_parameter_Dual_replica_Match",
ZonesPresent: true,
Zones: "zoneX,zoneY",
ReplicaCount: 2,
Reject: false,
ExpectSpecificZones: true,
ExpectedZones: "zoneX,zoneY",
},
// Select zones from active zones with nodes [Pass]
// [1] nil Node (Node irrelevant)
// [2] no Zone parameter
// [3] no AllowedTopologies
// [4] ReplicaCount specified
// [5] ZonesWithNodes specified
{
Name: "Active_zones_Nil_node_Dual_replica_Superset",
ZonesWithNodes: "zoneW,zoneX,zoneY,zoneZ",
ReplicaCount: 2,
Reject: false,
ExpectedZones: "zoneW,zoneX,zoneY,zoneZ",
},
// Select zones from active zones [Pass]
// [1] nil Node (Node irrelevant)
// [2] no Zone[s] parameter
// [3] no AllowedTopologies
// [4] ReplicaCount specified
// [5] ZonesWithNodes specified
{
Name: "Active_zones_Nil_node_Dual_replica_Match",
ZonesWithNodes: "zoneW,zoneX",
ReplicaCount: 2,
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectSpecificZones: true,
ExpectedZones: "zoneW,zoneX",
},
// Select zones from node label and active zones [Pass]
// [1] Node with zone labels
// [2] no Zone parameter
// [3] no AllowedTopologies
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Active_zones_Node_with_Zone_labels_Dual_replica_Superset",
Node: nodeWithZoneLabels,
ZonesWithNodes: "zoneW,zoneX,zoneY,zoneZ",
ReplicaCount: 2,
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectedZones: "zoneW,zoneX,zoneY,zoneZ",
},
// Select zones from node label and active zones [Pass]
// [1] Node with zone labels
// [2] no Zone[s] parameter
// [3] no AllowedTopologies
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Active_zones_Node_with_Zone_labels_Dual_replica_Match",
Node: nodeWithZoneLabels,
ZonesWithNodes: "zoneW,zoneX",
ReplicaCount: 2,
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectSpecificZones: true,
ExpectedZones: "zoneW,zoneX",
},
// Select zones from node label and AllowedTopologies [Pass]
// [1] Node with zone labels
// [2] no Zone/Zones parameters
// [3] AllowedTopologies with single term with multiple values specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
// Note: the test Name suffix is used as the pvcname and it's suffix is important to influence ChooseZonesForVolume
// to NOT pick zoneX from AllowedTopologies if zoneFromNode is incorrectly set or not set at all.
{
Name: "Node_with_Zone_labels_and_Multiple_Allowed_Topology_values_Superset-1",
Node: nodeWithZoneLabels,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneV", "zoneW", "zoneX", "zoneY", "zoneZ"},
},
},
},
},
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectedZones: "zoneV,zoneW,zoneX,zoneY,zoneZ",
},
// Select zones from node label and AllowedTopologies [Pass]
// [1] Node with zone labels
// [2] no Zone/Zones parameters
// [3] AllowedTopologies with single term with multiple values specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Node_with_Zone_labels_and_Multiple_Allowed_Topology_values_Match",
Node: nodeWithZoneLabels,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX", "zoneY"},
},
},
},
},
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectSpecificZones: true,
ExpectedZones: "zoneX,zoneY",
},
// Select Zones from AllowedTopologies [Pass]
// [1] nil Node
// [2] no Zone/Zones parametes specified
// [3] AllowedTopologies with single term with multiple values specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_with_Multiple_Allowed_Topology_values_Superset",
Node: nil,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX", "zoneY", "zoneZ"},
},
},
},
},
Reject: false,
ExpectedZones: "zoneX,zoneY,zoneZ",
},
// Select Zones from AllowedTopologies [Pass]
// [1] nil Node
// [2] no Zone/Zones parametes specified
// [3] AllowedTopologies with single term with multiple values specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_with_Multiple_Allowed_Topology_values_Match",
Node: nil,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX", "zoneY"},
},
},
},
},
Reject: false,
ExpectSpecificZones: true,
ExpectedZones: "zoneX,zoneY",
},
// Select zones from node label and AllowedTopologies [Pass]
// [1] Node with zone labels
// [2] no Zone/Zones parametes specified
// [3] AllowedTopologies with multiple terms specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
// Note: the test Name is used as the pvcname and it's hash is important to influence ChooseZonesForVolume
// to NOT pick zoneX from AllowedTopologies of 5 zones. If zoneFromNode (zoneX) is incorrectly set or
// not set at all in SelectZonesForVolume it will be detected.
{
Name: "Node_with_Zone_labels_and_Multiple_Allowed_Topology_terms_Superset-2",
Node: nodeWithZoneLabels,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneV"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneW"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneY"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneZ"},
},
},
},
},
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectedZones: "zoneV,zoneW,zoneX,zoneY,zoneZ",
},
// Select zones from node label and AllowedTopologies [Pass]
// [1] Node with zone labels
// [2] no Zone/Zones parametes specified
// [3] AllowedTopologies with multiple terms specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Node_with_Zone_labels_and_Multiple_Allowed_Topology_terms_Match",
Node: nodeWithZoneLabels,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneY"},
},
},
},
},
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectSpecificZones: true,
ExpectedZones: "zoneX,zoneY",
},
// Select zones from AllowedTopologies [Pass]
// [1] nil Node
// [2] no Zone/Zones parametes specified
// [3] AllowedTopologies with multiple terms specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_and_Multiple_Allowed_Topology_terms_Superset",
Node: nil,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneY"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneZ"},
},
},
},
},
Reject: false,
ExpectedZones: "zoneX,zoneY,zoneZ",
},
// Select zones from AllowedTopologies [Pass]
// [1] nil Node
// [2] no Zone/Zones parametes specified
// [3] AllowedTopologies with multiple terms specified
// [4] ReplicaCount specified
// [5] ZonesWithNodes irrelevant
{
Name: "Nil_Node_and_Multiple_Allowed_Topology_terms_Match",
Node: nil,
ReplicaCount: 2,
AllowedTopologies: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneX"},
},
},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: kubeletapis.LabelZoneFailureDomain,
Values: []string{"zoneY"},
},
},
},
},
Reject: false,
ExpectSpecificZones: true,
ExpectedZones: "zoneX,zoneY",
},
}
for _, test := range tests {
var zonesParameter, zonesWithNodes sets.String
var err error
if test.Zones != "" {
zonesParameter, err = ZonesToSet(test.Zones)
if err != nil {
t.Errorf("Could not convert Zones to a set: %s. This is a test error %s", test.Zones, test.Name)
continue
}
}
if test.ZonesWithNodes != "" {
zonesWithNodes, err = ZonesToSet(test.ZonesWithNodes)
if err != nil {
t.Errorf("Could not convert specified ZonesWithNodes to a set: %s. This is a test error %s", test.ZonesWithNodes, test.Name)
continue
}
}
zones, err := SelectZonesForVolume(test.ZonePresent, test.ZonesPresent, test.Zone, zonesParameter, zonesWithNodes, test.Node, test.AllowedTopologies, test.Name, test.ReplicaCount)
if test.Reject && err == nil {
t.Errorf("Unexpected zones from SelectZonesForVolume in %s. Zones: %v", test.Name, zones)
continue
}
if !test.Reject {
if err != nil {
t.Errorf("Unexpected error from SelectZonesForVolume in %s; Error: %v", test.Name, err)
continue
}
if uint32(zones.Len()) != test.ReplicaCount {
t.Errorf("Number of elements in returned zones %v does not equal replica count %d in %s", zones, test.ReplicaCount, test.Name)
continue
}
expectedZones, err := ZonesToSet(test.ExpectedZones)
if err != nil {
t.Errorf("Could not convert ExpectedZones to a set: %v. This is a test error in %s", test.ExpectedZones, test.Name)
continue
}
if test.ExpectSpecificZones && !zones.Equal(expectedZones) {
t.Errorf("Expected zones %v does not match obtained zones %v in %s", expectedZones, zones, test.Name)
continue
}
if test.ExpectSpecificZone && !zones.Has(test.ExpectedZone) {
t.Errorf("Expected zone %s not found in obtained zones %v in %s", test.ExpectedZone, zones, test.Name)
continue
}
if !expectedZones.IsSuperset(zones) {
t.Errorf("Obtained zones %v not subset of of expectedZones %v in %s", zones, expectedZones, test.Name)
}
}
}
}
func TestChooseZonesForVolumeIncludingZone(t *testing.T) {
tests := []struct {
// Parameters passed by test to chooseZonesForVolumeIncludingZone
Name string
ReplicaCount uint32
Zones string
ZoneToInclude string
// Expectations around returned zones from chooseZonesForVolumeIncludingZone
Reject bool // expect error due to validation failing
ExpectSpecificZones bool // expect set of returned zones to be equal to ExpectedZones (rather than subset of ExpectedZones)
ExpectedZones string // set of zones that is a superset of returned zones or equal to returned zones (if ExpectSpecificZones is set)
ExpectSpecificZone bool // expect set of returned zones to include ExpectedZone as a member
ExpectedZone string // zone that should be a member of returned zones
}{
// NEGATIVE TESTS
// Not enough zones specified to fit ReplicaCount [Fail]
{
Name: "Too_Few_Zones",
ReplicaCount: 2,
Zones: "zoneX",
Reject: true,
},
// Invalid ReplicaCount [Fail]
{
Name: "Zero_Replica_Count",
ReplicaCount: 0,
Zones: "zoneY,zoneZ",
Reject: true,
},
// Invalid ZoneToInclude [Fail]
{
Name: "ZoneToInclude_Not_In_Zones_Single_replica_Dual_zones",
ReplicaCount: 1,
ZoneToInclude: "zoneX",
Zones: "zoneY,zoneZ",
Reject: true,
},
// Invalid ZoneToInclude [Fail]
{
Name: "ZoneToInclude_Not_In_Zones_Single_replica_Single_zone",
ReplicaCount: 1,
ZoneToInclude: "zoneX",
Zones: "zoneY",
Reject: true,
},
// Invalid ZoneToInclude [Fail]
{
Name: "ZoneToInclude_Not_In_Zones_Dual_replica_Multiple_zones",
ReplicaCount: 2,
ZoneToInclude: "zoneX",
Zones: "zoneY,zoneZ,zoneW",
Reject: true,
},
// POSITIVE TESTS
// Pick any one zone from Zones
{
Name: "No_zones_to_include_and_Single_replica_and_Superset",
ReplicaCount: 1,
Zones: "zoneX,zoneY,zoneZ",
Reject: false,
ExpectedZones: "zoneX,zoneY,zoneZ",
},
// Pick any two zones from Zones
{
Name: "No_zones_to_include_and_Dual_replicas_and_Superset",
ReplicaCount: 2,
Zones: "zoneW,zoneX,zoneY,zoneZ",
Reject: false,
ExpectedZones: "zoneW,zoneX,zoneY,zoneZ",
},
// Pick the two zones from Zones
{
Name: "No_zones_to_include_and_Dual_replicas_and_Match",
ReplicaCount: 2,
Zones: "zoneX,zoneY",
Reject: false,
ExpectSpecificZones: true,
ExpectedZones: "zoneX,zoneY",
},
// Pick one zone from ZoneToInclude (other zones ignored)
{
Name: "Include_zone_and_Single_replica_and_Match",
ReplicaCount: 1,
Zones: "zoneW,zoneX,zoneY,zoneZ",
ZoneToInclude: "zoneX",
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectSpecificZones: true,
ExpectedZones: "zoneX",
},
// Pick one zone from ZoneToInclude (other zones ignored)
{
Name: "Include_zone_and_single_replica_and_Match",
ReplicaCount: 1,
Zones: "zoneX",
ZoneToInclude: "zoneX",
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectSpecificZones: true,
ExpectedZones: "zoneX",
},
// Pick one zone from Zones and the other from ZoneToInclude
{
Name: "Include_zone_and_dual_replicas_and_Superset",
ReplicaCount: 2,
Zones: "zoneW,zoneX,zoneY,zoneZ",
ZoneToInclude: "zoneX",
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectedZones: "zoneW,zoneX,zoneY,zoneZ",
},
// Pick one zone from Zones and the other from ZoneToInclude
{
Name: "Include_zone_and_dual_replicas_and_Match",
ReplicaCount: 2,
Zones: "zoneX,zoneY",
ZoneToInclude: "zoneX",
Reject: false,
ExpectSpecificZone: true,
ExpectedZone: "zoneX",
ExpectSpecificZones: true,
ExpectedZones: "zoneX,zoneY",
},
}
for _, test := range tests {
zonesParameter, err := ZonesToSet(test.Zones)
if err != nil {
t.Errorf("Could not convert Zones to a set: %s. This is a test error %s", test.Zones, test.Name)
continue
}
zones, err := chooseZonesForVolumeIncludingZone(zonesParameter, test.Name, test.ZoneToInclude, test.ReplicaCount)
if test.Reject && err == nil {
t.Errorf("Unexpected zones from chooseZonesForVolumeIncludingZone in %s. Zones: %v", test.Name, zones)
continue
}
if !test.Reject {
if err != nil {
t.Errorf("Unexpected error from chooseZonesForVolumeIncludingZone in %s; Error: %v", test.Name, err)
continue
}
if uint32(zones.Len()) != test.ReplicaCount {
t.Errorf("Number of elements in returned zones %v does not equal replica count %d in %s", zones, test.ReplicaCount, test.Name)
continue
}
expectedZones, err := ZonesToSet(test.ExpectedZones)
if err != nil {
t.Errorf("Could not convert ExpectedZones to a set: %v. This is a test error in %s", test.ExpectedZones, test.Name)
continue
}
if test.ExpectSpecificZones && !zones.Equal(expectedZones) {
t.Errorf("Expected zones %v does not match obtained zones %v in %s", expectedZones, zones, test.Name)
continue
}
if test.ExpectSpecificZone && !zones.Has(test.ExpectedZone) {
t.Errorf("Expected zone %s not found in obtained zones %v in %s", test.ExpectedZone, zones, test.Name)
continue
}
if !expectedZones.IsSuperset(zones) {
t.Errorf("Obtained zones %v not subset of of expectedZones %v in %s", zones, expectedZones, test.Name)
}
}
}
}
func TestGetWindowsPath(t *testing.T) {
tests := []struct {
path string