@@ -23,6 +23,7 @@ import (
23
23
"fmt"
24
24
"math"
25
25
"reflect"
26
+ "sort"
26
27
"strconv"
27
28
"strings"
28
29
@@ -82,6 +83,9 @@ const (
82
83
// ServiceAnnotationPIPName specifies the pip that will be applied to load balancer
83
84
ServiceAnnotationPIPName = "service.beta.kubernetes.io/azure-pip-name"
84
85
86
+ // ServiceAnnotationIPTagsForPublicIP specifies the iptags used when dynamically creating a public ip
87
+ ServiceAnnotationIPTagsForPublicIP = "service.beta.kubernetes.io/azure-pip-ip-tags"
88
+
85
89
// ServiceAnnotationAllowedServiceTag is the annotation used on the service
86
90
// to specify a list of allowed service tags separated by comma
87
91
// 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
546
550
pip .Location = to .StringPtr (az .Location )
547
551
pip .PublicIPAddressPropertiesFormat = & network.PublicIPAddressPropertiesFormat {
548
552
PublicIPAllocationMethod : network .Static ,
553
+ IPTags : getServiceIPTagRequestForPublicIP (service ).IPTags ,
549
554
}
550
555
pip .Tags = map [string ]* string {
551
556
serviceTagKey : & serviceName ,
@@ -604,6 +609,93 @@ func (az *Cloud) ensurePublicIPExists(service *v1.Service, pipName string, domai
604
609
return & pip , nil
605
610
}
606
611
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
+
607
699
func getDomainNameLabel (pip * network.PublicIPAddress ) string {
608
700
if pip == nil || pip .PublicIPAddressPropertiesFormat == nil || pip .PublicIPAddressPropertiesFormat .DNSSettings == nil {
609
701
return ""
@@ -1468,10 +1560,34 @@ func deduplicate(collection *[]string) *[]string {
1468
1560
return & result
1469
1561
}
1470
1562
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
+
1471
1586
// This reconciles the PublicIP resources similar to how the LB is reconciled.
1472
1587
func (az * Cloud ) reconcilePublicIP (clusterName string , service * v1.Service , lbName string , wantLb bool ) (* network.PublicIPAddress , error ) {
1473
1588
isInternal := requiresInternalLoadBalancer (service )
1474
1589
serviceName := getServiceName (service )
1590
+ serviceIPTagRequest := getServiceIPTagRequestForPublicIP (service )
1475
1591
var lb * network.LoadBalancer
1476
1592
var desiredPipName string
1477
1593
var err error
@@ -1498,27 +1614,39 @@ func (az *Cloud) reconcilePublicIP(clusterName string, service *v1.Service, lbNa
1498
1614
return nil , err
1499
1615
}
1500
1616
1501
- var found bool
1617
+ var serviceAnnotationRequestsNamedPublicIP bool = shouldPIPExisted
1618
+ var discoveredDesiredPublicIP bool
1619
+ var deletedDesiredPublicIP bool
1502
1620
var pipsToBeDeleted []* network.PublicIPAddress
1503
1621
for i := range pips {
1504
1622
pip := pips [i ]
1505
1623
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.
1517
1643
}
1518
1644
}
1519
- if ! isInternal && shouldPIPExisted && ! found && wantLb {
1645
+
1646
+ if ! isInternal && serviceAnnotationRequestsNamedPublicIP && ! discoveredDesiredPublicIP && wantLb {
1520
1647
return nil , fmt .Errorf ("reconcilePublicIP for service(%s): pip(%s) not found" , serviceName , desiredPipName )
1521
1648
}
1649
+
1522
1650
var deleteFuncs []func () error
1523
1651
for _ , pip := range pipsToBeDeleted {
1524
1652
pipCopy := * pip
@@ -1536,7 +1664,8 @@ func (az *Cloud) reconcilePublicIP(clusterName string, service *v1.Service, lbNa
1536
1664
// Confirm desired public ip resource exists
1537
1665
var pip * network.PublicIPAddress
1538
1666
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 {
1540
1669
return nil , err
1541
1670
}
1542
1671
return pip , nil
0 commit comments