github.com/openshift/installer@v1.4.17/pkg/asset/manifests/aws/zones.go (about)

     1  package aws
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  
     8  	"k8s.io/apimachinery/pkg/util/sets"
     9  	capa "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
    10  
    11  	"github.com/openshift/installer/pkg/asset/installconfig"
    12  	"github.com/openshift/installer/pkg/asset/installconfig/aws"
    13  	"github.com/openshift/installer/pkg/asset/manifests/capiutils"
    14  	utilscidr "github.com/openshift/installer/pkg/asset/manifests/capiutils/cidr"
    15  	"github.com/openshift/installer/pkg/types"
    16  )
    17  
    18  // subnetsInput handles subnets information gathered from metadata.
    19  type subnetsInput struct {
    20  	vpc            string
    21  	privateSubnets aws.Subnets
    22  	publicSubnets  aws.Subnets
    23  	edgeSubnets    aws.Subnets
    24  }
    25  
    26  // zonesInput handles input parameters required to create managed and unmanaged
    27  // Subnets to CAPI.
    28  type zonesInput struct {
    29  	InstallConfig *installconfig.InstallConfig
    30  	Cluster       *capa.AWSCluster
    31  	ClusterID     *installconfig.ClusterID
    32  	ZonesInRegion []string
    33  	Subnets       *subnetsInput
    34  }
    35  
    36  // GatherZonesFromMetadata retrieves zones from AWS API to be used
    37  // when building the subnets to CAPA.
    38  func (zin *zonesInput) GatherZonesFromMetadata(ctx context.Context) (err error) {
    39  	zin.ZonesInRegion, err = zin.InstallConfig.AWS.AvailabilityZones(ctx)
    40  	if err != nil {
    41  		return fmt.Errorf("failed to get availability zones: %w", err)
    42  	}
    43  	return nil
    44  }
    45  
    46  // GatherSubnetsFromMetadata retrieves subnets from AWS API to be used
    47  // when building the subnets to CAPA.
    48  func (zin *zonesInput) GatherSubnetsFromMetadata(ctx context.Context) (err error) {
    49  	zin.Subnets = &subnetsInput{}
    50  	if zin.Subnets.privateSubnets, err = zin.InstallConfig.AWS.PrivateSubnets(ctx); err != nil {
    51  		return fmt.Errorf("failed to get private subnets: %w", err)
    52  	}
    53  	if zin.Subnets.publicSubnets, err = zin.InstallConfig.AWS.PublicSubnets(ctx); err != nil {
    54  		return fmt.Errorf("failed to get public subnets: %w", err)
    55  	}
    56  	if zin.Subnets.edgeSubnets, err = zin.InstallConfig.AWS.EdgeSubnets(ctx); err != nil {
    57  		return fmt.Errorf("failed to get edge subnets: %w", err)
    58  	}
    59  	if zin.Subnets.vpc, err = zin.InstallConfig.AWS.VPC(ctx); err != nil {
    60  		return fmt.Errorf("failed to get VPC: %w", err)
    61  	}
    62  	return nil
    63  }
    64  
    65  // ZonesCAPI handles the discovered zones used to create subnets to CAPA.
    66  // ZonesCAPI is scoped in this package, but exported to use complex scenarios
    67  // with go-cmp on unit tests.
    68  type ZonesCAPI struct {
    69  	ControlPlaneZones sets.Set[string]
    70  	ComputeZones      sets.Set[string]
    71  	EdgeZones         sets.Set[string]
    72  }
    73  
    74  // GetAvailabilityZones returns a sorted union of Availability Zones defined
    75  // in the zone attribute in the pools for control plane and compute.
    76  func (zo *ZonesCAPI) GetAvailabilityZones() []string {
    77  	return sets.List(zo.ControlPlaneZones.Union(zo.ComputeZones))
    78  }
    79  
    80  // GetEdgeZones returns a sorted union of Local Zones or Wavelength Zones
    81  // defined in the zone attribute in the edge compute pool.
    82  func (zo *ZonesCAPI) GetEdgeZones() []string {
    83  	return sets.List(zo.EdgeZones)
    84  }
    85  
    86  // SetAvailabilityZones insert the zone to the given compute pool, and to
    87  // the regular zone (zone type availability-zone) list.
    88  func (zo *ZonesCAPI) SetAvailabilityZones(pool string, zones []string) {
    89  	switch pool {
    90  	case types.MachinePoolControlPlaneRoleName:
    91  		zo.ControlPlaneZones.Insert(zones...)
    92  
    93  	case types.MachinePoolComputeRoleName:
    94  		zo.ComputeZones.Insert(zones...)
    95  	}
    96  }
    97  
    98  // SetDefaultConfigZones evaluates if machine pools (control plane and workers) have been
    99  // set the zones from install-config.yaml, if not sets the default from platform, when exists,
   100  // otherwise set the default from the region discovered from AWS API.
   101  func (zo *ZonesCAPI) SetDefaultConfigZones(pool string, defConfig []string, defRegion []string) {
   102  	zones := []string{}
   103  	switch pool {
   104  	case types.MachinePoolControlPlaneRoleName:
   105  		if len(zo.ControlPlaneZones) == 0 && len(defConfig) > 0 {
   106  			zones = defConfig
   107  		} else if len(zo.ControlPlaneZones) == 0 {
   108  			zones = defRegion
   109  		}
   110  		zo.ControlPlaneZones.Insert(zones...)
   111  
   112  	case types.MachinePoolComputeRoleName:
   113  		if len(zo.ComputeZones) == 0 && len(defConfig) > 0 {
   114  			zones = defConfig
   115  		} else if len(zo.ComputeZones) == 0 {
   116  			zones = defRegion
   117  		}
   118  		zo.ComputeZones.Insert(zones...)
   119  	}
   120  }
   121  
   122  // setSubnets is the entrypoint to create the CAPI NetworkSpec structures
   123  // for managed or BYO VPC deployments from install-config.yaml.
   124  // The NetworkSpec.Subnets will be populated with the desired zones.
   125  func setSubnets(ctx context.Context, in *zonesInput) error {
   126  	if in.InstallConfig == nil {
   127  		return fmt.Errorf("failed to get installConfig")
   128  	}
   129  	if in.InstallConfig.AWS == nil {
   130  		return fmt.Errorf("failed to get AWS metadata")
   131  	}
   132  	if in.InstallConfig.Config == nil {
   133  		return fmt.Errorf("unable to get Config")
   134  	}
   135  	if in.Cluster == nil {
   136  		return fmt.Errorf("failed to get AWSCluster config")
   137  	}
   138  
   139  	// BYO VPC ("unmanaged") deployments
   140  	if len(in.InstallConfig.Config.AWS.Subnets) > 0 {
   141  		if err := in.GatherSubnetsFromMetadata(ctx); err != nil {
   142  			return fmt.Errorf("failed to get subnets from metadata: %w", err)
   143  		}
   144  		return setSubnetsBYOVPC(in)
   145  	}
   146  
   147  	// Managed VPC (fully automated) deployments
   148  	if err := in.GatherZonesFromMetadata(ctx); err != nil {
   149  		return fmt.Errorf("failed to get availability zones from metadata: %w", err)
   150  	}
   151  	return setSubnetsManagedVPC(in)
   152  }
   153  
   154  // setSubnetsBYOVPC creates the CAPI NetworkSpec.Subnets setting the
   155  // desired subnets from install-config.yaml in the BYO VPC deployment.
   156  // This function does not provide support for unit test to mock for AWS API,
   157  // so all API calls must be done prior this execution.
   158  // TODO: create support to mock AWS API calls in the unit tests, then the method
   159  // GatherSubnetsFromMetadata() can be added in setSubnetsBYOVPC.
   160  func setSubnetsBYOVPC(in *zonesInput) error {
   161  	in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{
   162  		ID: in.Subnets.vpc,
   163  	}
   164  	for _, subnet := range in.Subnets.privateSubnets {
   165  		in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
   166  			ID:               subnet.ID,
   167  			CidrBlock:        subnet.CIDR,
   168  			AvailabilityZone: subnet.Zone.Name,
   169  			IsPublic:         subnet.Public,
   170  		})
   171  	}
   172  
   173  	for _, subnet := range in.Subnets.publicSubnets {
   174  		in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
   175  			ID:               subnet.ID,
   176  			CidrBlock:        subnet.CIDR,
   177  			AvailabilityZone: subnet.Zone.Name,
   178  			IsPublic:         subnet.Public,
   179  		})
   180  	}
   181  
   182  	// edgeSubnets are subnet created on AWS Local Zones or Wavelength Zone,
   183  	// discovered by ID and zone-type attribute.
   184  	for _, subnet := range in.Subnets.edgeSubnets {
   185  		in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
   186  			ID:               subnet.ID,
   187  			CidrBlock:        subnet.CIDR,
   188  			AvailabilityZone: subnet.Zone.Name,
   189  			IsPublic:         subnet.Public,
   190  		})
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  // setSubnetsManagedVPC creates the CAPI NetworkSpec.VPC and the NetworkSpec.Subnets,
   197  // setting the desired zones from install-config.yaml in the managed
   198  // VPC deployment, when specified, otherwise default zones are set from
   199  // the AWS API, previously discovered.
   200  // The CIDR blocks are calculated leaving free blocks to allow future expansions,
   201  // in Day-2, when desired.
   202  // This function does not have mock for AWS API, so all API calls must be added prior
   203  // this execution.
   204  // TODO: create support to mock AWS API calls in the unit tests, then the method
   205  // GatherZonesFromMetadata() can be added in setSubnetsManagedVPC.
   206  func setSubnetsManagedVPC(in *zonesInput) error {
   207  	out, err := extractZonesFromInstallConfig(in)
   208  	if err != nil {
   209  		return fmt.Errorf("failed to get availability zones: %w", err)
   210  	}
   211  
   212  	isPublishingExternal := in.InstallConfig.Config.Publish == types.ExternalPublishingStrategy
   213  	allAvailabilityZones := out.GetAvailabilityZones()
   214  	allEdgeZones := out.GetEdgeZones()
   215  
   216  	mainCIDR := capiutils.CIDRFromInstallConfig(in.InstallConfig)
   217  	in.Cluster.Spec.NetworkSpec.VPC = capa.VPCSpec{
   218  		CidrBlock: mainCIDR.String(),
   219  	}
   220  
   221  	// Base subnets count considering only private zones, leaving one free block to allow
   222  	// future subnet expansions in Day-2.
   223  	numSubnets := len(allAvailabilityZones) + 1
   224  
   225  	// Public subnets consumes one range from private CIDR block.
   226  	if isPublishingExternal {
   227  		numSubnets++
   228  	}
   229  
   230  	// Edge subnets consumes one CIDR block from private CIDR, slicing it
   231  	// into smaller depending on the amount edge zones added to install config.
   232  	if len(allEdgeZones) > 0 {
   233  		numSubnets++
   234  	}
   235  
   236  	privateCIDRs, err := utilscidr.SplitIntoSubnetsIPv4(mainCIDR.String(), numSubnets)
   237  	if err != nil {
   238  		return fmt.Errorf("unable to generate CIDR blocks for all private subnets: %w", err)
   239  	}
   240  
   241  	publicCIDR := privateCIDRs[len(allAvailabilityZones)].String()
   242  
   243  	var edgeCIDR string
   244  	if len(allEdgeZones) > 0 {
   245  		edgeCIDR = privateCIDRs[len(allAvailabilityZones)+1].String()
   246  	}
   247  
   248  	var publicCIDRs []*net.IPNet
   249  	if isPublishingExternal {
   250  		// The last num(zones) blocks are dedicated to the public subnets.
   251  		publicCIDRs, err = utilscidr.SplitIntoSubnetsIPv4(publicCIDR, len(allAvailabilityZones))
   252  		if err != nil {
   253  			return fmt.Errorf("unable to generate CIDR blocks for all public subnets: %w", err)
   254  		}
   255  	}
   256  
   257  	// Create subnets from zone pools (control plane and compute) with type availability-zone.
   258  	if len(privateCIDRs) < len(allAvailabilityZones) {
   259  		return fmt.Errorf("unable to define CIDR blocks to all zones for private subnets")
   260  	}
   261  	if isPublishingExternal && len(publicCIDRs) < len(allAvailabilityZones) {
   262  		return fmt.Errorf("unable to define CIDR blocks to all zones for public subnets")
   263  	}
   264  
   265  	for idxCIDR, zone := range allAvailabilityZones {
   266  		in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
   267  			AvailabilityZone: zone,
   268  			CidrBlock:        privateCIDRs[idxCIDR].String(),
   269  			ID:               fmt.Sprintf("%s-subnet-private-%s", in.ClusterID.InfraID, zone),
   270  			IsPublic:         false,
   271  		})
   272  		if isPublishingExternal {
   273  			in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
   274  				AvailabilityZone: zone,
   275  				CidrBlock:        publicCIDRs[idxCIDR].String(),
   276  				ID:               fmt.Sprintf("%s-subnet-public-%s", in.ClusterID.InfraID, zone),
   277  				IsPublic:         true,
   278  			})
   279  		}
   280  	}
   281  
   282  	// no edge zones, nothing else to do
   283  	if len(allEdgeZones) == 0 {
   284  		return nil
   285  	}
   286  
   287  	// Create subnets from edge zone pool with type local-zone.
   288  
   289  	// Slice the main CIDR (edgeCIDR) into N*zones for privates subnets,
   290  	// and, when publish external, duplicate to create public subnets.
   291  	numEdgeSubnets := len(allEdgeZones)
   292  	if isPublishingExternal {
   293  		numEdgeSubnets *= 2
   294  	}
   295  
   296  	// Allow one CIDR block for future expansion.
   297  	numEdgeSubnets++
   298  
   299  	// Slice the edgeCIDR into the amount of desired subnets.
   300  	edgeCIDRs, err := utilscidr.SplitIntoSubnetsIPv4(edgeCIDR, numEdgeSubnets)
   301  	if err != nil {
   302  		return fmt.Errorf("unable to generate CIDR blocks for all edge subnets: %w", err)
   303  	}
   304  	if len(edgeCIDRs) < len(allEdgeZones) {
   305  		return fmt.Errorf("unable to define CIDR blocks to all edge zones for private subnets")
   306  	}
   307  	if isPublishingExternal && (len(edgeCIDRs) < (len(allEdgeZones) * 2)) {
   308  		return fmt.Errorf("unable to define CIDR blocks to all edge zones for public subnets")
   309  	}
   310  
   311  	// Create subnets from zone pool with type local-zone or wavelength-zone (edge zones)
   312  	for idxCIDR, zone := range allEdgeZones {
   313  		in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
   314  			AvailabilityZone: zone,
   315  			CidrBlock:        edgeCIDRs[idxCIDR].String(),
   316  			ID:               fmt.Sprintf("%s-subnet-private-%s", in.ClusterID.InfraID, zone),
   317  			IsPublic:         false,
   318  		})
   319  		if isPublishingExternal {
   320  			in.Cluster.Spec.NetworkSpec.Subnets = append(in.Cluster.Spec.NetworkSpec.Subnets, capa.SubnetSpec{
   321  				AvailabilityZone: zone,
   322  				CidrBlock:        edgeCIDRs[len(allEdgeZones)+idxCIDR].String(),
   323  				ID:               fmt.Sprintf("%s-subnet-public-%s", in.ClusterID.InfraID, zone),
   324  				IsPublic:         true,
   325  			})
   326  		}
   327  	}
   328  
   329  	return nil
   330  }
   331  
   332  // extractZonesFromInstallConfig extracts zones defined in the install-config.
   333  func extractZonesFromInstallConfig(in *zonesInput) (*ZonesCAPI, error) {
   334  	out := ZonesCAPI{
   335  		ControlPlaneZones: sets.New[string](),
   336  		ComputeZones:      sets.New[string](),
   337  		EdgeZones:         sets.New[string](),
   338  	}
   339  
   340  	cfg := in.InstallConfig.Config
   341  	defaultZones := []string{}
   342  	if cfg.AWS != nil && cfg.AWS.DefaultMachinePlatform != nil && len(cfg.AWS.DefaultMachinePlatform.Zones) > 0 {
   343  		defaultZones = cfg.AWS.DefaultMachinePlatform.Zones
   344  	}
   345  
   346  	if cfg.ControlPlane != nil && cfg.ControlPlane.Platform.AWS != nil {
   347  		out.SetAvailabilityZones(types.MachinePoolControlPlaneRoleName, cfg.ControlPlane.Platform.AWS.Zones)
   348  	}
   349  	out.SetDefaultConfigZones(types.MachinePoolControlPlaneRoleName, defaultZones, in.ZonesInRegion)
   350  
   351  	// set the zones in the compute/worker pool, when defined, otherwise use defaults.
   352  	for _, pool := range cfg.Compute {
   353  		if pool.Platform.AWS == nil {
   354  			continue
   355  		}
   356  		// edge compute pools should have zones defined.
   357  		if pool.Name == types.MachinePoolEdgeRoleName {
   358  			if len(pool.Platform.AWS.Zones) == 0 {
   359  				return nil, fmt.Errorf("expect one or more zones in the edge compute pool, got: %q", pool.Platform.AWS.Zones)
   360  			}
   361  			out.EdgeZones.Insert(pool.Platform.AWS.Zones...)
   362  			continue
   363  		}
   364  
   365  		if len(pool.Platform.AWS.Zones) > 0 {
   366  			out.SetAvailabilityZones(pool.Name, pool.Platform.AWS.Zones)
   367  		}
   368  		out.SetDefaultConfigZones(types.MachinePoolComputeRoleName, defaultZones, in.ZonesInRegion)
   369  	}
   370  
   371  	// set defaults for worker pool when not defined in config.
   372  	if len(out.ComputeZones) == 0 {
   373  		out.SetDefaultConfigZones(types.MachinePoolComputeRoleName, defaultZones, in.ZonesInRegion)
   374  	}
   375  
   376  	// should raise an error if no zones is available in the pools, default platform config, or metadata.
   377  	if azs := out.GetAvailabilityZones(); len(azs) == 0 {
   378  		return nil, fmt.Errorf("failed to set zones from config, got: %q", azs)
   379  	}
   380  
   381  	return &out, nil
   382  }