Merge pull request #90943 from foobarfran/feature-aws-lb-node-targeting

feat: use annotation to filter AWS LB target nodes
This commit is contained in:
Kubernetes Prow Robot 2020-05-28 12:24:25 -07:00 committed by GitHub
commit 74b857cb0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 15 deletions

View File

@ -210,6 +210,12 @@ const ServiceAnnotationLoadBalancerHCInterval = "service.beta.kubernetes.io/aws-
// static IP addresses for the NLB. Only supported on elbv2 (NLB) // static IP addresses for the NLB. Only supported on elbv2 (NLB)
const ServiceAnnotationLoadBalancerEIPAllocations = "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" const ServiceAnnotationLoadBalancerEIPAllocations = "service.beta.kubernetes.io/aws-load-balancer-eip-allocations"
// ServiceAnnotationLoadBalancerTargetNodeLabels is the annotation used on the service
// to specify a comma-separated list of key-value pairs which will be used to select
// the target nodes for the load balancer
// For example: "Key1=Val1,Key2=Val2,KeyNoVal1=,KeyNoVal2"
const ServiceAnnotationLoadBalancerTargetNodeLabels = "service.beta.kubernetes.io/aws-load-balancer-target-node-labels"
// Event key when a volume is stuck on attaching state when being attached to a volume // Event key when a volume is stuck on attaching state when being attached to a volume
const volumeAttachmentStuck = "VolumeAttachmentStuck" const volumeAttachmentStuck = "VolumeAttachmentStuck"
@ -3568,7 +3574,7 @@ func (c *Cloud) buildELBSecurityGroupList(serviceName types.NamespacedName, load
// Create a security group for the load balancer // Create a security group for the load balancer
sgName := "k8s-elb-" + loadBalancerName sgName := "k8s-elb-" + loadBalancerName
sgDescription := fmt.Sprintf("Security group for Kubernetes ELB %s (%v)", loadBalancerName, serviceName) sgDescription := fmt.Sprintf("Security group for Kubernetes ELB %s (%v)", loadBalancerName, serviceName)
securityGroupID, err = c.ensureSecurityGroup(sgName, sgDescription, getLoadBalancerAdditionalTags(annotations)) securityGroupID, err = c.ensureSecurityGroup(sgName, sgDescription, getKeyValuePropertiesFromAnnotation(annotations, ServiceAnnotationLoadBalancerAdditionalTags))
if err != nil { if err != nil {
klog.Errorf("Error creating load balancer security group: %q", err) klog.Errorf("Error creating load balancer security group: %q", err)
return nil, setupSg, err return nil, setupSg, err
@ -3686,7 +3692,7 @@ func (c *Cloud) EnsureLoadBalancer(ctx context.Context, clusterName string, apiS
return nil, fmt.Errorf("LoadBalancerIP cannot be specified for AWS ELB") return nil, fmt.Errorf("LoadBalancerIP cannot be specified for AWS ELB")
} }
instances, err := c.findInstancesForELB(nodes) instances, err := c.findInstancesForELB(nodes, annotations)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -4470,7 +4476,7 @@ func (c *Cloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName strin
// UpdateLoadBalancer implements LoadBalancer.UpdateLoadBalancer // UpdateLoadBalancer implements LoadBalancer.UpdateLoadBalancer
func (c *Cloud) UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error { func (c *Cloud) UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error {
instances, err := c.findInstancesForELB(nodes) instances, err := c.findInstancesForELB(nodes, service.Annotations)
if err != nil { if err != nil {
return err return err
} }

View File

@ -84,12 +84,11 @@ type nlbPortMapping struct {
SSLPolicy string SSLPolicy string
} }
// getLoadBalancerAdditionalTags converts the comma separated list of key-value // getKeyValuePropertiesFromAnnotation converts the comma separated list of key-value
// pairs in the ServiceAnnotationLoadBalancerAdditionalTags annotation and returns // pairs from the specified annotation and returns it as a map.
// it as a map. func getKeyValuePropertiesFromAnnotation(annotations map[string]string, annotation string) map[string]string {
func getLoadBalancerAdditionalTags(annotations map[string]string) map[string]string {
additionalTags := make(map[string]string) additionalTags := make(map[string]string)
if additionalTagsList, ok := annotations[ServiceAnnotationLoadBalancerAdditionalTags]; ok { if additionalTagsList, ok := annotations[annotation]; ok {
additionalTagsList = strings.TrimSpace(additionalTagsList) additionalTagsList = strings.TrimSpace(additionalTagsList)
// Break up list of "Key1=Val,Key2=Val2" // Break up list of "Key1=Val,Key2=Val2"
@ -123,7 +122,7 @@ func (c *Cloud) ensureLoadBalancerv2(namespacedName types.NamespacedName, loadBa
dirty := false dirty := false
// Get additional tags set by the user // Get additional tags set by the user
tags := getLoadBalancerAdditionalTags(annotations) tags := getKeyValuePropertiesFromAnnotation(annotations, ServiceAnnotationLoadBalancerAdditionalTags)
// Add default tags // Add default tags
tags[TagNameKubernetesService] = namespacedName.String() tags[TagNameKubernetesService] = namespacedName.String()
tags = c.tagging.buildTags(ResourceLifecycleOwned, tags) tags = c.tagging.buildTags(ResourceLifecycleOwned, tags)
@ -939,7 +938,7 @@ func (c *Cloud) ensureLoadBalancer(namespacedName types.NamespacedName, loadBala
} }
// Get additional tags set by the user // Get additional tags set by the user
tags := getLoadBalancerAdditionalTags(annotations) tags := getKeyValuePropertiesFromAnnotation(annotations, ServiceAnnotationLoadBalancerAdditionalTags)
// Add default tags // Add default tags
tags[TagNameKubernetesService] = namespacedName.String() tags[TagNameKubernetesService] = namespacedName.String()
@ -1128,7 +1127,7 @@ func (c *Cloud) ensureLoadBalancer(namespacedName types.NamespacedName, loadBala
{ {
// Add additional tags // Add additional tags
klog.V(2).Infof("Creating additional load balancer tags for %s", loadBalancerName) klog.V(2).Infof("Creating additional load balancer tags for %s", loadBalancerName)
tags := getLoadBalancerAdditionalTags(annotations) tags := getKeyValuePropertiesFromAnnotation(annotations, ServiceAnnotationLoadBalancerAdditionalTags)
if len(tags) > 0 { if len(tags) > 0 {
err := c.addLoadBalancerTags(loadBalancerName, tags) err := c.addLoadBalancerTags(loadBalancerName, tags)
if err != nil { if err != nil {
@ -1521,9 +1520,12 @@ func proxyProtocolEnabled(backend *elb.BackendServerDescription) bool {
// findInstancesForELB gets the EC2 instances corresponding to the Nodes, for setting up an ELB // findInstancesForELB gets the EC2 instances corresponding to the Nodes, for setting up an ELB
// We ignore Nodes (with a log message) where the instanceid cannot be determined from the provider, // We ignore Nodes (with a log message) where the instanceid cannot be determined from the provider,
// and we ignore instances which are not found // and we ignore instances which are not found
func (c *Cloud) findInstancesForELB(nodes []*v1.Node) (map[InstanceID]*ec2.Instance, error) { func (c *Cloud) findInstancesForELB(nodes []*v1.Node, annotations map[string]string) (map[InstanceID]*ec2.Instance, error) {
targetNodes := filterTargetNodes(nodes, annotations)
// Map to instance ids ignoring Nodes where we cannot find the id (but logging) // Map to instance ids ignoring Nodes where we cannot find the id (but logging)
instanceIDs := mapToAWSInstanceIDsTolerant(nodes) instanceIDs := mapToAWSInstanceIDsTolerant(targetNodes)
cacheCriteria := cacheCriteria{ cacheCriteria := cacheCriteria{
// MaxAge not required, because we only care about security groups, which should not change // MaxAge not required, because we only care about security groups, which should not change
@ -1539,3 +1541,35 @@ func (c *Cloud) findInstancesForELB(nodes []*v1.Node) (map[InstanceID]*ec2.Insta
return instances, nil return instances, nil
} }
// filterTargetNodes uses node labels to filter the nodes that should be targeted by the ELB,
// checking if all the labels provided in an annotation are present in the nodes
func filterTargetNodes(nodes []*v1.Node, annotations map[string]string) []*v1.Node {
targetNodeLabels := getKeyValuePropertiesFromAnnotation(annotations, ServiceAnnotationLoadBalancerTargetNodeLabels)
if len(targetNodeLabels) == 0 {
return nodes
}
targetNodes := make([]*v1.Node, 0, len(nodes))
for _, node := range nodes {
if node.Labels != nil && len(node.Labels) > 0 {
allFiltersMatch := true
for targetLabelKey, targetLabelValue := range targetNodeLabels {
if nodeLabelValue, ok := node.Labels[targetLabelKey]; !ok || (nodeLabelValue != targetLabelValue && targetLabelValue != "") {
allFiltersMatch = false
break
}
}
if allFiltersMatch {
targetNodes = append(targetNodes, node)
}
}
}
return targetNodes
}

View File

@ -25,6 +25,8 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/elb"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
) )
func TestElbProtocolsAreEqual(t *testing.T) { func TestElbProtocolsAreEqual(t *testing.T) {
@ -420,3 +422,57 @@ func TestBuildTargetGroupName(t *testing.T) {
}) })
} }
} }
func TestFilterTargetNodes(t *testing.T) {
tests := []struct {
name string
nodeLabels, annotations map[string]string
nodeTargeted bool
}{
{
name: "when no filter is provided, node should be targeted",
nodeLabels: map[string]string{"k1": "v1"},
nodeTargeted: true,
},
{
name: "when all key-value filters match, node should be targeted",
nodeLabels: map[string]string{"k1": "v1", "k2": "v2"},
annotations: map[string]string{ServiceAnnotationLoadBalancerTargetNodeLabels: "k1=v1,k2=v2"},
nodeTargeted: true,
},
{
name: "when all just-key filter match, node should be targeted",
nodeLabels: map[string]string{"k1": "v1", "k2": "v2"},
annotations: map[string]string{ServiceAnnotationLoadBalancerTargetNodeLabels: "k1,k2"},
nodeTargeted: true,
},
{
name: "when some filters do not match, node should not be targeted",
nodeLabels: map[string]string{"k1": "v1"},
annotations: map[string]string{ServiceAnnotationLoadBalancerTargetNodeLabels: "k1=v1,k2"},
nodeTargeted: false,
},
{
name: "when no filter matches, node should not be targeted",
nodeLabels: map[string]string{"k1": "v1", "k2": "v2"},
annotations: map[string]string{ServiceAnnotationLoadBalancerTargetNodeLabels: "k3=v3"},
nodeTargeted: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
node := &v1.Node{}
node.Labels = test.nodeLabels
nodes := []*v1.Node{node}
targetNodes := filterTargetNodes(nodes, test.annotations)
if test.nodeTargeted {
assert.Equal(t, nodes, targetNodes)
} else {
assert.Empty(t, targetNodes)
}
})
}
}

View File

@ -1530,7 +1530,7 @@ func TestProxyProtocolEnabled(t *testing.T) {
assert.False(t, result, "did not expect to find %s in %s", ProxyProtocolPolicyName, policies) assert.False(t, result, "did not expect to find %s in %s", ProxyProtocolPolicyName, policies)
} }
func TestGetLoadBalancerAdditionalTags(t *testing.T) { func TestGetKeyValuePropertiesFromAnnotation(t *testing.T) {
tagTests := []struct { tagTests := []struct {
Annotations map[string]string Annotations map[string]string
Tags map[string]string Tags map[string]string
@ -1581,7 +1581,7 @@ func TestGetLoadBalancerAdditionalTags(t *testing.T) {
} }
for _, tagTest := range tagTests { for _, tagTest := range tagTests {
result := getLoadBalancerAdditionalTags(tagTest.Annotations) result := getKeyValuePropertiesFromAnnotation(tagTest.Annotations, ServiceAnnotationLoadBalancerAdditionalTags)
for k, v := range result { for k, v := range result {
if len(result) != len(tagTest.Tags) { if len(result) != len(tagTest.Tags) {
t.Errorf("incorrect expected length: %v != %v", result, tagTest.Tags) t.Errorf("incorrect expected length: %v != %v", result, tagTest.Tags)