sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/services/virtualmachines/virtualmachines.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 virtualmachines
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  
    23  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
    24  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4"
    25  	"github.com/pkg/errors"
    26  	corev1 "k8s.io/api/core/v1"
    27  	"k8s.io/utils/ptr"
    28  	azprovider "sigs.k8s.io/cloud-provider-azure/pkg/provider"
    29  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    30  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    31  	"sigs.k8s.io/cluster-api-provider-azure/azure/converters"
    32  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/async"
    33  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/identities"
    34  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/networkinterfaces"
    35  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/publicips"
    36  	azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure"
    37  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    38  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    39  )
    40  
    41  const serviceName = "virtualmachine"
    42  const vmMissingUAI = "VM is missing expected user assigned identity with client ID: "
    43  
    44  // VMScope defines the scope interface for a virtual machines service.
    45  type VMScope interface {
    46  	azure.Authorizer
    47  	azure.AsyncStatusUpdater
    48  	VMSpec() azure.ResourceSpecGetter
    49  	SetAnnotation(string, string)
    50  	SetProviderID(string)
    51  	SetAddresses([]corev1.NodeAddress)
    52  	SetVMState(infrav1.ProvisioningState)
    53  	SetConditionFalse(clusterv1.ConditionType, string, clusterv1.ConditionSeverity, string)
    54  }
    55  
    56  // Service provides operations on Azure resources.
    57  type Service struct {
    58  	Scope VMScope
    59  	async.Reconciler
    60  	interfacesGetter async.Getter
    61  	publicIPsGetter  async.Getter
    62  	identitiesGetter identities.Client
    63  }
    64  
    65  // New creates a new service.
    66  func New(scope VMScope) (*Service, error) {
    67  	Client, err := NewClient(scope, scope.DefaultedAzureCallTimeout())
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	identitiesSvc, err := identities.NewClient(scope)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	interfacesSvc, err := networkinterfaces.NewClient(scope, scope.DefaultedAzureCallTimeout())
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	publicIPsSvc, err := publicips.NewClient(scope, scope.DefaultedAzureCallTimeout())
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	return &Service{
    84  		Scope:            scope,
    85  		interfacesGetter: interfacesSvc,
    86  		publicIPsGetter:  publicIPsSvc,
    87  		identitiesGetter: identitiesSvc,
    88  		Reconciler: async.New[armcompute.VirtualMachinesClientCreateOrUpdateResponse,
    89  			armcompute.VirtualMachinesClientDeleteResponse](scope, Client, Client),
    90  	}, nil
    91  }
    92  
    93  // Name returns the service name.
    94  func (s *Service) Name() string {
    95  	return serviceName
    96  }
    97  
    98  // Reconcile idempotently creates or updates a virtual machine.
    99  func (s *Service) Reconcile(ctx context.Context) error {
   100  	ctx, _, done := tele.StartSpanWithLogger(ctx, "virtualmachines.Service.Reconcile")
   101  	defer done()
   102  
   103  	ctx, cancel := context.WithTimeout(ctx, s.Scope.DefaultedAzureServiceReconcileTimeout())
   104  	defer cancel()
   105  
   106  	vmSpec := s.Scope.VMSpec()
   107  	if vmSpec == nil {
   108  		return nil
   109  	}
   110  
   111  	result, err := s.CreateOrUpdateResource(ctx, vmSpec, serviceName)
   112  	s.Scope.UpdatePutStatus(infrav1.VMRunningCondition, serviceName, err)
   113  	// Set the DiskReady condition here since the disk gets created with the VM.
   114  	s.Scope.UpdatePutStatus(infrav1.DisksReadyCondition, serviceName, err)
   115  	if err == nil && result != nil {
   116  		vm, ok := result.(armcompute.VirtualMachine)
   117  		if !ok {
   118  			return errors.Errorf("%T is not an armcompute.VirtualMachine", result)
   119  		}
   120  		infraVM := converters.SDKToVM(vm)
   121  		// Transform the VM resource representation to conform to the cloud-provider-azure representation
   122  		providerID, err := azprovider.ConvertResourceGroupNameToLower(azureutil.ProviderIDPrefix + infraVM.ID)
   123  		if err != nil {
   124  			return errors.Wrapf(err, "failed to parse VM ID %s", infraVM.ID)
   125  		}
   126  		s.Scope.SetProviderID(providerID)
   127  		s.Scope.SetAnnotation("cluster-api-provider-azure", "true")
   128  
   129  		// Discover addresses for NICs associated with the VM
   130  		addresses, err := s.getAddresses(ctx, vm, vmSpec.ResourceGroupName())
   131  		if err != nil {
   132  			return errors.Wrap(err, "failed to fetch VM addresses")
   133  		}
   134  		s.Scope.SetAddresses(addresses)
   135  		s.Scope.SetVMState(infraVM.State)
   136  
   137  		spec, ok := vmSpec.(*VMSpec)
   138  		if !ok {
   139  			return errors.Errorf("%T is not a valid VM spec", vmSpec)
   140  		}
   141  
   142  		err = s.checkUserAssignedIdentities(ctx, spec.UserAssignedIdentities, infraVM.UserAssignedIdentities)
   143  		if err != nil {
   144  			return errors.Wrap(err, "failed to check user assigned identities")
   145  		}
   146  	}
   147  	return err
   148  }
   149  
   150  // Delete deletes the virtual machine with the provided name.
   151  func (s *Service) Delete(ctx context.Context) error {
   152  	ctx, _, done := tele.StartSpanWithLogger(ctx, "virtualmachines.Service.Delete")
   153  	defer done()
   154  
   155  	ctx, cancel := context.WithTimeout(ctx, s.Scope.DefaultedAzureServiceReconcileTimeout())
   156  	defer cancel()
   157  
   158  	vmSpec := s.Scope.VMSpec()
   159  	if vmSpec == nil {
   160  		return nil
   161  	}
   162  
   163  	err := s.DeleteResource(ctx, vmSpec, serviceName)
   164  	if err != nil {
   165  		s.Scope.SetVMState(infrav1.Deleting)
   166  	} else {
   167  		s.Scope.SetVMState(infrav1.Deleted)
   168  	}
   169  	s.Scope.UpdateDeleteStatus(infrav1.VMRunningCondition, serviceName, err)
   170  	return err
   171  }
   172  
   173  func (s *Service) checkUserAssignedIdentities(ctx context.Context, specIdentities []infrav1.UserAssignedIdentity, vmIdentities []infrav1.UserAssignedIdentity) error {
   174  	expectedMap := make(map[string]struct{})
   175  	actualMap := make(map[string]struct{})
   176  
   177  	// Create a map of the expected identities. The ProviderID is converted to match the format of the VM identity.
   178  	for _, expectedIdentity := range specIdentities {
   179  		identitiesClient := s.identitiesGetter
   180  		parsed, err := azureutil.ParseResourceID(expectedIdentity.ProviderID)
   181  		if err != nil {
   182  			return err
   183  		}
   184  		if parsed.SubscriptionID != s.Scope.SubscriptionID() {
   185  			identitiesClient, err = identities.NewClientBySub(s.Scope, parsed.SubscriptionID)
   186  			if err != nil {
   187  				return errors.Wrapf(err, "failed to create identities client from subscription ID %s", parsed.SubscriptionID)
   188  			}
   189  		}
   190  		expectedClientID, err := identitiesClient.GetClientID(ctx, expectedIdentity.ProviderID)
   191  		if err != nil {
   192  			return errors.Wrap(err, "failed to get client ID")
   193  		}
   194  		expectedMap[expectedClientID] = struct{}{}
   195  	}
   196  
   197  	// Create a map of the actual identities from the vm.
   198  	for _, actualIdentity := range vmIdentities {
   199  		actualMap[actualIdentity.ProviderID] = struct{}{}
   200  	}
   201  
   202  	// Check if the expected identities are present in the vm.
   203  	for expectedKey := range expectedMap {
   204  		_, exists := actualMap[expectedKey]
   205  		if !exists {
   206  			s.Scope.SetConditionFalse(infrav1.VMIdentitiesReadyCondition, infrav1.UserAssignedIdentityMissingReason, clusterv1.ConditionSeverityWarning, vmMissingUAI+expectedKey)
   207  			return nil
   208  		}
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func (s *Service) getAddresses(ctx context.Context, vm armcompute.VirtualMachine, rgName string) ([]corev1.NodeAddress, error) {
   215  	ctx, _, done := tele.StartSpanWithLogger(ctx, "virtualmachines.Service.getAddresses")
   216  	defer done()
   217  
   218  	addresses := []corev1.NodeAddress{
   219  		{
   220  			Type:    corev1.NodeInternalDNS,
   221  			Address: ptr.Deref(vm.Name, ""),
   222  		},
   223  	}
   224  	if vm.Properties.NetworkProfile.NetworkInterfaces == nil {
   225  		return addresses, nil
   226  	}
   227  	for _, nicRef := range vm.Properties.NetworkProfile.NetworkInterfaces {
   228  		// The full ID includes the name at the very end. Split the string and pull the last element
   229  		// Ex: /subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.Network/networkInterfaces/$NICNAME
   230  		// We'll check to see if ID is nil and bail early if we don't have it
   231  		if nicRef.ID == nil {
   232  			continue
   233  		}
   234  		nicName := getResourceNameByID(ptr.Deref(nicRef.ID, ""))
   235  
   236  		// Fetch nic and append its addresses
   237  		existingNic, err := s.interfacesGetter.Get(ctx, &networkinterfaces.NICSpec{
   238  			Name:          nicName,
   239  			ResourceGroup: rgName,
   240  		})
   241  		if err != nil {
   242  			return addresses, err
   243  		}
   244  
   245  		nic, ok := existingNic.(armnetwork.Interface)
   246  		if !ok {
   247  			return nil, errors.Errorf("%T is not an armnetwork.Interface", existingNic)
   248  		}
   249  
   250  		if nic.Properties.IPConfigurations == nil {
   251  			continue
   252  		}
   253  		for _, ipConfig := range nic.Properties.IPConfigurations {
   254  			if ipConfig != nil && ipConfig.Properties != nil && ipConfig.Properties.PrivateIPAddress != nil {
   255  				addresses = append(addresses,
   256  					corev1.NodeAddress{
   257  						Type:    corev1.NodeInternalIP,
   258  						Address: ptr.Deref(ipConfig.Properties.PrivateIPAddress, ""),
   259  					},
   260  				)
   261  			}
   262  
   263  			if ipConfig.Properties.PublicIPAddress == nil {
   264  				continue
   265  			}
   266  			// ID is the only field populated in PublicIPAddress sub-resource.
   267  			// Thus, we have to go fetch the publicIP with the name.
   268  			publicIPName := getResourceNameByID(ptr.Deref(ipConfig.Properties.PublicIPAddress.ID, ""))
   269  			publicNodeAddress, err := s.getPublicIPAddress(ctx, publicIPName, rgName)
   270  			if err != nil {
   271  				return addresses, err
   272  			}
   273  			addresses = append(addresses, publicNodeAddress)
   274  		}
   275  	}
   276  
   277  	return addresses, nil
   278  }
   279  
   280  // getPublicIPAddress will fetch a public ip address resource by name and return a nodeaddresss representation.
   281  func (s *Service) getPublicIPAddress(ctx context.Context, publicIPAddressName string, rgName string) (corev1.NodeAddress, error) {
   282  	ctx, _, done := tele.StartSpanWithLogger(ctx, "virtualmachines.Service.getPublicIPAddress")
   283  	defer done()
   284  
   285  	retAddress := corev1.NodeAddress{}
   286  	result, err := s.publicIPsGetter.Get(ctx, &publicips.PublicIPSpec{
   287  		Name:          publicIPAddressName,
   288  		ResourceGroup: rgName,
   289  	})
   290  	if err != nil {
   291  		return retAddress, err
   292  	}
   293  
   294  	publicIP, ok := result.(armnetwork.PublicIPAddress)
   295  	if !ok {
   296  		return retAddress, errors.Errorf("%T is not an armnetwork.PublicIPAddress", result)
   297  	}
   298  
   299  	retAddress.Type = corev1.NodeExternalIP
   300  	retAddress.Address = ptr.Deref(publicIP.Properties.IPAddress, "")
   301  
   302  	return retAddress, nil
   303  }
   304  
   305  // getResourceNameById takes a resource ID like
   306  // `/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.Network/networkInterfaces/$NICNAME`
   307  // and parses out the string after the last slash.
   308  func getResourceNameByID(resourceID string) string {
   309  	explodedResourceID := strings.Split(resourceID, "/")
   310  	resourceName := explodedResourceID[len(explodedResourceID)-1]
   311  	return resourceName
   312  }
   313  
   314  // IsManaged returns always returns true as CAPZ does not support BYO VM.
   315  func (s *Service) IsManaged(ctx context.Context) (bool, error) {
   316  	return true, nil
   317  }