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 }