github.com/openshift/installer@v1.4.17/pkg/tfvars/openstack/openstack.go (about)

     1  // Package openstack contains OpenStack-specific Terraform-variable logic.
     2  package openstack
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  
     9  	"github.com/gophercloud/gophercloud/v2"
    10  	"github.com/gophercloud/gophercloud/v2/openstack"
    11  	"github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens"
    12  
    13  	configv1 "github.com/openshift/api/config/v1"
    14  	machinev1alpha1 "github.com/openshift/api/machine/v1alpha1"
    15  	"github.com/openshift/installer/pkg/asset/installconfig"
    16  	"github.com/openshift/installer/pkg/asset/machines"
    17  	"github.com/openshift/installer/pkg/rhcos"
    18  	types_openstack "github.com/openshift/installer/pkg/types/openstack"
    19  	openstackdefaults "github.com/openshift/installer/pkg/types/openstack/defaults"
    20  )
    21  
    22  // TFVars generates OpenStack-specific Terraform variables.
    23  func TFVars(
    24  	ctx context.Context,
    25  	installConfig *installconfig.InstallConfig,
    26  	mastersAsset *machines.Master,
    27  	workersAsset *machines.Worker,
    28  	baseImage string,
    29  	clusterID *installconfig.ClusterID,
    30  	bootstrapIgn string,
    31  ) ([]byte, error) {
    32  	var (
    33  		cloud        = installConfig.Config.Platform.OpenStack.Cloud
    34  		mastermpool  = installConfig.Config.ControlPlane.Platform.OpenStack
    35  		defaultmpool = installConfig.Config.OpenStack.DefaultMachinePlatform
    36  	)
    37  
    38  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", openstackdefaults.DefaultClientOpts(cloud))
    39  	if err != nil {
    40  		return nil, fmt.Errorf("failed to build an OpenStack service client: %w", err)
    41  	}
    42  
    43  	var masterSpecs []*machinev1alpha1.OpenstackProviderSpec
    44  	{
    45  		masters, err := mastersAsset.Machines()
    46  		if err != nil {
    47  			return nil, err
    48  		}
    49  
    50  		for _, master := range masters {
    51  			masterSpecs = append(masterSpecs, master.Spec.ProviderSpec.Value.Object.(*machinev1alpha1.OpenstackProviderSpec))
    52  		}
    53  	}
    54  
    55  	var workerSpecs []*machinev1alpha1.OpenstackProviderSpec
    56  	{
    57  		workers, err := workersAsset.MachineSets()
    58  		if err != nil {
    59  			return nil, err
    60  		}
    61  
    62  		for _, worker := range workers {
    63  			workerSpecs = append(workerSpecs, worker.Spec.Template.Spec.ProviderSpec.Value.Object.(*machinev1alpha1.OpenstackProviderSpec))
    64  		}
    65  	}
    66  
    67  	var workermpool *types_openstack.MachinePool
    68  	if len(installConfig.Config.Compute) > 0 {
    69  		// Only considering the first Compute machinepool here, because
    70  		// the current Installer implementation allows for one only.
    71  		//
    72  		// This validation code[1] errors if the pool is not named
    73  		// "worker", and also errors in case of duplicate names,
    74  		// factually rendering impossible to have two machinepools in
    75  		// the install-config YAML array.
    76  		//
    77  		// [1]: https://github.com/openshift/installer/blob/252facf5e6e1238ee60b5f78607214e8691a3eab/pkg/types/validation/installconfig.go#L404-L410
    78  		if len(installConfig.Config.Compute) > 1 {
    79  			panic("Multiple machine-pools are currently not supported by the OpenShift installer on OpenStack platform")
    80  		}
    81  		workermpool = installConfig.Config.Compute[0].Platform.OpenStack
    82  	}
    83  
    84  	var userManagedLoadBalancer bool
    85  	if lb := installConfig.Config.Platform.OpenStack.LoadBalancer; lb != nil && lb.Type == configv1.LoadBalancerTypeUserManaged {
    86  		userManagedLoadBalancer = true
    87  	}
    88  
    89  	// computeAvailabilityZones is a slice where each index targets a master.
    90  	computeAvailabilityZones := make([]string, len(masterSpecs))
    91  	for i := range computeAvailabilityZones {
    92  		computeAvailabilityZones[i] = masterSpecs[i].AvailabilityZone
    93  	}
    94  
    95  	// storageAvailabilityZones is a slice where each index targets a master.
    96  	storageAvailabilityZones := make([]string, len(masterSpecs))
    97  	for i := range storageAvailabilityZones {
    98  		if masterSpecs[i].RootVolume != nil {
    99  			storageAvailabilityZones[i] = masterSpecs[i].RootVolume.Zone
   100  		}
   101  	}
   102  
   103  	// storageVolumeTypes is a slice where each index targets a master.
   104  	storageVolumeTypes := make([]string, len(masterSpecs))
   105  	for i := range storageVolumeTypes {
   106  		if masterSpecs[i].RootVolume != nil {
   107  			storageVolumeTypes[i] = masterSpecs[i].RootVolume.VolumeType
   108  		}
   109  	}
   110  
   111  	// If baseImage is a URL, the corresponding image will be uploaded to
   112  	// Glance in the PreTerraform hook of the Cluster asset.
   113  	imageName, _ := rhcos.GenerateOpenStackImageName(baseImage, clusterID.InfraID)
   114  
   115  	serviceCatalog, err := getServiceCatalog(ctx, cloud)
   116  	if err != nil {
   117  		return nil, fmt.Errorf("could not retrieve service catalog: %w", err)
   118  	}
   119  
   120  	octaviaSupport, err := isOctaviaSupported(serviceCatalog)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	var rootVolumeSize int
   126  	if rootVolume := masterSpecs[0].RootVolume; rootVolume != nil {
   127  		rootVolumeSize = rootVolume.Size
   128  	}
   129  
   130  	masterServerGroupPolicy := GetServerGroupPolicy(mastermpool, defaultmpool)
   131  	masterServerGroupName := masterSpecs[0].ServerGroupName
   132  	if masterSpecs[0].ServerGroupID != "" {
   133  		return nil, fmt.Errorf("the field ServerGroupID is not implemented in the Installer. Please use ServerGroupName for automatic creation of the Control Plane server group")
   134  	}
   135  
   136  	workerServerGroupPolicy := GetServerGroupPolicy(workermpool, defaultmpool)
   137  	var workerServerGroupNames []string
   138  	{
   139  		for _, workerConfig := range workerSpecs {
   140  			workerServerGroupNames = append(workerServerGroupNames, workerConfig.ServerGroupName)
   141  			if workerConfig.ServerGroupID != "" {
   142  				return nil, fmt.Errorf("the field ServerGroupID is not implemented in the Installer. Please use ServerGroupName for automatic creation of the Compute server group")
   143  			}
   144  		}
   145  	}
   146  
   147  	var additionalNetworkIDs []string
   148  	if mastermpool != nil {
   149  		additionalNetworkIDs = mastermpool.AdditionalNetworkIDs
   150  	}
   151  
   152  	// defaultMachinesPort carries the machine subnets and the network.
   153  	var defaultMachinesPort *terraformPort
   154  	if controlPlanePort := installConfig.Config.Platform.OpenStack.ControlPlanePort; controlPlanePort != nil {
   155  		port, err := portTargetToTerraformPort(ctx, conn, *controlPlanePort)
   156  		if err != nil {
   157  			return nil, fmt.Errorf("failed to resolve portTarget :%w", err)
   158  		}
   159  		defaultMachinesPort = &port
   160  	}
   161  
   162  	// machinesPorts defines the primary port for a master. A nil value
   163  	// signals Terraform to fill in the blank with the network it creates.
   164  	// Each slice index targets a master.
   165  	machinesPorts := make([]*terraformPort, len(masterSpecs))
   166  
   167  	// additionalPorts translates non-control-plane
   168  	// `failureDomain.portTarget` information in Terraform-understandable
   169  	// syntax. Each slice index targets a master.
   170  	additionalPorts := make([][]terraformPort, len(masterSpecs))
   171  	for i := range masterSpecs {
   172  		// Assign a slice to each master's index, no matter what.
   173  		// Terraform expects each master to get an array, empty or otherwise.
   174  		additionalPorts[i] = []terraformPort{}
   175  		machinesPorts[i] = defaultMachinesPort
   176  	}
   177  
   178  	var additionalSecurityGroupIDs []string
   179  	if mastermpool != nil {
   180  		additionalSecurityGroupIDs = mastermpool.AdditionalSecurityGroupIDs
   181  	}
   182  
   183  	return json.MarshalIndent(struct {
   184  		BaseImageName                     string                            `json:"openstack_base_image_name,omitempty"`
   185  		ExternalNetwork                   string                            `json:"openstack_external_network,omitempty"`
   186  		Cloud                             string                            `json:"openstack_credentials_cloud,omitempty"`
   187  		FlavorName                        string                            `json:"openstack_master_flavor_name,omitempty"`
   188  		APIFloatingIP                     string                            `json:"openstack_api_floating_ip,omitempty"`
   189  		IngressFloatingIP                 string                            `json:"openstack_ingress_floating_ip,omitempty"`
   190  		APIVIPs                           []string                          `json:"openstack_api_int_ips,omitempty"`
   191  		IngressVIPs                       []string                          `json:"openstack_ingress_ips,omitempty"`
   192  		OctaviaSupport                    bool                              `json:"openstack_octavia_support,omitempty"`
   193  		RootVolumeSize                    int                               `json:"openstack_master_root_volume_size,omitempty"`
   194  		BootstrapShim                     string                            `json:"openstack_bootstrap_shim_ignition,omitempty"`
   195  		ExternalDNS                       []string                          `json:"openstack_external_dns,omitempty"`
   196  		MasterServerGroupName             string                            `json:"openstack_master_server_group_name,omitempty"`
   197  		MasterServerGroupPolicy           types_openstack.ServerGroupPolicy `json:"openstack_master_server_group_policy"`
   198  		WorkerServerGroupNames            []string                          `json:"openstack_worker_server_group_names,omitempty"`
   199  		WorkerServerGroupPolicy           types_openstack.ServerGroupPolicy `json:"openstack_worker_server_group_policy"`
   200  		AdditionalNetworkIDs              []string                          `json:"openstack_additional_network_ids,omitempty"`
   201  		AdditionalPorts                   [][]terraformPort                 `json:"openstack_additional_ports"`
   202  		AdditionalSecurityGroupIDs        []string                          `json:"openstack_master_extra_sg_ids,omitempty"`
   203  		DefaultMachinesPort               *terraformPort                    `json:"openstack_default_machines_port,omitempty"`
   204  		MachinesPorts                     []*terraformPort                  `json:"openstack_machines_ports"`
   205  		MasterAvailabilityZones           []string                          `json:"openstack_master_availability_zones,omitempty"`
   206  		MasterRootVolumeAvailabilityZones []string                          `json:"openstack_master_root_volume_availability_zones,omitempty"`
   207  		MasterRootVolumeTypes             []string                          `json:"openstack_master_root_volume_types,omitempty"`
   208  		UserManagedLoadBalancer           bool                              `json:"openstack_user_managed_load_balancer"`
   209  	}{
   210  		BaseImageName:                     imageName,
   211  		ExternalNetwork:                   installConfig.Config.Platform.OpenStack.ExternalNetwork,
   212  		Cloud:                             cloud,
   213  		FlavorName:                        masterSpecs[0].Flavor,
   214  		APIFloatingIP:                     installConfig.Config.Platform.OpenStack.APIFloatingIP,
   215  		IngressFloatingIP:                 installConfig.Config.Platform.OpenStack.IngressFloatingIP,
   216  		APIVIPs:                           installConfig.Config.Platform.OpenStack.APIVIPs,
   217  		IngressVIPs:                       installConfig.Config.Platform.OpenStack.IngressVIPs,
   218  		OctaviaSupport:                    octaviaSupport,
   219  		RootVolumeSize:                    rootVolumeSize,
   220  		BootstrapShim:                     bootstrapIgn,
   221  		ExternalDNS:                       installConfig.Config.Platform.OpenStack.ExternalDNS,
   222  		MasterServerGroupName:             masterServerGroupName,
   223  		MasterServerGroupPolicy:           masterServerGroupPolicy,
   224  		WorkerServerGroupNames:            workerServerGroupNames,
   225  		WorkerServerGroupPolicy:           workerServerGroupPolicy,
   226  		AdditionalNetworkIDs:              additionalNetworkIDs,
   227  		AdditionalPorts:                   additionalPorts,
   228  		AdditionalSecurityGroupIDs:        additionalSecurityGroupIDs,
   229  		DefaultMachinesPort:               defaultMachinesPort,
   230  		MachinesPorts:                     machinesPorts,
   231  		MasterAvailabilityZones:           computeAvailabilityZones,
   232  		MasterRootVolumeAvailabilityZones: storageAvailabilityZones,
   233  		MasterRootVolumeTypes:             storageVolumeTypes,
   234  		UserManagedLoadBalancer:           userManagedLoadBalancer,
   235  	}, "", "  ")
   236  }
   237  
   238  // getServiceCatalog fetches OpenStack service catalog with service endpoints
   239  func getServiceCatalog(ctx context.Context, cloud string) (*tokens.ServiceCatalog, error) {
   240  	conn, err := openstackdefaults.NewServiceClient(ctx, "identity", openstackdefaults.DefaultClientOpts(cloud))
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	authResult := conn.GetAuthResult()
   246  	auth, ok := authResult.(tokens.CreateResult)
   247  	if !ok {
   248  		return nil, fmt.Errorf("unable to extract service catalog")
   249  	}
   250  
   251  	serviceCatalog, err := auth.ExtractServiceCatalog()
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	return serviceCatalog, nil
   257  }
   258  
   259  func isOctaviaSupported(serviceCatalog *tokens.ServiceCatalog) (bool, error) {
   260  	_, err := openstack.V3EndpointURL(serviceCatalog, gophercloud.EndpointOpts{
   261  		Type:         "load-balancer",
   262  		Name:         "octavia",
   263  		Availability: gophercloud.AvailabilityPublic,
   264  	})
   265  	if err != nil {
   266  		if _, ok := err.(*gophercloud.ErrEndpointNotFound); ok {
   267  			return false, nil
   268  		}
   269  		return false, err
   270  	}
   271  
   272  	return true, nil
   273  }
   274  
   275  // GetServerGroupPolicy returns the server group policy set in the given machine-pool, or in the default one, or falls back to soft-anti-affinity.
   276  func GetServerGroupPolicy(machinePool, defaultMachinePool *types_openstack.MachinePool) types_openstack.ServerGroupPolicy {
   277  	if machinePool != nil && machinePool.ServerGroupPolicy.IsSet() {
   278  		return machinePool.ServerGroupPolicy
   279  	}
   280  	if defaultMachinePool != nil && defaultMachinePool.ServerGroupPolicy.IsSet() {
   281  		return defaultMachinePool.ServerGroupPolicy
   282  	}
   283  	return types_openstack.SGPolicySoftAntiAffinity
   284  }