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 }