Skip to content

Commit 4fd93ff

Browse files
authored
Merge pull request kubernetes#94114 from MarcPow/azure-provider-ip-tags
Azure Cloud Provider should support Service annotations that allow for ip-tag control over the public ips created for LoadBalancer Services
2 parents 6b388f0 + 56706d2 commit 4fd93ff

File tree

2 files changed

+771
-29
lines changed

2 files changed

+771
-29
lines changed

staging/src/k8s.io/legacy-cloud-providers/azure/azure_loadbalancer.go

Lines changed: 143 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"math"
2525
"reflect"
26+
"sort"
2627
"strconv"
2728
"strings"
2829

@@ -82,6 +83,9 @@ const (
8283
// ServiceAnnotationPIPName specifies the pip that will be applied to load balancer
8384
ServiceAnnotationPIPName = "service.beta.kubernetes.io/azure-pip-name"
8485

86+
// ServiceAnnotationIPTagsForPublicIP specifies the iptags used when dynamically creating a public ip
87+
ServiceAnnotationIPTagsForPublicIP = "service.beta.kubernetes.io/azure-pip-ip-tags"
88+
8589
// ServiceAnnotationAllowedServiceTag is the annotation used on the service
8690
// to specify a list of allowed service tags separated by comma
8791
// Refer https://docs.microsoft.com/en-us/azure/virtual-network/security-overview#service-tags for all supported service tags.
@@ -546,6 +550,7 @@ func (az *Cloud) ensurePublicIPExists(service *v1.Service, pipName string, domai
546550
pip.Location = to.StringPtr(az.Location)
547551
pip.PublicIPAddressPropertiesFormat = &network.PublicIPAddressPropertiesFormat{
548552
PublicIPAllocationMethod: network.Static,
553+
IPTags: getServiceIPTagRequestForPublicIP(service).IPTags,
549554
}
550555
pip.Tags = map[string]*string{
551556
serviceTagKey: &serviceName,
@@ -604,6 +609,93 @@ func (az *Cloud) ensurePublicIPExists(service *v1.Service, pipName string, domai
604609
return &pip, nil
605610
}
606611

612+
type serviceIPTagRequest struct {
613+
IPTagsRequestedByAnnotation bool
614+
IPTags *[]network.IPTag
615+
}
616+
617+
// Get the ip tag Request for the public ip from service annotations.
618+
func getServiceIPTagRequestForPublicIP(service *v1.Service) serviceIPTagRequest {
619+
if service != nil {
620+
if ipTagString, found := service.Annotations[ServiceAnnotationIPTagsForPublicIP]; found {
621+
return serviceIPTagRequest{
622+
IPTagsRequestedByAnnotation: true,
623+
IPTags: convertIPTagMapToSlice(getIPTagMap(ipTagString)),
624+
}
625+
}
626+
}
627+
628+
return serviceIPTagRequest{
629+
IPTagsRequestedByAnnotation: false,
630+
IPTags: nil,
631+
}
632+
}
633+
634+
func getIPTagMap(ipTagString string) map[string]string {
635+
outputMap := make(map[string]string)
636+
commaDelimitedPairs := strings.Split(strings.TrimSpace(ipTagString), ",")
637+
for _, commaDelimitedPair := range commaDelimitedPairs {
638+
splitKeyValue := strings.Split(commaDelimitedPair, "=")
639+
640+
// Include only valid pairs in the return value
641+
// Last Write wins.
642+
if len(splitKeyValue) == 2 {
643+
tagKey := strings.TrimSpace(splitKeyValue[0])
644+
tagValue := strings.TrimSpace(splitKeyValue[1])
645+
646+
outputMap[tagKey] = tagValue
647+
}
648+
}
649+
650+
return outputMap
651+
}
652+
653+
func sortIPTags(ipTags *[]network.IPTag) {
654+
if ipTags != nil {
655+
sort.Slice(*ipTags, func(i, j int) bool {
656+
ipTag := *ipTags
657+
return to.String(ipTag[i].IPTagType) < to.String(ipTag[j].IPTagType) ||
658+
to.String(ipTag[i].Tag) < to.String(ipTag[j].Tag)
659+
})
660+
}
661+
}
662+
663+
func areIPTagsEquivalent(ipTags1 *[]network.IPTag, ipTags2 *[]network.IPTag) bool {
664+
sortIPTags(ipTags1)
665+
sortIPTags(ipTags2)
666+
667+
if ipTags1 == nil {
668+
ipTags1 = &[]network.IPTag{}
669+
}
670+
671+
if ipTags2 == nil {
672+
ipTags2 = &[]network.IPTag{}
673+
}
674+
675+
return reflect.DeepEqual(ipTags1, ipTags2)
676+
}
677+
678+
func convertIPTagMapToSlice(ipTagMap map[string]string) *[]network.IPTag {
679+
if ipTagMap == nil {
680+
return nil
681+
}
682+
683+
if len(ipTagMap) == 0 {
684+
return &[]network.IPTag{}
685+
}
686+
687+
outputTags := []network.IPTag{}
688+
for k, v := range ipTagMap {
689+
ipTag := network.IPTag{
690+
IPTagType: to.StringPtr(k),
691+
Tag: to.StringPtr(v),
692+
}
693+
outputTags = append(outputTags, ipTag)
694+
}
695+
696+
return &outputTags
697+
}
698+
607699
func getDomainNameLabel(pip *network.PublicIPAddress) string {
608700
if pip == nil || pip.PublicIPAddressPropertiesFormat == nil || pip.PublicIPAddressPropertiesFormat.DNSSettings == nil {
609701
return ""
@@ -1468,10 +1560,34 @@ func deduplicate(collection *[]string) *[]string {
14681560
return &result
14691561
}
14701562

1563+
// Determine if we should release existing owned public IPs
1564+
func shouldReleaseExistingOwnedPublicIP(existingPip *network.PublicIPAddress, lbShouldExist bool, lbIsInternal bool, desiredPipName string, ipTagRequest serviceIPTagRequest) bool {
1565+
// Latch some variables for readability purposes.
1566+
pipName := *(*existingPip).Name
1567+
1568+
// Assume the current IP Tags are empty by default unless properties specify otherwise.
1569+
currentIPTags := &[]network.IPTag{}
1570+
pipPropertiesFormat := (*existingPip).PublicIPAddressPropertiesFormat
1571+
if pipPropertiesFormat != nil {
1572+
currentIPTags = (*pipPropertiesFormat).IPTags
1573+
}
1574+
1575+
// Release the ip under the following criteria -
1576+
// #1 - If we don't actually want a load balancer,
1577+
return !lbShouldExist ||
1578+
// #2 - If the load balancer is internal, and thus doesn't require public exposure
1579+
lbIsInternal ||
1580+
// #3 - If the name of this public ip does not match the desired name,
1581+
(pipName != desiredPipName) ||
1582+
// #4 If the service annotations have specified the ip tags that the public ip must have, but they do not match the ip tags of the existing instance
1583+
(ipTagRequest.IPTagsRequestedByAnnotation && !areIPTagsEquivalent(currentIPTags, ipTagRequest.IPTags))
1584+
}
1585+
14711586
// This reconciles the PublicIP resources similar to how the LB is reconciled.
14721587
func (az *Cloud) reconcilePublicIP(clusterName string, service *v1.Service, lbName string, wantLb bool) (*network.PublicIPAddress, error) {
14731588
isInternal := requiresInternalLoadBalancer(service)
14741589
serviceName := getServiceName(service)
1590+
serviceIPTagRequest := getServiceIPTagRequestForPublicIP(service)
14751591
var lb *network.LoadBalancer
14761592
var desiredPipName string
14771593
var err error
@@ -1498,27 +1614,39 @@ func (az *Cloud) reconcilePublicIP(clusterName string, service *v1.Service, lbNa
14981614
return nil, err
14991615
}
15001616

1501-
var found bool
1617+
var serviceAnnotationRequestsNamedPublicIP bool = shouldPIPExisted
1618+
var discoveredDesiredPublicIP bool
1619+
var deletedDesiredPublicIP bool
15021620
var pipsToBeDeleted []*network.PublicIPAddress
15031621
for i := range pips {
15041622
pip := pips[i]
15051623
pipName := *pip.Name
1506-
if serviceOwnsPublicIP(&pip, clusterName, serviceName) {
1507-
// We need to process for pips belong to this service
1508-
if wantLb && !isInternal && pipName == desiredPipName {
1509-
// This is the only case we should preserve the
1510-
// Public ip resource with match service tag
1511-
found = true
1512-
} else {
1513-
pipsToBeDeleted = append(pipsToBeDeleted, &pip)
1514-
}
1515-
} else if wantLb && !isInternal && pipName == desiredPipName {
1516-
found = true
1624+
1625+
// If we've been told to use a specific public ip by the client, let's track whether or not it actually existed
1626+
// when we inspect the set in Azure.
1627+
discoveredDesiredPublicIP = discoveredDesiredPublicIP || wantLb && !isInternal && pipName == desiredPipName
1628+
1629+
// Now, let's perform additional analysis to determine if we should release the public ips we have found.
1630+
// We can only let them go if (a) they are owned by this service and (b) they meet the criteria for deletion.
1631+
if serviceOwnsPublicIP(&pip, clusterName, serviceName) &&
1632+
shouldReleaseExistingOwnedPublicIP(&pip, wantLb, isInternal, desiredPipName, serviceIPTagRequest) {
1633+
1634+
// Then, release the public ip
1635+
pipsToBeDeleted = append(pipsToBeDeleted, &pip)
1636+
1637+
// Flag if we deleted the desired public ip
1638+
deletedDesiredPublicIP = deletedDesiredPublicIP || pipName == desiredPipName
1639+
1640+
// An aside: It would be unusual, but possible, for us to delete a public ip referred to explicitly by name
1641+
// in Service annotations (which is usually reserved for non-service-owned externals), if that IP is tagged as
1642+
// having been owned by a particular Kubernetes cluster.
15171643
}
15181644
}
1519-
if !isInternal && shouldPIPExisted && !found && wantLb {
1645+
1646+
if !isInternal && serviceAnnotationRequestsNamedPublicIP && !discoveredDesiredPublicIP && wantLb {
15201647
return nil, fmt.Errorf("reconcilePublicIP for service(%s): pip(%s) not found", serviceName, desiredPipName)
15211648
}
1649+
15221650
var deleteFuncs []func() error
15231651
for _, pip := range pipsToBeDeleted {
15241652
pipCopy := *pip
@@ -1536,7 +1664,8 @@ func (az *Cloud) reconcilePublicIP(clusterName string, service *v1.Service, lbNa
15361664
// Confirm desired public ip resource exists
15371665
var pip *network.PublicIPAddress
15381666
domainNameLabel, found := getPublicIPDomainNameLabel(service)
1539-
if pip, err = az.ensurePublicIPExists(service, desiredPipName, domainNameLabel, clusterName, shouldPIPExisted, found); err != nil {
1667+
errorIfPublicIPDoesNotExist := serviceAnnotationRequestsNamedPublicIP && discoveredDesiredPublicIP && !deletedDesiredPublicIP
1668+
if pip, err = az.ensurePublicIPExists(service, desiredPipName, domainNameLabel, clusterName, errorIfPublicIPDoesNotExist, found); err != nil {
15401669
return nil, err
15411670
}
15421671
return pip, nil

0 commit comments

Comments
 (0)