mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 13:37:30 +00:00
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:
commit
74b857cb0e
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user