github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/common/availabilityzones.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package common
     5  
     6  import (
     7  	"sort"
     8  
     9  	"github.com/juju/juju/core/instance"
    10  	"github.com/juju/juju/core/network"
    11  	"github.com/juju/juju/environs"
    12  	"github.com/juju/juju/environs/context"
    13  )
    14  
    15  // ZonedEnviron is an environs.Environ that has support for availability zones.
    16  //
    17  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/zoned_environ.go github.com/juju/juju/provider/common ZonedEnviron
    18  type ZonedEnviron interface {
    19  	environs.Environ
    20  
    21  	// AvailabilityZones returns all availability zones in the environment.
    22  	AvailabilityZones(ctx context.ProviderCallContext) (network.AvailabilityZones, error)
    23  
    24  	// InstanceAvailabilityZoneNames returns the names of the availability
    25  	// zones for the specified instances. The error returned follows the same
    26  	// rules as Environ.Instances.
    27  	InstanceAvailabilityZoneNames(ctx context.ProviderCallContext, ids []instance.Id) (map[instance.Id]string, error)
    28  
    29  	// DeriveAvailabilityZones attempts to derive availability zones from
    30  	// the specified StartInstanceParams.
    31  	//
    32  	// The parameters for starting an instance may imply (or explicitly
    33  	// specify) availability zones, e.g. due to placement, or due to the
    34  	// attachment of existing volumes, or due to subnet placement. If
    35  	// there is no such restriction, then DeriveAvailabilityZones should
    36  	// return an empty string slice to indicate that the caller should
    37  	// choose an availability zone.
    38  	DeriveAvailabilityZones(ctx context.ProviderCallContext, args environs.StartInstanceParams) ([]string, error)
    39  }
    40  
    41  // AvailabilityZoneInstances describes an availability zone and
    42  // a set of instances in that zone.
    43  type AvailabilityZoneInstances struct {
    44  	// ZoneName is the name of the availability zone.
    45  	ZoneName string
    46  
    47  	// Instances is a set of instances within the availability zone.
    48  	Instances []instance.Id
    49  }
    50  
    51  type byPopulationThenName []AvailabilityZoneInstances
    52  
    53  func (b byPopulationThenName) Len() int {
    54  	return len(b)
    55  }
    56  
    57  func (b byPopulationThenName) Less(i, j int) bool {
    58  	switch {
    59  	case len(b[i].Instances) < len(b[j].Instances):
    60  		return true
    61  	case len(b[i].Instances) == len(b[j].Instances):
    62  		return b[i].ZoneName < b[j].ZoneName
    63  	}
    64  	return false
    65  }
    66  
    67  func (b byPopulationThenName) Swap(i, j int) {
    68  	b[i], b[j] = b[j], b[i]
    69  }
    70  
    71  // AvailabilityZoneAllocations returns the availability zones and their
    72  // instance allocations from the specified group, in ascending order of
    73  // population. Availability zones with the same population size are
    74  // ordered by name.
    75  //
    76  // If the specified group is empty, then it will behave as if the result of
    77  // AllRunningInstances were provided.
    78  func AvailabilityZoneAllocations(
    79  	env ZonedEnviron, ctx context.ProviderCallContext, group []instance.Id,
    80  ) ([]AvailabilityZoneInstances, error) {
    81  	if len(group) == 0 {
    82  		instances, err := env.AllRunningInstances(ctx)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		group = make([]instance.Id, len(instances))
    87  		for i, inst := range instances {
    88  			group[i] = inst.Id()
    89  		}
    90  	}
    91  	instanceZones, err := env.InstanceAvailabilityZoneNames(ctx, group)
    92  	switch err {
    93  	case nil, environs.ErrPartialInstances:
    94  	case environs.ErrNoInstances:
    95  		group = nil
    96  	default:
    97  		return nil, err
    98  	}
    99  
   100  	// Get the list of all "available" availability zones,
   101  	// and then initialise a tally for each one.
   102  	zones, err := env.AvailabilityZones(ctx)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	instancesByZoneName := make(map[string][]instance.Id)
   107  	for _, zone := range zones {
   108  		if !zone.Available() {
   109  			continue
   110  		}
   111  		name := zone.Name()
   112  		instancesByZoneName[name] = nil
   113  	}
   114  	if len(instancesByZoneName) == 0 {
   115  		return nil, nil
   116  	}
   117  
   118  	for _, id := range group {
   119  		zone := instanceZones[id]
   120  		if zone == "" {
   121  			continue
   122  		}
   123  		if _, ok := instancesByZoneName[zone]; !ok {
   124  			// zone is not available
   125  			continue
   126  		}
   127  		instancesByZoneName[zone] = append(instancesByZoneName[zone], id)
   128  	}
   129  
   130  	zoneInstances := make([]AvailabilityZoneInstances, 0, len(instancesByZoneName))
   131  	for zoneName, instances := range instancesByZoneName {
   132  		zoneInstances = append(zoneInstances, AvailabilityZoneInstances{
   133  			ZoneName:  zoneName,
   134  			Instances: instances,
   135  		})
   136  	}
   137  	sort.Sort(byPopulationThenName(zoneInstances))
   138  	return zoneInstances, nil
   139  }
   140  
   141  var internalAvailabilityZoneAllocations = AvailabilityZoneAllocations
   142  
   143  // DistributeInstances is a common function for implement the
   144  // state.InstanceDistributor policy based on availability zone spread.
   145  // TODO (manadart 2018-11-27) This method signature has grown to the point
   146  // where the argument list should be replaced with a struct.
   147  // At that time limitZones could be transformed to a map so that lookups in the
   148  // filtering below are more efficient.
   149  func DistributeInstances(
   150  	env ZonedEnviron, ctx context.ProviderCallContext, candidates, group []instance.Id, limitZones []string,
   151  ) ([]instance.Id, error) {
   152  	// Determine availability zone distribution for the group.
   153  	zoneInstances, err := internalAvailabilityZoneAllocations(env, ctx, group)
   154  	if err != nil || len(zoneInstances) == 0 {
   155  		return nil, err
   156  	}
   157  
   158  	// If there are any zones supplied for limitation,
   159  	// filter to distribution data so that only those zones are considered.
   160  	filteredZoneInstances := zoneInstances[:0]
   161  	if len(limitZones) > 0 {
   162  		for _, zi := range zoneInstances {
   163  			for _, zone := range limitZones {
   164  				if zi.ZoneName == zone {
   165  					filteredZoneInstances = append(filteredZoneInstances, zi)
   166  					break
   167  				}
   168  			}
   169  		}
   170  	} else {
   171  		filteredZoneInstances = zoneInstances
   172  	}
   173  
   174  	// Determine which of the candidates are eligible based on whether
   175  	// they are allocated in one of the least-populated availability zones.
   176  	var allEligible []string
   177  	for i := range filteredZoneInstances {
   178  		if i > 0 && len(filteredZoneInstances[i].Instances) > len(filteredZoneInstances[i-1].Instances) {
   179  			break
   180  		}
   181  		for _, id := range filteredZoneInstances[i].Instances {
   182  			allEligible = append(allEligible, string(id))
   183  		}
   184  	}
   185  	sort.Strings(allEligible)
   186  
   187  	eligible := make([]instance.Id, 0, len(candidates))
   188  	for _, candidate := range candidates {
   189  		n := sort.SearchStrings(allEligible, string(candidate))
   190  		if n >= 0 && n < len(allEligible) {
   191  			eligible = append(eligible, candidate)
   192  		}
   193  	}
   194  	return eligible, nil
   195  }