Skip to content

Commit 805b6cb

Browse files
authored
Merge pull request kubernetes#76976 from zhan849/aws-get-zone
AWS EBS provisioner should get node zone info from k8s
2 parents 0adbd69 + 97b221d commit 805b6cb

File tree

3 files changed

+183
-51
lines changed

3 files changed

+183
-51
lines changed

staging/src/k8s.io/legacy-cloud-providers/aws/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ go_test(
8484
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
8585
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
8686
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
87+
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
8788
"//staging/src/k8s.io/cloud-provider/volume:go_default_library",
8889
"//vendor/github.com/aws/aws-sdk-go/aws:go_default_library",
8990
"//vendor/github.com/aws/aws-sdk-go/service/ec2:go_default_library",

staging/src/k8s.io/legacy-cloud-providers/aws/aws.go

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import (
5757
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
5858
"k8s.io/client-go/pkg/version"
5959
"k8s.io/client-go/tools/record"
60-
cloudprovider "k8s.io/cloud-provider"
60+
"k8s.io/cloud-provider"
6161
nodehelpers "k8s.io/cloud-provider/node/helpers"
6262
servicehelpers "k8s.io/cloud-provider/service/helpers"
6363
cloudvolume "k8s.io/cloud-provider/volume"
@@ -205,6 +205,16 @@ const volumeAttachmentStuck = "VolumeAttachmentStuck"
205205
// Indicates that a node has volumes stuck in attaching state and hence it is not fit for scheduling more pods
206206
const nodeWithImpairedVolumes = "NodeWithImpairedVolumes"
207207

208+
const (
209+
// These constants help to identify if a node is a master or a minion
210+
labelKeyNodeRole = "kubernetes.io/role"
211+
nodeMasterRole = "master"
212+
nodeMinionRole = "node"
213+
labelKeyNodeMaster = "node-role.kubernetes.io/master"
214+
labelKeyNodeCompute = "node-role.kubernetes.io/compute"
215+
labelKeyNodeMinion = "node-role.kubernetes.io/node"
216+
)
217+
208218
const (
209219
// volumeAttachmentConsecutiveErrorLimit is the number of consecutive errors we will ignore when waiting for a volume to attach/detach
210220
volumeAttachmentStatusConsecutiveErrorLimit = 10
@@ -1598,69 +1608,77 @@ func (c *Cloud) InstanceType(ctx context.Context, nodeName types.NodeName) (stri
15981608
// GetCandidateZonesForDynamicVolume retrieves a list of all the zones in which nodes are running
15991609
// It currently involves querying all instances
16001610
func (c *Cloud) GetCandidateZonesForDynamicVolume() (sets.String, error) {
1601-
// We don't currently cache this; it is currently used only in volume
1602-
// creation which is expected to be a comparatively rare occurrence.
1603-
1604-
// TODO: Caching / expose v1.Nodes to the cloud provider?
1605-
// TODO: We could also query for subnets, I think
1606-
1607-
// Note: It is more efficient to call the EC2 API twice with different tag
1608-
// filters than to call it once with a tag filter that results in a logical
1609-
// OR. For really large clusters the logical OR will result in EC2 API rate
1610-
// limiting.
1611-
instances := []*ec2.Instance{}
1612-
1613-
baseFilters := []*ec2.Filter{newEc2Filter("instance-state-name", "running")}
1611+
zones := sets.NewString()
16141612

1615-
filters := c.tagging.addFilters(baseFilters)
1616-
di, err := c.describeInstances(filters)
1613+
// TODO: list from cache?
1614+
nodes, err := c.kubeClient.CoreV1().Nodes().List(metav1.ListOptions{})
16171615
if err != nil {
1616+
klog.Errorf("Failed to get nodes from api server: %#v", err)
16181617
return nil, err
16191618
}
16201619

1621-
instances = append(instances, di...)
1622-
1623-
if c.tagging.usesLegacyTags {
1624-
filters = c.tagging.addLegacyFilters(baseFilters)
1625-
di, err = c.describeInstances(filters)
1626-
if err != nil {
1627-
return nil, err
1620+
for _, n := range nodes.Items {
1621+
if !c.isNodeReady(&n) {
1622+
klog.V(4).Infof("Ignoring not ready node %q in zone discovery", n.Name)
1623+
continue
16281624
}
1629-
1630-
instances = append(instances, di...)
1631-
}
1632-
1633-
if len(instances) == 0 {
1634-
return nil, fmt.Errorf("no instances returned")
1635-
}
1636-
1637-
zones := sets.NewString()
1638-
1639-
for _, instance := range instances {
1640-
// We skip over master nodes, if the installation tool labels them with one of the well-known master labels
1641-
// This avoids creating a volume in a zone where only the master is running - e.g. #34583
1642-
// This is a short-term workaround until the scheduler takes care of zone selection
1643-
master := false
1644-
for _, tag := range instance.Tags {
1645-
tagKey := aws.StringValue(tag.Key)
1646-
if awsTagNameMasterRoles.Has(tagKey) {
1647-
master = true
1625+
// In some cluster provisioning software, a node can be both a minion and a master. Therefore we white-list
1626+
// here, and only filter out node that is not minion AND is labeled as master explicitly
1627+
if c.isMinionNode(&n) || !c.isMasterNode(&n) {
1628+
if zone, ok := n.Labels[v1.LabelZoneFailureDomain]; ok {
1629+
zones.Insert(zone)
1630+
} else {
1631+
klog.Warningf("Node %s does not have zone label, ignore for zone discovery.", n.Name)
16481632
}
1633+
} else {
1634+
klog.V(4).Infof("Ignoring master node %q in zone discovery", n.Name)
16491635
}
1636+
}
16501637

1651-
if master {
1652-
klog.V(4).Infof("Ignoring master instance %q in zone discovery", aws.StringValue(instance.InstanceId))
1653-
continue
1654-
}
1638+
klog.V(2).Infof("Found instances in zones %s", zones)
1639+
return zones, nil
1640+
}
16551641

1656-
if instance.Placement != nil {
1657-
zone := aws.StringValue(instance.Placement.AvailabilityZone)
1658-
zones.Insert(zone)
1642+
// isNodeReady checks node condition and return true if NodeReady is marked as true
1643+
func (c *Cloud) isNodeReady(node *v1.Node) bool {
1644+
for _, c := range node.Status.Conditions {
1645+
if c.Type == v1.NodeReady {
1646+
return c.Status == v1.ConditionTrue
16591647
}
16601648
}
1649+
return false
1650+
}
1651+
1652+
// isMasterNode checks if the node is labeled as master
1653+
func (c *Cloud) isMasterNode(node *v1.Node) bool {
1654+
// Master node has one or more of the following labels:
1655+
//
1656+
// kubernetes.io/role: master
1657+
// node-role.kubernetes.io/master: ""
1658+
// node-role.kubernetes.io/master: "true"
1659+
if val, ok := node.Labels[labelKeyNodeMaster]; ok && val != "false" {
1660+
return true
1661+
} else if role, ok := node.Labels[labelKeyNodeRole]; ok && role == nodeMasterRole {
1662+
return true
1663+
}
1664+
return false
1665+
}
16611666

1662-
klog.V(2).Infof("Found instances in zones %s", zones)
1663-
return zones, nil
1667+
// isMinionNode checks if the node is labeled as minion
1668+
func (c *Cloud) isMinionNode(node *v1.Node) bool {
1669+
// Minion node has one or more oof the following labels:
1670+
//
1671+
// kubernetes.io/role: "node"
1672+
// node-role.kubernetes.io/compute: "true"
1673+
// node-role.kubernetes.io/node: ""
1674+
if val, ok := node.Labels[labelKeyNodeMinion]; ok && val != "false" {
1675+
return true
1676+
} else if val, ok := node.Labels[labelKeyNodeCompute]; ok && val != "false" {
1677+
return true
1678+
} else if role, ok := node.Labels[labelKeyNodeRole]; ok && role == nodeMinionRole {
1679+
return true
1680+
}
1681+
return false
16641682
}
16651683

16661684
// GetZone implements Zones.GetZone

staging/src/k8s.io/legacy-cloud-providers/aws/aws_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3737
"k8s.io/apimachinery/pkg/types"
3838
"k8s.io/apimachinery/pkg/util/sets"
39+
"k8s.io/client-go/kubernetes/fake"
3940
cloudvolume "k8s.io/cloud-provider/volume"
4041
)
4142

@@ -1834,6 +1835,118 @@ func TestCreateDisk(t *testing.T) {
18341835
awsServices.ec2.(*MockedFakeEC2).AssertExpectations(t)
18351836
}
18361837

1838+
func TestGetCandidateZonesForDynamicVolume(t *testing.T) {
1839+
tests := []struct {
1840+
name string
1841+
labels map[string]string
1842+
ready bool
1843+
expectedZones sets.String
1844+
}{
1845+
{
1846+
name: "master node with role label",
1847+
labels: map[string]string{labelKeyNodeRole: nodeMasterRole, v1.LabelZoneFailureDomain: "us-east-1a"},
1848+
ready: true,
1849+
expectedZones: sets.NewString(),
1850+
},
1851+
{
1852+
name: "master node with master label empty",
1853+
labels: map[string]string{labelKeyNodeMaster: "", v1.LabelZoneFailureDomain: "us-east-1a"},
1854+
ready: true,
1855+
expectedZones: sets.NewString(),
1856+
},
1857+
{
1858+
name: "master node with master label true",
1859+
labels: map[string]string{labelKeyNodeMaster: "true", v1.LabelZoneFailureDomain: "us-east-1a"},
1860+
ready: true,
1861+
expectedZones: sets.NewString(),
1862+
},
1863+
{
1864+
name: "master node with master label false",
1865+
labels: map[string]string{labelKeyNodeMaster: "false", v1.LabelZoneFailureDomain: "us-east-1a"},
1866+
ready: true,
1867+
expectedZones: sets.NewString("us-east-1a"),
1868+
},
1869+
{
1870+
name: "minion node with role label",
1871+
labels: map[string]string{labelKeyNodeRole: nodeMinionRole, v1.LabelZoneFailureDomain: "us-east-1a"},
1872+
ready: true,
1873+
expectedZones: sets.NewString("us-east-1a"),
1874+
},
1875+
{
1876+
name: "minion node with minion label",
1877+
labels: map[string]string{labelKeyNodeMinion: "", v1.LabelZoneFailureDomain: "us-east-1a"},
1878+
ready: true,
1879+
expectedZones: sets.NewString("us-east-1a"),
1880+
},
1881+
{
1882+
name: "minion node with compute label",
1883+
labels: map[string]string{labelKeyNodeCompute: "true", v1.LabelZoneFailureDomain: "us-east-1a"},
1884+
ready: true,
1885+
expectedZones: sets.NewString("us-east-1a"),
1886+
},
1887+
{
1888+
name: "master and minion node",
1889+
labels: map[string]string{labelKeyNodeMaster: "true", labelKeyNodeCompute: "true", v1.LabelZoneFailureDomain: "us-east-1a"},
1890+
ready: true,
1891+
expectedZones: sets.NewString("us-east-1a"),
1892+
},
1893+
{
1894+
name: "node not ready",
1895+
labels: map[string]string{v1.LabelZoneFailureDomain: "us-east-1a"},
1896+
ready: false,
1897+
expectedZones: sets.NewString(),
1898+
},
1899+
{
1900+
name: "node has no zone",
1901+
labels: map[string]string{},
1902+
ready: true,
1903+
expectedZones: sets.NewString(),
1904+
},
1905+
{
1906+
name: "node with no label",
1907+
labels: map[string]string{v1.LabelZoneFailureDomain: "us-east-1a"},
1908+
ready: true,
1909+
expectedZones: sets.NewString("us-east-1a"),
1910+
},
1911+
}
1912+
1913+
for i, test := range tests {
1914+
t.Run(test.name, func(t *testing.T) {
1915+
awsServices := newMockedFakeAWSServices(TestClusterID)
1916+
c, _ := newAWSCloud(CloudConfig{}, awsServices)
1917+
c.kubeClient = fake.NewSimpleClientset()
1918+
nodeName := fmt.Sprintf("node-%d", i)
1919+
_, err := c.kubeClient.CoreV1().Nodes().Create(&v1.Node{
1920+
ObjectMeta: metav1.ObjectMeta{
1921+
Name: nodeName,
1922+
Labels: test.labels,
1923+
},
1924+
Status: genNodeStatus(test.ready),
1925+
})
1926+
assert.Nil(t, err)
1927+
zones, err := c.GetCandidateZonesForDynamicVolume()
1928+
assert.Nil(t, err)
1929+
assert.Equal(t, test.expectedZones, zones)
1930+
})
1931+
}
1932+
}
1933+
1934+
func genNodeStatus(ready bool) v1.NodeStatus {
1935+
status := v1.ConditionFalse
1936+
if ready {
1937+
status = v1.ConditionTrue
1938+
}
1939+
ret := v1.NodeStatus{
1940+
Conditions: []v1.NodeCondition{
1941+
{
1942+
Type: v1.NodeReady,
1943+
Status: status,
1944+
},
1945+
},
1946+
}
1947+
return ret
1948+
}
1949+
18371950
func newMockedFakeAWSServices(id string) *FakeAWSServices {
18381951
s := NewFakeAWSServices(id)
18391952
s.ec2 = &MockedFakeEC2{FakeEC2Impl: s.ec2.(*FakeEC2Impl)}

0 commit comments

Comments
 (0)