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

     1  /*
     2  Copyright 2019 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 inboundnatrules
    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  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    25  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    26  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/async"
    27  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    28  )
    29  
    30  const serviceName = "inboundnatrules"
    31  
    32  // InboundNatScope defines the scope interface for an inbound NAT service.
    33  type InboundNatScope interface {
    34  	azure.ClusterDescriber
    35  	azure.AsyncStatusUpdater
    36  	APIServerLBName() string
    37  	InboundNatSpecs() []azure.ResourceSpecGetter
    38  }
    39  
    40  // Service provides operations on Azure resources.
    41  type Service struct {
    42  	Scope InboundNatScope
    43  	client
    44  	async.Reconciler
    45  }
    46  
    47  // New creates a new service.
    48  func New(scope InboundNatScope) (*Service, error) {
    49  	client, err := newClient(scope, scope.DefaultedAzureCallTimeout())
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	return &Service{
    54  		Scope:  scope,
    55  		client: client,
    56  		Reconciler: async.New[armnetwork.InboundNatRulesClientCreateOrUpdateResponse,
    57  			armnetwork.InboundNatRulesClientDeleteResponse](scope, client, client),
    58  	}, nil
    59  }
    60  
    61  // Name returns the service name.
    62  func (s *Service) Name() string {
    63  	return serviceName
    64  }
    65  
    66  // Reconcile idempotently creates or updates an inbound NAT rule.
    67  func (s *Service) Reconcile(ctx context.Context) error {
    68  	ctx, log, done := tele.StartSpanWithLogger(ctx, "inboundnatrules.Service.Reconcile")
    69  	defer done()
    70  
    71  	ctx, cancel := context.WithTimeout(ctx, s.Scope.DefaultedAzureServiceReconcileTimeout())
    72  	defer cancel()
    73  
    74  	// Externally managed clusters might not have an LB
    75  	if s.Scope.APIServerLBName() == "" {
    76  		log.V(4).Info("Skipping InboundNatRule reconciliation as the cluster has no LB configured")
    77  		return nil
    78  	}
    79  
    80  	specs := s.Scope.InboundNatSpecs()
    81  	if len(specs) == 0 {
    82  		return nil
    83  	}
    84  
    85  	existingRules, err := s.client.List(ctx, s.Scope.ResourceGroup(), s.Scope.APIServerLBName())
    86  	if err != nil {
    87  		result := errors.Wrapf(err, "failed to get existing NAT rules")
    88  		s.Scope.UpdatePutStatus(infrav1.InboundNATRulesReadyCondition, serviceName, result)
    89  		return result
    90  	}
    91  
    92  	portsInUse := make(map[int32]struct{})
    93  	for _, rule := range existingRules {
    94  		portsInUse[*rule.Properties.FrontendPort] = struct{}{} // Mark frontend port as in use
    95  	}
    96  
    97  	// We go through the list of InboundNatSpecs to reconcile each one, independently of the result of the previous one.
    98  	// If multiple errors occur, we return the most pressing one.
    99  	//  Order of precedence (highest -> lowest) is: error that is not an operationNotDoneError (i.e. error creating) -> operationNotDoneError (i.e. creating in progress) -> no error (i.e. created)
   100  	var result error
   101  	for _, spec := range specs {
   102  		// Find an available SSH port for the rule.
   103  		sshFrontendPort, err := getAvailableSSHFrontendPort(portsInUse)
   104  		if err != nil {
   105  			return errors.Wrapf(err, "failed to find available SSH Frontend port for NAT Rule %s in load balancer %s", spec.ResourceName(), spec.OwnerResourceName())
   106  		}
   107  		natRule, ok := spec.(*InboundNatSpec)
   108  		if !ok {
   109  			result = errors.Errorf("%T is not of type InboundNatSpec", spec)
   110  		}
   111  		natRule.SSHFrontendPort = &sshFrontendPort
   112  		// Add the SSH frontend port to the list of ports in use
   113  		portsInUse[sshFrontendPort] = struct{}{}
   114  		if _, err := s.CreateOrUpdateResource(ctx, natRule, serviceName); err != nil {
   115  			if !azure.IsOperationNotDoneError(err) || result == nil {
   116  				result = err
   117  			}
   118  		}
   119  	}
   120  
   121  	s.Scope.UpdatePutStatus(infrav1.InboundNATRulesReadyCondition, serviceName, result)
   122  
   123  	return result
   124  }
   125  
   126  // Delete deletes the inbound NAT rule with the provided name.
   127  func (s *Service) Delete(ctx context.Context) error {
   128  	ctx, _, done := tele.StartSpanWithLogger(ctx, "inboundnatrules.Service.Delete")
   129  	defer done()
   130  
   131  	ctx, cancel := context.WithTimeout(ctx, s.Scope.DefaultedAzureServiceReconcileTimeout())
   132  	defer cancel()
   133  
   134  	specs := s.Scope.InboundNatSpecs()
   135  	if len(specs) == 0 {
   136  		return nil
   137  	}
   138  
   139  	// We go through the list of InboundNatSpecs to delete each one, independently of the result of the previous one.
   140  	// If multiple errors occur, we return the most pressing one.
   141  	//  Order of precedence (highest -> lowest) is: error that is not an operationNotDoneError (i.e. error deleting) -> operationNotDoneError (i.e. deleting in progress) -> no error (i.e. deleted)
   142  	var result error
   143  	for _, natRule := range specs {
   144  		if err := s.DeleteResource(ctx, natRule, serviceName); err != nil {
   145  			if !azure.IsOperationNotDoneError(err) || result == nil {
   146  				result = err
   147  			}
   148  		}
   149  	}
   150  
   151  	s.Scope.UpdateDeleteStatus(infrav1.InboundNATRulesReadyCondition, serviceName, result)
   152  	return result
   153  }
   154  
   155  // IsManaged returns always returns true as CAPZ does not support BYO inbound NAT rules.
   156  func (s *Service) IsManaged(ctx context.Context) (bool, error) {
   157  	return true, nil
   158  }