github.com/openshift/installer@v1.4.17/pkg/asset/machines/clusterapi.go (about) 1 package machines 2 3 import ( 4 "context" 5 "fmt" 6 "net" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "github.com/pkg/errors" 12 "github.com/sirupsen/logrus" 13 "github.com/vmware/govmomi/vim25/soap" 14 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 "k8s.io/utils/ptr" 16 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" 17 "sigs.k8s.io/controller-runtime/pkg/client" 18 "sigs.k8s.io/yaml" 19 20 configv1 "github.com/openshift/api/config/v1" 21 "github.com/openshift/installer/pkg/asset" 22 "github.com/openshift/installer/pkg/asset/installconfig" 23 icazure "github.com/openshift/installer/pkg/asset/installconfig/azure" 24 "github.com/openshift/installer/pkg/asset/machines/aws" 25 "github.com/openshift/installer/pkg/asset/machines/azure" 26 "github.com/openshift/installer/pkg/asset/machines/gcp" 27 nutanixcapi "github.com/openshift/installer/pkg/asset/machines/nutanix" 28 "github.com/openshift/installer/pkg/asset/machines/openstack" 29 "github.com/openshift/installer/pkg/asset/machines/powervs" 30 vspherecapi "github.com/openshift/installer/pkg/asset/machines/vsphere" 31 "github.com/openshift/installer/pkg/asset/manifests/capiutils" 32 "github.com/openshift/installer/pkg/asset/rhcos" 33 "github.com/openshift/installer/pkg/clusterapi" 34 rhcosutils "github.com/openshift/installer/pkg/rhcos" 35 "github.com/openshift/installer/pkg/types" 36 awstypes "github.com/openshift/installer/pkg/types/aws" 37 awsdefaults "github.com/openshift/installer/pkg/types/aws/defaults" 38 azuretypes "github.com/openshift/installer/pkg/types/azure" 39 azuredefaults "github.com/openshift/installer/pkg/types/azure/defaults" 40 gcptypes "github.com/openshift/installer/pkg/types/gcp" 41 nutanixtypes "github.com/openshift/installer/pkg/types/nutanix" 42 openstacktypes "github.com/openshift/installer/pkg/types/openstack" 43 powervstypes "github.com/openshift/installer/pkg/types/powervs" 44 vspheretypes "github.com/openshift/installer/pkg/types/vsphere" 45 ) 46 47 var _ asset.WritableRuntimeAsset = (*ClusterAPI)(nil) 48 49 var machineManifestDir = filepath.Join(capiutils.ManifestDir, "machines") 50 51 // ClusterAPI is the asset for CAPI control-plane manifests. 52 type ClusterAPI struct { 53 FileList []*asset.RuntimeFile 54 } 55 56 // Name returns a human friendly name for the operator. 57 func (c *ClusterAPI) Name() string { 58 return "Cluster API Machine Manifests" 59 } 60 61 // Dependencies returns all of the dependencies directly needed by the 62 // ClusterAPI machines asset. 63 func (c *ClusterAPI) Dependencies() []asset.Asset { 64 return []asset.Asset{ 65 &installconfig.InstallConfig{}, 66 &installconfig.ClusterID{}, 67 new(rhcos.Image), 68 } 69 } 70 71 // Generate generates Cluster API machine manifests. 72 // 73 //nolint:gocyclo 74 func (c *ClusterAPI) Generate(ctx context.Context, dependencies asset.Parents) error { 75 installConfig := &installconfig.InstallConfig{} 76 clusterID := &installconfig.ClusterID{} 77 rhcosImage := new(rhcos.Image) 78 dependencies.Get(installConfig, clusterID, rhcosImage) 79 80 // If the feature gate is not enabled, do not generate any manifests. 81 if !capiutils.IsEnabled(installConfig) { 82 return nil 83 } 84 85 c.FileList = []*asset.RuntimeFile{} 86 87 var err error 88 ic := installConfig.Config 89 pool := *ic.ControlPlane 90 91 switch ic.Platform.Name() { 92 case awstypes.Name: 93 subnets := map[string]string{} 94 bootstrapSubnets := map[string]string{} 95 if len(ic.Platform.AWS.Subnets) > 0 { 96 // fetch private subnets to master nodes. 97 subnetMeta, err := installConfig.AWS.PrivateSubnets(ctx) 98 if err != nil { 99 return err 100 } 101 for id, subnet := range subnetMeta { 102 subnets[subnet.Zone.Name] = id 103 } 104 // fetch public subnets for bootstrap, when exists, otherwise use private. 105 if installConfig.Config.Publish == types.ExternalPublishingStrategy { 106 subnetMeta, err := installConfig.AWS.PublicSubnets(ctx) 107 if err != nil { 108 return err 109 } 110 for id, subnet := range subnetMeta { 111 bootstrapSubnets[subnet.Zone.Name] = id 112 } 113 } else { 114 bootstrapSubnets = subnets 115 } 116 } 117 118 mpool := defaultAWSMachinePoolPlatform("master") 119 120 osImage := strings.SplitN(rhcosImage.ControlPlane, ",", 2) 121 osImageID := osImage[0] 122 if len(osImage) == 2 { 123 osImageID = "" // the AMI will be generated later on 124 } 125 mpool.AMIID = osImageID 126 127 mpool.Set(ic.Platform.AWS.DefaultMachinePlatform) 128 mpool.Set(pool.Platform.AWS) 129 zoneDefaults := false 130 if len(mpool.Zones) == 0 { 131 if len(subnets) > 0 { 132 for zone := range subnets { 133 mpool.Zones = append(mpool.Zones, zone) 134 } 135 } else { 136 mpool.Zones, err = installConfig.AWS.AvailabilityZones(ctx) 137 if err != nil { 138 return err 139 } 140 zoneDefaults = true 141 } 142 } 143 144 if mpool.InstanceType == "" { 145 topology := configv1.HighlyAvailableTopologyMode 146 if pool.Replicas != nil && *pool.Replicas == 1 { 147 topology = configv1.SingleReplicaTopologyMode 148 } 149 mpool.InstanceType, err = aws.PreferredInstanceType(ctx, installConfig.AWS, awsdefaults.InstanceTypes(installConfig.Config.Platform.AWS.Region, installConfig.Config.ControlPlane.Architecture, topology), mpool.Zones) 150 if err != nil { 151 logrus.Warn(errors.Wrap(err, "failed to find default instance type")) 152 mpool.InstanceType = awsdefaults.InstanceTypes(installConfig.Config.Platform.AWS.Region, installConfig.Config.ControlPlane.Architecture, topology)[0] 153 } 154 } 155 156 // if the list of zones is the default we need to try to filter the list in case there are some zones where the instance might not be available 157 if zoneDefaults { 158 mpool.Zones, err = aws.FilterZonesBasedOnInstanceType(ctx, installConfig.AWS, mpool.InstanceType, mpool.Zones) 159 if err != nil { 160 logrus.Warn(errors.Wrap(err, "failed to filter zone list")) 161 } 162 } 163 164 tags, err := aws.CapaTagsFromUserTags(clusterID.InfraID, installConfig.Config.Platform.AWS.UserTags) 165 if err != nil { 166 return fmt.Errorf("failed to create CAPA tags from UserTags: %w", err) 167 } 168 169 pool.Platform.AWS = &mpool 170 awsMachines, err := aws.GenerateMachines(clusterID.InfraID, &aws.MachineInput{ 171 Role: "master", 172 Pool: &pool, 173 Subnets: subnets, 174 Tags: tags, 175 PublicIP: false, 176 Ignition: &v1beta2.Ignition{ 177 Version: "3.2", 178 // master machines should get ignition from the MCS on the bootstrap node 179 StorageType: v1beta2.IgnitionStorageTypeOptionUnencryptedUserData, 180 }, 181 }) 182 if err != nil { 183 return errors.Wrap(err, "failed to create master machine objects") 184 } 185 c.FileList = append(c.FileList, awsMachines...) 186 187 ignition, err := aws.CapaIgnitionWithCertBundleAndProxy(installConfig.Config.AdditionalTrustBundle, installConfig.Config.Proxy) 188 if err != nil { 189 return fmt.Errorf("failed to generation CAPA ignition: %w", err) 190 } 191 ignition.StorageType = v1beta2.IgnitionStorageTypeOptionClusterObjectStore 192 193 pool := *ic.ControlPlane 194 pool.Name = "bootstrap" 195 pool.Replicas = ptr.To[int64](1) 196 pool.Platform.AWS = &mpool 197 bootstrapAWSMachine, err := aws.GenerateMachines(clusterID.InfraID, &aws.MachineInput{ 198 Role: "bootstrap", 199 Subnets: bootstrapSubnets, 200 Pool: &pool, 201 Tags: tags, 202 PublicIP: installConfig.Config.Publish == types.ExternalPublishingStrategy, 203 PublicIpv4Pool: ic.Platform.AWS.PublicIpv4Pool, 204 Ignition: ignition, 205 }) 206 if err != nil { 207 return fmt.Errorf("failed to create bootstrap machine object: %w", err) 208 } 209 c.FileList = append(c.FileList, bootstrapAWSMachine...) 210 case azuretypes.Name: 211 mpool := defaultAzureMachinePoolPlatform() 212 mpool.InstanceType = azuredefaults.ControlPlaneInstanceType( 213 installConfig.Config.Platform.Azure.CloudName, 214 installConfig.Config.Platform.Azure.Region, 215 installConfig.Config.ControlPlane.Architecture, 216 ) 217 mpool.OSDisk.DiskSizeGB = 1024 218 if installConfig.Config.Platform.Azure.CloudName == azuretypes.StackCloud { 219 mpool.OSDisk.DiskSizeGB = azuredefaults.AzurestackMinimumDiskSize 220 } 221 mpool.Set(ic.Platform.Azure.DefaultMachinePlatform) 222 mpool.Set(pool.Platform.Azure) 223 224 session, err := installConfig.Azure.Session() 225 if err != nil { 226 return fmt.Errorf("failed to fetch session: %w", err) 227 } 228 client := icazure.NewClient(session) 229 230 if len(mpool.Zones) == 0 { 231 // if no azs are given we set to []string{""} for convenience over later operations. 232 // It means no-zoned for the machine API 233 mpool.Zones = []string{""} 234 } 235 if len(mpool.Zones) == 0 { 236 azs, err := client.GetAvailabilityZones(ctx, ic.Platform.Azure.Region, mpool.InstanceType) 237 if err != nil { 238 return fmt.Errorf("failed to fetch availability zones: %w", err) 239 } 240 mpool.Zones = azs 241 if len(azs) == 0 { 242 // if no azs are given we set to []string{""} for convenience over later operations. 243 // It means no-zoned for the machine API 244 mpool.Zones = []string{""} 245 } 246 } 247 // client.GetControlPlaneSubnet(ctx, ic.Platform.Azure.ResourceGroupName, ic.Platform.Azure.VirtualNetwork, ) 248 249 if mpool.OSImage.Publisher != "" { 250 img, ierr := client.GetMarketplaceImage(ctx, ic.Platform.Azure.Region, mpool.OSImage.Publisher, mpool.OSImage.Offer, mpool.OSImage.SKU, mpool.OSImage.Version) 251 if ierr != nil { 252 return fmt.Errorf("failed to fetch marketplace image: %w", ierr) 253 } 254 // Publisher is case-sensitive and matched against exactly. Also the 255 // Plan's publisher might not be exactly the same as the Image's 256 // publisher 257 if img.Plan != nil && img.Plan.Publisher != nil { 258 mpool.OSImage.Publisher = *img.Plan.Publisher 259 } 260 } 261 capabilities, err := client.GetVMCapabilities(ctx, mpool.InstanceType, installConfig.Config.Platform.Azure.Region) 262 if err != nil { 263 return err 264 } 265 if mpool.VMNetworkingType == "" { 266 isAccelerated := icazure.GetVMNetworkingCapability(capabilities) 267 if isAccelerated { 268 mpool.VMNetworkingType = string(azuretypes.VMnetworkingTypeAccelerated) 269 } else { 270 logrus.Infof("Instance type %s does not support Accelerated Networking. Using Basic Networking instead.", mpool.InstanceType) 271 } 272 } 273 pool.Platform.Azure = &mpool 274 subnet := ic.Azure.ControlPlaneSubnet 275 276 hyperVGen, err := icazure.GetHyperVGenerationVersion(capabilities, "") 277 if err != nil { 278 return err 279 } 280 281 azureMachines, err := azure.GenerateMachines(clusterID.InfraID, 282 installConfig.Config.Azure.ClusterResourceGroupName(clusterID.InfraID), 283 session.Credentials.SubscriptionID, 284 &azure.MachineInput{ 285 Subnet: subnet, 286 Role: "master", 287 UserDataSecret: "master-user-data", 288 HyperVGen: hyperVGen, 289 UseImageGallery: false, 290 Private: installConfig.Config.Publish == types.InternalPublishingStrategy, 291 UserTags: installConfig.Config.Platform.Azure.UserTags, 292 Platform: installConfig.Config.Platform.Azure, 293 Pool: &pool, 294 }, 295 ) 296 if err != nil { 297 return fmt.Errorf("failed to create master machine objects: %w", err) 298 } 299 300 c.FileList = append(c.FileList, azureMachines...) 301 case gcptypes.Name: 302 // Generate GCP master machines using ControPlane machinepool 303 mpool := defaultGCPMachinePoolPlatform(pool.Architecture) 304 mpool.Set(ic.Platform.GCP.DefaultMachinePlatform) 305 mpool.Set(pool.Platform.GCP) 306 if len(mpool.Zones) == 0 { 307 azs, err := gcp.ZonesForInstanceType(ic.Platform.GCP.ProjectID, ic.Platform.GCP.Region, mpool.InstanceType) 308 if err != nil { 309 return errors.Wrap(err, "failed to fetch availability zones") 310 } 311 mpool.Zones = azs 312 } 313 pool.Platform.GCP = &mpool 314 315 gcpMachines, err := gcp.GenerateMachines( 316 installConfig, 317 clusterID.InfraID, 318 &pool, 319 rhcosImage.ControlPlane, 320 ) 321 if err != nil { 322 return fmt.Errorf("failed to create master machine objects %w", err) 323 } 324 c.FileList = append(c.FileList, gcpMachines...) 325 326 // Generate GCP bootstrap machines 327 bootstrapMachines, err := gcp.GenerateBootstrapMachines( 328 capiutils.GenerateBoostrapMachineName(clusterID.InfraID), 329 installConfig, 330 clusterID.InfraID, 331 &pool, 332 rhcosImage.ControlPlane, 333 ) 334 if err != nil { 335 return fmt.Errorf("failed to create bootstrap machine objects %w", err) 336 } 337 c.FileList = append(c.FileList, bootstrapMachines...) 338 case vspheretypes.Name: 339 mpool := defaultVSphereMachinePoolPlatform() 340 mpool.NumCPUs = 4 341 mpool.NumCoresPerSocket = 4 342 mpool.MemoryMiB = 16384 343 mpool.Set(ic.Platform.VSphere.DefaultMachinePlatform) 344 mpool.Set(pool.Platform.VSphere) 345 346 platform := ic.VSphere 347 resolver := &net.Resolver{ 348 PreferGo: true, 349 } 350 351 for _, v := range platform.VCenters { 352 // Defense against potential issues with assisted installer 353 // If the installer is unable to resolve vCenter there is a good possibility 354 // that the installer's install-config has been provided with bogus values. 355 356 // Timeout context for Lookup 357 ctx, cancel := context.WithTimeout(ctx, 30*time.Second) 358 defer cancel() 359 360 _, err := resolver.LookupHost(ctx, v.Server) 361 if err != nil { 362 logrus.Warnf("unable to resolve vSphere server %s", v.Server) 363 return nil 364 } 365 366 // Timeout context for Networks 367 // vCenter APIs can be unreliable in performance, extended this context 368 // timeout to 60 seconds. 369 ctx, cancel = context.WithTimeout(ctx, 60*time.Second) 370 defer cancel() 371 372 err = installConfig.VSphere.Networks(ctx, v, platform.FailureDomains) 373 if err != nil { 374 // If we are receiving an error as a Soap Fault this is caused by 375 // incorrect credentials and in the scenario of assisted installer 376 // the credentials are never valid. Since vCenter hostname is 377 // incorrect as well we shouldn't get this far. 378 if soap.IsSoapFault(err) { 379 logrus.Warn("authentication failure to vCenter, Cluster API machine manifests not created, cluster may not install") 380 return nil 381 } 382 return err 383 } 384 } 385 386 // The machinepool has no zones defined, there are FailureDomains 387 // This is a vSphere zonal installation. Generate machinepool zone 388 // list. 389 390 fdCount := int64(len(ic.Platform.VSphere.FailureDomains)) 391 var idx int64 392 if len(mpool.Zones) == 0 && len(ic.VSphere.FailureDomains) != 0 { 393 for i := int64(0); i < *(ic.ControlPlane.Replicas); i++ { 394 idx = i 395 if idx >= fdCount { 396 idx = i % fdCount 397 } 398 mpool.Zones = append(mpool.Zones, ic.VSphere.FailureDomains[idx].Name) 399 } 400 } 401 402 pool.Platform.VSphere = &mpool 403 templateName := clusterID.InfraID + "-rhcos" 404 405 c.FileList, err = vspherecapi.GenerateMachines(ctx, clusterID.InfraID, ic, &pool, templateName, "master", installConfig.VSphere) 406 if err != nil { 407 return fmt.Errorf("unable to generate CAPI machines for vSphere %w", err) 408 } 409 case openstacktypes.Name: 410 mpool := defaultOpenStackMachinePoolPlatform() 411 mpool.Set(ic.Platform.OpenStack.DefaultMachinePlatform) 412 mpool.Set(pool.Platform.OpenStack) 413 pool.Platform.OpenStack = &mpool 414 415 imageName, _ := rhcosutils.GenerateOpenStackImageName(rhcosImage.ControlPlane, clusterID.InfraID) 416 417 for _, role := range []string{"master", "bootstrap"} { 418 openStackMachines, err := openstack.GenerateMachines( 419 clusterID.InfraID, 420 ic, 421 &pool, 422 imageName, 423 role, 424 ) 425 if err != nil { 426 return fmt.Errorf("failed to create machine objects: %w", err) 427 } 428 c.FileList = append(c.FileList, openStackMachines...) 429 } 430 case powervstypes.Name: 431 // Generate PowerVS master machines using ControPlane machinepool 432 mpool := defaultPowerVSMachinePoolPlatform(ic) 433 mpool.Set(ic.Platform.PowerVS.DefaultMachinePlatform) 434 mpool.Set(pool.Platform.PowerVS) 435 pool.Platform.PowerVS = &mpool 436 437 powervsMachines, err := powervs.GenerateMachines( 438 clusterID.InfraID, 439 ic, 440 &pool, 441 "master", 442 ) 443 if err != nil { 444 return fmt.Errorf("failed to create master machine objects %w", err) 445 } 446 447 c.FileList = append(c.FileList, powervsMachines...) 448 case nutanixtypes.Name: 449 mpool := defaultNutanixMachinePoolPlatform() 450 mpool.NumCPUs = 8 451 mpool.Set(ic.Platform.Nutanix.DefaultMachinePlatform) 452 mpool.Set(pool.Platform.Nutanix) 453 if err = mpool.ValidateConfig(ic.Platform.Nutanix, "master"); err != nil { 454 return fmt.Errorf("failed to generate Cluster API machine manifests for control-plane: %w", err) 455 } 456 pool.Platform.Nutanix = &mpool 457 templateName := nutanixtypes.RHCOSImageName(clusterID.InfraID) 458 459 c.FileList, err = nutanixcapi.GenerateMachines(clusterID.InfraID, ic, &pool, templateName, "master") 460 if err != nil { 461 return fmt.Errorf("unable to generate CAPI machines for Nutanix %w", err) 462 } 463 default: 464 // TODO: support other platforms 465 } 466 467 // Create the machine manifests. 468 for _, m := range c.FileList { 469 objData, err := yaml.Marshal(m.Object) 470 if err != nil { 471 return errors.Wrapf(err, "failed to marshal Cluster API machine manifest %s", m.Filename) 472 } 473 m.Data = objData 474 475 // If the filename is already a path, do not append the manifest dir. 476 if filepath.Dir(m.Filename) == machineManifestDir { 477 continue 478 } 479 m.Filename = filepath.Join(machineManifestDir, m.Filename) 480 } 481 asset.SortManifestFiles(c.FileList) 482 return nil 483 } 484 485 // Files returns the files generated by the asset. 486 func (c *ClusterAPI) Files() []*asset.File { 487 files := []*asset.File{} 488 for _, f := range c.FileList { 489 f := f // TODO: remove with golang 1.22 490 files = append(files, &f.File) 491 } 492 return files 493 } 494 495 // RuntimeFiles returns the files generated by the asset. 496 func (c *ClusterAPI) RuntimeFiles() []*asset.RuntimeFile { 497 return c.FileList 498 } 499 500 // Load returns the openshift asset from disk. 501 func (c *ClusterAPI) Load(f asset.FileFetcher) (bool, error) { 502 yamlFileList, err := f.FetchByPattern(filepath.Join(machineManifestDir, "*.yaml")) 503 if err != nil { 504 return false, errors.Wrap(err, "failed to load *.yaml files") 505 } 506 ymlFileList, err := f.FetchByPattern(filepath.Join(machineManifestDir, "*.yml")) 507 if err != nil { 508 return false, errors.Wrap(err, "failed to load *.yml files") 509 } 510 jsonFileList, err := f.FetchByPattern(filepath.Join(machineManifestDir, "*.json")) 511 if err != nil { 512 return false, errors.Wrap(err, "failed to load *.json files") 513 } 514 fileList := append(yamlFileList, ymlFileList...) //nolint:gocritic 515 fileList = append(fileList, jsonFileList...) 516 517 for _, file := range fileList { 518 u := &unstructured.Unstructured{} 519 if err := yaml.Unmarshal(file.Data, u); err != nil { 520 return false, errors.Wrap(err, "failed to unmarshal file") 521 } 522 obj, err := clusterapi.Scheme.New(u.GroupVersionKind()) 523 if err != nil { 524 return false, errors.Wrap(err, "failed to create object") 525 } 526 if err := clusterapi.Scheme.Convert(u, obj, nil); err != nil { 527 return false, errors.Wrap(err, "failed to convert object") 528 } 529 c.FileList = append(c.FileList, &asset.RuntimeFile{ 530 File: asset.File{ 531 Filename: file.Filename, 532 Data: file.Data, 533 }, 534 Object: obj.(client.Object), 535 }) 536 } 537 538 asset.SortManifestFiles(c.FileList) 539 return len(c.FileList) > 0, nil 540 }