Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reserve ILB IP address before deleting FR on protocol change #1839

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 2 additions & 44 deletions pkg/loadbalancers/forwarding_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,53 +195,11 @@ func (l7 *L7) getEffectiveIP() (string, bool, error) {

// ensureForwardingRule creates a forwarding rule with the given name, if it does not exist. It updates the existing
// forwarding rule if needed.
func (l4 *L4) ensureForwardingRule(bsLink string, options gce.ILBOptions, existingFwdRule *composite.ForwardingRule) (*composite.ForwardingRule, error) {
func (l4 *L4) ensureForwardingRule(bsLink string, options gce.ILBOptions, existingFwdRule *composite.ForwardingRule, subnetworkURL, ipToUse string) (*composite.ForwardingRule, error) {
// version used for creating the existing forwarding rule.
version := meta.VersionGA
frName := l4.GetFRName()

if l4.cloud.IsLegacyNetwork() {
l4.recorder.Event(l4.Service, v1.EventTypeWarning, "ILBOptionsIgnored", "Internal LoadBalancer options are not supported with Legacy Networks.")
options = gce.ILBOptions{}
}
subnetworkURL := l4.cloud.SubnetworkURL()

// Custom subnet feature is always enabled when running L4 controller.
// Changes to subnet annotation will be picked up and reflected in the forwarding rule.
// Removing the annotation will set the forwarding rule to use the default subnet.
if options.SubnetName != "" {
var err error
subnetworkURL, err = l4.getSubnetworkURLByName(options.SubnetName)
if err != nil {
return nil, err
}
}
// Determine IP which will be used for this LB. If no forwarding rule has been established
// or specified in the Service spec, then requestedIP = "".
ipToUse := l4lbIPToUse(l4.Service, existingFwdRule, subnetworkURL)
klog.V(2).Infof("ensureForwardingRule(%v): Using subnet %q for LoadBalancer IP %s", frName, subnetworkURL, ipToUse)

var addrMgr *addressManager
// If the network is not a legacy network, use the address manager
if !l4.cloud.IsLegacyNetwork() {
nm := types.NamespacedName{Namespace: l4.Service.Namespace, Name: l4.Service.Name}.String()
// ILB can be created only in Premium Tier
addrMgr = newAddressManager(l4.cloud, nm, l4.cloud.Region(), subnetworkURL, frName, ipToUse, cloud.SchemeInternal, cloud.NetworkTierPremium)
var err error
ipToUse, _, err = addrMgr.HoldAddress()
if err != nil {
return nil, err
}
klog.V(2).Infof("ensureForwardingRule(%v): reserved IP %q for the forwarding rule", frName, ipToUse)
defer func() {
// Release the address that was reserved, in all cases. If the forwarding rule was successfully created,
// the ephemeral IP is not needed anymore. If it was not created, the address should be released to prevent leaks.
if err := addrMgr.ReleaseAddress(); err != nil {
klog.Errorf("ensureForwardingRule: failed to release address reservation, possibly causing an orphan: %v", err)
}
}()
}

servicePorts := l4.Service.Spec.Ports
ports := utils.GetPorts(servicePorts)
protocol := utils.GetProtocol(servicePorts)
Expand Down Expand Up @@ -275,7 +233,7 @@ func (l4 *L4) ensureForwardingRule(bsLink string, options gce.ILBOptions, existi
if existingFwdRule != nil {
equal, err := Equal(existingFwdRule, fr)
if err != nil {
return existingFwdRule, err
return nil, err
}
if equal {
// nothing to do
Expand Down
109 changes: 78 additions & 31 deletions pkg/loadbalancers/l4.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package loadbalancers

import (
"fmt"
"strings"
"time"

Expand Down Expand Up @@ -99,9 +100,14 @@ func (l4 *L4) CreateKey(name string) (*meta.Key, error) {
}

// getILBOptions fetches the optional features requested on the given ILB service.
func getILBOptions(svc *corev1.Service) gce.ILBOptions {
return gce.ILBOptions{AllowGlobalAccess: gce.GetLoadBalancerAnnotationAllowGlobalAccess(svc),
SubnetName: gce.GetLoadBalancerAnnotationSubnet(svc)}
func (l4 *L4) getILBOptions() gce.ILBOptions {
if l4.cloud.IsLegacyNetwork() {
l4.recorder.Event(l4.Service, corev1.EventTypeWarning, "ILBOptionsIgnored", "Internal LoadBalancer options are not supported with Legacy Networks.")
return gce.ILBOptions{}
}

return gce.ILBOptions{AllowGlobalAccess: gce.GetLoadBalancerAnnotationAllowGlobalAccess(l4.Service),
SubnetName: gce.GetLoadBalancerAnnotationSubnet(l4.Service)}
}

// EnsureInternalLoadBalancerDeleted performs a cleanup of all GCE resources for the given loadbalancer service.
Expand Down Expand Up @@ -186,7 +192,8 @@ func (l4 *L4) EnsureInternalLoadBalancer(nodeNames []string, svc *corev1.Service
result := &L4ILBSyncResult{
Annotations: make(map[string]string),
StartTime: time.Now(),
SyncType: SyncTypeCreate}
SyncType: SyncTypeCreate,
}

// If service already has an IP assigned, treat it as an update instead of a new Loadbalancer.
// This will also cover cases where an external LB is updated to an ILB, which is technically a create for ILB.
Expand All @@ -196,58 +203,66 @@ func (l4 *L4) EnsureInternalLoadBalancer(nodeNames []string, svc *corev1.Service
}

l4.Service = svc
// All resources use the L4Backend name, except forwarding rule.
name := l4.namer.L4Backend(l4.Service.Namespace, l4.Service.Name)
options := getILBOptions(l4.Service)

// create healthcheck
sharedHC := !helpers.RequestsOnlyLocalTraffic(l4.Service)
hcResult := l4.healthChecks.EnsureHealthCheckWithFirewall(l4.Service, l4.namer, sharedHC, meta.Global, utils.ILB, nodeNames)

if hcResult.Err != nil {
result.GCEResourceInError = hcResult.GceResourceInError
result.Error = hcResult.Err
return result
}
result.Annotations[annotations.HealthcheckKey] = hcResult.HCName

servicePorts := l4.Service.Spec.Ports
portRanges := utils.GetServicePortRanges(servicePorts)
protocol := utils.GetProtocol(servicePorts)

// Check if protocol has changed for this service. In this case, forwarding rule should be deleted before
// the backend service can be updated.
existingBS, err := l4.backendPool.Get(name, meta.VersionGA, l4.scope)
err = utils.IgnoreHTTPNotFound(err)
if err != nil {
klog.Errorf("Failed to lookup existing backend service, ignoring err: %v", err)
}
existingFR, err := l4.forwardingRules.Get(l4.GetFRName())
if existingBS != nil && existingBS.Protocol != string(protocol) {
klog.Infof("Protocol changed from %q to %q for service %s", existingBS.Protocol, string(protocol), l4.NamespacedName)
// Delete forwarding rule if it exists
frName := l4.getFRNameWithProtocol(existingBS.Protocol)
existingFR, err = l4.forwardingRules.Get(frName)
// Reserve existing IP address before making any changes
existingFR, err := l4.getOldForwardingRule()
options := l4.getILBOptions()
subnetworkURL, err := l4.getServiceSubnetworkURL(options)
ipToUse := l4lbIPToUse(l4.Service, existingFR, subnetworkURL)
expectedFRName := l4.GetFRName()
if !l4.cloud.IsLegacyNetwork() {
nm := types.NamespacedName{Namespace: l4.Service.Namespace, Name: l4.Service.Name}.String()
// ILB can be created only in Premium Tier
addrMgr := newAddressManager(l4.cloud, nm, l4.cloud.Region(), subnetworkURL, expectedFRName, ipToUse, cloud.SchemeInternal, cloud.NetworkTierPremium)
ipToUse, _, err = addrMgr.HoldAddress()
if err != nil {
klog.Errorf("Failed to get forwarding rule %s, err %v", frName, err)
result.Error = fmt.Errorf("EnsureInternalLoadBalancer error: addrMgr.HoldAddress() returned error %w", err)
return result
}
err = l4.forwardingRules.Delete(frName)
klog.V(2).Infof("EnsureInternalLoadBalancer(%v): reserved IP %q", nm, ipToUse)
defer func() {
// Release the address that was reserved, in all cases. If the forwarding rule was successfully created,
// the ephemeral IP is not needed anymore. If it was not created, the address should be released to prevent leaks.
if err := addrMgr.ReleaseAddress(); err != nil {
klog.Errorf("EnsureInternalLoadBalancer: failed to release address reservation, possibly causing an orphan: %v", err)
}
}()
}

// if Service protocol changed, we must delete forwarding rule before changing backend service,
panslava marked this conversation as resolved.
Show resolved Hide resolved
// otherwise, on updating backend service, google cloud api will return error
servicePorts := l4.Service.Spec.Ports
protocol := utils.GetProtocol(servicePorts)
if existingFR != nil && existingFR.IPProtocol != string(protocol) {
err = l4.forwardingRules.Delete(existingFR.Name)
if err != nil {
klog.Errorf("Failed to delete forwarding rule %s, err %v", frName, err)
klog.Errorf("Failed to delete forwarding rule %s, err %v", existingFR.Name, err)
}
}

// ensure backend service
bs, err := l4.backendPool.EnsureL4BackendService(name, hcResult.HCLink, string(protocol), string(l4.Service.Spec.SessionAffinity),
bsName := l4.namer.L4Backend(l4.Service.Namespace, l4.Service.Name)
bs, err := l4.backendPool.EnsureL4BackendService(bsName, hcResult.HCLink, string(protocol), string(l4.Service.Spec.SessionAffinity),
string(cloud.SchemeInternal), l4.NamespacedName, meta.VersionGA)
if err != nil {
result.GCEResourceInError = annotations.BackendServiceResource
result.Error = err
return result
}
result.Annotations[annotations.BackendServiceKey] = name
result.Annotations[annotations.BackendServiceKey] = bsName

// create fr rule
fr, err := l4.ensureForwardingRule(bs.SelfLink, options, existingFR)
fr, err := l4.ensureForwardingRule(bs.SelfLink, options, existingFR, subnetworkURL, ipToUse)
if err != nil {
klog.Errorf("EnsureInternalLoadBalancer: Failed to create forwarding rule - %v", err)
result.GCEResourceInError = annotations.ForwardingRuleResource
Expand All @@ -268,6 +283,7 @@ func (l4 *L4) EnsureInternalLoadBalancer(nodeNames []string, svc *corev1.Service
}
// Add firewall rule for ILB traffic to nodes
firewallName := l4.namer.L4Firewall(l4.Service.Namespace, l4.Service.Name)
portRanges := utils.GetServicePortRanges(servicePorts)
nodesFWRParams := firewalls.FirewallParams{
PortRanges: portRanges,
SourceRanges: sourceRanges.StringSlice(),
Expand Down Expand Up @@ -300,10 +316,41 @@ func (l4 *L4) EnsureInternalLoadBalancer(nodeNames []string, svc *corev1.Service
return result
}

func (l4 *L4) getServiceSubnetworkURL(options gce.ILBOptions) (string, error) {
// Custom subnet feature is always enabled when running L4 controller.
// Changes to subnet annotation will be picked up and reflected in the forwarding rule.
// Removing the annotation will set the forwarding rule to use the default subnet.
if options.SubnetName != "" {
return l4.getSubnetworkURLByName(options.SubnetName)
}
return l4.cloud.SubnetworkURL(), nil
}

func (l4 *L4) getSubnetworkURLByName(subnetName string) (string, error) {
subnetKey, err := l4.CreateKey(subnetName)
if err != nil {
return "", err
}
return cloud.SelfLink(meta.VersionGA, l4.cloud.NetworkProjectID(), "subnetworks", subnetKey), nil
}

func (l4 *L4) getOldForwardingRule() (*composite.ForwardingRule, error) {
bsName := l4.namer.L4Backend(l4.Service.Namespace, l4.Service.Name)
// Check if protocol has changed for this service. In this case, forwarding rule has different protocol and name
existingBS, err := l4.backendPool.Get(bsName, meta.VersionGA, l4.scope)
err = utils.IgnoreHTTPNotFound(err)
if err != nil {
klog.Errorf("Failed to lookup existing backend service, ignoring err: %v", err)
}

servicePorts := l4.Service.Spec.Ports
protocol := utils.GetProtocol(servicePorts)
if existingBS != nil && existingBS.Protocol != string(protocol) {
klog.Infof("Protocol changed from %q to %q for service %s", existingBS.Protocol, string(protocol), l4.NamespacedName)
// Delete forwarding rule if it exists
oldProtocolFRRName := l4.getFRNameWithProtocol(existingBS.Protocol)
return l4.forwardingRules.Get(oldProtocolFRRName)
}

return l4.forwardingRules.Get(l4.GetFRName())
}
23 changes: 23 additions & 0 deletions pkg/loadbalancers/l4_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,27 @@ func TestEnsureInternalLoadBalancerModifyProtocol(t *testing.T) {
}
return mock.UpdateRegionBackendServiceHook(ctx, key, be, m)
}
// Before deleting forwarding rule, check, that the address was reserved
c.MockForwardingRules.DeleteHook = func(ctx context.Context, key *meta.Key, m *cloud.MockForwardingRules) (bool, error) {
fr, err := c.MockForwardingRules.Get(ctx, key)
// if forwarding rule not exists, don't need to check if address reserved
if utils.IsNotFoundError(err) {
return false, nil
}
if err != nil {
return false, err
}

addr, err := l4.cloud.GetRegionAddressByIP(fr.Region, fr.IPAddress)
if utils.IgnoreHTTPNotFound(err) != nil {
return true, err
}
if addr == nil || utils.IsNotFoundError(err) {
t.Errorf("Address not reserved before deleting forwarding rule +%v", fr)
}

return false, nil
}

frName := l4.getFRNameWithProtocol("TCP")
result := l4.EnsureInternalLoadBalancer(nodeNames, svc)
Expand Down Expand Up @@ -1280,6 +1301,8 @@ func TestEnsureInternalLoadBalancerModifyProtocol(t *testing.T) {
t.Errorf("Unexpected protocol value %s, expected UDP", fwdRule.IPProtocol)
}

// on final deletion of Load Balancer we don't need to check if address was reserved (which was happening in the hook)
c.MockForwardingRules.DeleteHook = nil
// Delete the service
result = l4.EnsureInternalLoadBalancerDeleted(svc)
if err != nil {
Expand Down