sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/loadbalancers/spec.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package loadbalancers
    18  
    19  import (
    20  	"context"
    21  
    22  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4"
    23  	"github.com/pkg/errors"
    24  	"k8s.io/utils/ptr"
    25  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    26  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    27  	"sigs.k8s.io/cluster-api-provider-azure/azure/converters"
    28  )
    29  
    30  // LBSpec defines the specification for a Load Balancer.
    31  type LBSpec struct {
    32  	Name                 string
    33  	ResourceGroup        string
    34  	SubscriptionID       string
    35  	ClusterName          string
    36  	Location             string
    37  	ExtendedLocation     *infrav1.ExtendedLocationSpec
    38  	Role                 string
    39  	Type                 infrav1.LBType
    40  	SKU                  infrav1.SKU
    41  	VNetName             string
    42  	VNetResourceGroup    string
    43  	SubnetName           string
    44  	BackendPoolName      string
    45  	FrontendIPConfigs    []infrav1.FrontendIP
    46  	APIServerPort        int32
    47  	IdleTimeoutInMinutes *int32
    48  	AdditionalTags       map[string]string
    49  }
    50  
    51  // ResourceName returns the name of the load balancer.
    52  func (s *LBSpec) ResourceName() string {
    53  	return s.Name
    54  }
    55  
    56  // ResourceGroupName returns the name of the resource group.
    57  func (s *LBSpec) ResourceGroupName() string {
    58  	return s.ResourceGroup
    59  }
    60  
    61  // OwnerResourceName is a no-op for load balancers.
    62  func (s *LBSpec) OwnerResourceName() string {
    63  	return ""
    64  }
    65  
    66  // Parameters returns the parameters for the load balancer.
    67  func (s *LBSpec) Parameters(ctx context.Context, existing interface{}) (parameters interface{}, err error) {
    68  	var (
    69  		etag                *string
    70  		frontendIDs         []*armnetwork.SubResource
    71  		frontendIPConfigs   []*armnetwork.FrontendIPConfiguration
    72  		loadBalancingRules  []*armnetwork.LoadBalancingRule
    73  		backendAddressPools []*armnetwork.BackendAddressPool
    74  		outboundRules       []*armnetwork.OutboundRule
    75  		probes              []*armnetwork.Probe
    76  	)
    77  
    78  	if existing != nil {
    79  		existingLB, ok := existing.(armnetwork.LoadBalancer)
    80  		if !ok {
    81  			return nil, errors.Errorf("%T is not an armnetwork.LoadBalancer", existing)
    82  		}
    83  		// LB already exists
    84  		// We append the existing LB etag to the header to ensure we only apply the updates if the LB has not been modified.
    85  		etag = existingLB.Etag
    86  		update := false
    87  
    88  		// merge existing LB properties with desired properties
    89  		frontendIPConfigs = existingLB.Properties.FrontendIPConfigurations
    90  		wantedIPs, wantedFrontendIDs := getFrontendIPConfigs(*s)
    91  		for _, ip := range wantedIPs {
    92  			if !ipExists(frontendIPConfigs, *ip) {
    93  				update = true
    94  				frontendIPConfigs = append(frontendIPConfigs, ip)
    95  			}
    96  		}
    97  
    98  		loadBalancingRules = existingLB.Properties.LoadBalancingRules
    99  		for _, rule := range getLoadBalancingRules(*s, wantedFrontendIDs) {
   100  			if !lbRuleExists(loadBalancingRules, *rule) {
   101  				update = true
   102  				loadBalancingRules = append(loadBalancingRules, rule)
   103  			}
   104  		}
   105  
   106  		backendAddressPools = existingLB.Properties.BackendAddressPools
   107  		for _, pool := range getBackendAddressPools(*s) {
   108  			if !poolExists(backendAddressPools, *pool) {
   109  				update = true
   110  				backendAddressPools = append(backendAddressPools, pool)
   111  			}
   112  		}
   113  
   114  		outboundRules = existingLB.Properties.OutboundRules
   115  		for _, rule := range getOutboundRules(*s, wantedFrontendIDs) {
   116  			if !outboundRuleExists(outboundRules, *rule) {
   117  				update = true
   118  				outboundRules = append(outboundRules, rule)
   119  			}
   120  		}
   121  
   122  		probes = existingLB.Properties.Probes
   123  		for _, probe := range getProbes(*s) {
   124  			if !probeExists(probes, *probe) {
   125  				update = true
   126  				probes = append(probes, probe)
   127  			}
   128  		}
   129  
   130  		if !update {
   131  			// load balancer already exists with all required defaults
   132  			return nil, nil
   133  		}
   134  	} else {
   135  		frontendIPConfigs, frontendIDs = getFrontendIPConfigs(*s)
   136  		loadBalancingRules = getLoadBalancingRules(*s, frontendIDs)
   137  		backendAddressPools = getBackendAddressPools(*s)
   138  		outboundRules = getOutboundRules(*s, frontendIDs)
   139  		probes = getProbes(*s)
   140  	}
   141  
   142  	lb := armnetwork.LoadBalancer{
   143  		Etag:             etag,
   144  		SKU:              &armnetwork.LoadBalancerSKU{Name: ptr.To(converters.SKUtoSDK(s.SKU))},
   145  		Location:         ptr.To(s.Location),
   146  		ExtendedLocation: converters.ExtendedLocationToNetworkSDK(s.ExtendedLocation),
   147  		Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{
   148  			ClusterName: s.ClusterName,
   149  			Lifecycle:   infrav1.ResourceLifecycleOwned,
   150  			Role:        ptr.To(s.Role),
   151  			Additional:  s.AdditionalTags,
   152  		})),
   153  		Properties: &armnetwork.LoadBalancerPropertiesFormat{
   154  			FrontendIPConfigurations: frontendIPConfigs,
   155  			BackendAddressPools:      backendAddressPools,
   156  			OutboundRules:            outboundRules,
   157  			Probes:                   probes,
   158  			LoadBalancingRules:       loadBalancingRules,
   159  		},
   160  	}
   161  
   162  	return lb, nil
   163  }
   164  
   165  func getFrontendIPConfigs(lbSpec LBSpec) ([]*armnetwork.FrontendIPConfiguration, []*armnetwork.SubResource) {
   166  	frontendIPConfigurations := make([]*armnetwork.FrontendIPConfiguration, 0)
   167  	frontendIDs := make([]*armnetwork.SubResource, 0)
   168  	for _, ipConfig := range lbSpec.FrontendIPConfigs {
   169  		var properties armnetwork.FrontendIPConfigurationPropertiesFormat
   170  		if lbSpec.Type == infrav1.Internal {
   171  			properties = armnetwork.FrontendIPConfigurationPropertiesFormat{
   172  				PrivateIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodStatic),
   173  				Subnet: &armnetwork.Subnet{
   174  					ID: ptr.To(azure.SubnetID(lbSpec.SubscriptionID, lbSpec.VNetResourceGroup, lbSpec.VNetName, lbSpec.SubnetName)),
   175  				},
   176  				PrivateIPAddress: ptr.To(ipConfig.PrivateIPAddress),
   177  			}
   178  		} else {
   179  			properties = armnetwork.FrontendIPConfigurationPropertiesFormat{
   180  				PublicIPAddress: &armnetwork.PublicIPAddress{
   181  					ID: ptr.To(azure.PublicIPID(lbSpec.SubscriptionID, lbSpec.ResourceGroup, ipConfig.PublicIP.Name)),
   182  				},
   183  			}
   184  		}
   185  		frontendIPConfigurations = append(frontendIPConfigurations, &armnetwork.FrontendIPConfiguration{
   186  			Properties: &properties,
   187  			Name:       ptr.To(ipConfig.Name),
   188  		})
   189  		frontendIDs = append(frontendIDs, &armnetwork.SubResource{
   190  			ID: ptr.To(azure.FrontendIPConfigID(lbSpec.SubscriptionID, lbSpec.ResourceGroup, lbSpec.Name, ipConfig.Name)),
   191  		})
   192  	}
   193  	return frontendIPConfigurations, frontendIDs
   194  }
   195  
   196  func getOutboundRules(lbSpec LBSpec, frontendIDs []*armnetwork.SubResource) []*armnetwork.OutboundRule {
   197  	if lbSpec.Type == infrav1.Internal {
   198  		return []*armnetwork.OutboundRule{}
   199  	}
   200  	return []*armnetwork.OutboundRule{
   201  		{
   202  			Name: ptr.To(outboundNAT),
   203  			Properties: &armnetwork.OutboundRulePropertiesFormat{
   204  				Protocol:                 ptr.To(armnetwork.LoadBalancerOutboundRuleProtocolAll),
   205  				IdleTimeoutInMinutes:     lbSpec.IdleTimeoutInMinutes,
   206  				FrontendIPConfigurations: frontendIDs,
   207  				BackendAddressPool: &armnetwork.SubResource{
   208  					ID: ptr.To(azure.AddressPoolID(lbSpec.SubscriptionID, lbSpec.ResourceGroup, lbSpec.Name, lbSpec.BackendPoolName)),
   209  				},
   210  			},
   211  		},
   212  	}
   213  }
   214  
   215  func getLoadBalancingRules(lbSpec LBSpec, frontendIDs []*armnetwork.SubResource) []*armnetwork.LoadBalancingRule {
   216  	if lbSpec.Role == infrav1.APIServerRole {
   217  		// We disable outbound SNAT explicitly in the HTTPS LB rule and enable TCP and UDP outbound NAT with an outbound rule.
   218  		// For more information on Standard LB outbound connections see https://learn.microsoft.com/azure/load-balancer/load-balancer-outbound-connections.
   219  		var frontendIPConfig *armnetwork.SubResource
   220  		if len(frontendIDs) != 0 {
   221  			frontendIPConfig = frontendIDs[0]
   222  		}
   223  		return []*armnetwork.LoadBalancingRule{
   224  			{
   225  				Name: ptr.To(lbRuleHTTPS),
   226  				Properties: &armnetwork.LoadBalancingRulePropertiesFormat{
   227  					DisableOutboundSnat:     ptr.To(true),
   228  					Protocol:                ptr.To(armnetwork.TransportProtocolTCP),
   229  					FrontendPort:            ptr.To[int32](lbSpec.APIServerPort),
   230  					BackendPort:             ptr.To[int32](lbSpec.APIServerPort),
   231  					IdleTimeoutInMinutes:    lbSpec.IdleTimeoutInMinutes,
   232  					EnableFloatingIP:        ptr.To(false),
   233  					LoadDistribution:        ptr.To(armnetwork.LoadDistributionDefault),
   234  					FrontendIPConfiguration: frontendIPConfig,
   235  					BackendAddressPool: &armnetwork.SubResource{
   236  						ID: ptr.To(azure.AddressPoolID(lbSpec.SubscriptionID, lbSpec.ResourceGroup, lbSpec.Name, lbSpec.BackendPoolName)),
   237  					},
   238  					Probe: &armnetwork.SubResource{
   239  						ID: ptr.To(azure.ProbeID(lbSpec.SubscriptionID, lbSpec.ResourceGroup, lbSpec.Name, httpsProbe)),
   240  					},
   241  				},
   242  			},
   243  		}
   244  	}
   245  	return []*armnetwork.LoadBalancingRule{}
   246  }
   247  
   248  func getBackendAddressPools(lbSpec LBSpec) []*armnetwork.BackendAddressPool {
   249  	return []*armnetwork.BackendAddressPool{
   250  		{
   251  			Name: ptr.To(lbSpec.BackendPoolName),
   252  		},
   253  	}
   254  }
   255  
   256  func getProbes(lbSpec LBSpec) []*armnetwork.Probe {
   257  	if lbSpec.Role == infrav1.APIServerRole {
   258  		return []*armnetwork.Probe{
   259  			{
   260  				Name: ptr.To(httpsProbe),
   261  				Properties: &armnetwork.ProbePropertiesFormat{
   262  					Protocol:          ptr.To(armnetwork.ProbeProtocolHTTPS),
   263  					Port:              ptr.To[int32](lbSpec.APIServerPort),
   264  					RequestPath:       ptr.To(httpsProbeRequestPath),
   265  					IntervalInSeconds: ptr.To[int32](15),
   266  					NumberOfProbes:    ptr.To[int32](4),
   267  				},
   268  			},
   269  		}
   270  	}
   271  	return []*armnetwork.Probe{}
   272  }
   273  
   274  func probeExists(probes []*armnetwork.Probe, probe armnetwork.Probe) bool {
   275  	for _, p := range probes {
   276  		if ptr.Deref(p.Name, "") == ptr.Deref(probe.Name, "") {
   277  			return true
   278  		}
   279  	}
   280  	return false
   281  }
   282  
   283  func outboundRuleExists(rules []*armnetwork.OutboundRule, rule armnetwork.OutboundRule) bool {
   284  	for _, r := range rules {
   285  		if ptr.Deref(r.Name, "") == ptr.Deref(rule.Name, "") {
   286  			return true
   287  		}
   288  	}
   289  	return false
   290  }
   291  
   292  func poolExists(pools []*armnetwork.BackendAddressPool, pool armnetwork.BackendAddressPool) bool {
   293  	for _, p := range pools {
   294  		if ptr.Deref(p.Name, "") == ptr.Deref(pool.Name, "") {
   295  			return true
   296  		}
   297  	}
   298  	return false
   299  }
   300  
   301  func lbRuleExists(rules []*armnetwork.LoadBalancingRule, rule armnetwork.LoadBalancingRule) bool {
   302  	for _, r := range rules {
   303  		if ptr.Deref(r.Name, "") == ptr.Deref(rule.Name, "") {
   304  			return true
   305  		}
   306  	}
   307  	return false
   308  }
   309  
   310  func ipExists(configs []*armnetwork.FrontendIPConfiguration, config armnetwork.FrontendIPConfiguration) bool {
   311  	for _, ip := range configs {
   312  		if ptr.Deref(ip.Name, "") == ptr.Deref(config.Name, "") {
   313  			return true
   314  		}
   315  	}
   316  	return false
   317  }