@@ -17,10 +17,13 @@ package gcloud
1717
1818import (
1919 "context"
20- "google.golang.org/api/option "
20+ "fmt "
2121 "net/http"
22+ "regexp"
2223 "time"
2324
25+ "google.golang.org/api/option"
26+
2427 "github.com/coreos/pkg/capnslog"
2528 "google.golang.org/api/compute/v1"
2629
@@ -50,6 +53,68 @@ type API struct {
5053 client * http.Client
5154 compute * compute.Service
5255 options * Options
56+ zones []string
57+ }
58+
59+ // This regex shuld match all standard (non-AI) zones
60+ // See: https://docs.cloud.google.com/compute/docs/regions-zones
61+ var standardZoneRegexp = regexp .MustCompile (`^([a-z]+-[a-z]+\d+)-[a-z]$` )
62+
63+ // zones are in the form "us-central1-a" and the region would be "us-central1"
64+ // See: https://docs.cloud.google.com/compute/docs/regions-zones
65+ func extractRegionFromZone (zone string ) (string , error ) {
66+ matches := standardZoneRegexp .FindStringSubmatch (zone )
67+ if matches == nil {
68+ return "" , fmt .Errorf ("zone %q does not match expected format {region}-{letter}" , zone )
69+ }
70+ return matches [1 ], nil
71+ }
72+
73+ func getAvailableZones (computeService * compute.Service , opts * Options ) ([]string , error ) {
74+ if opts .MachineType == "" {
75+ return []string {opts .Zone }, nil
76+ }
77+
78+ list , err := computeService .MachineTypes .AggregatedList (opts .Project ).
79+ Filter ("name=" + opts .MachineType ).Do ()
80+
81+ if err != nil {
82+ return nil , err
83+ }
84+
85+ targetRegion , err := extractRegionFromZone (opts .Zone )
86+ if err != nil {
87+ return nil , fmt .Errorf ("could not extract region from zone %q: %w" , opts .Zone , err )
88+ }
89+
90+ zones := []string {}
91+ for _ , scopedList := range list .Items {
92+ // There should be either 1 or 0 MachineTypes
93+ // 0 if this zone does not have the required machine type
94+ // 1 if this zone does have the required machine type
95+ if len (scopedList .MachineTypes ) == 0 {
96+ continue
97+ }
98+ if len (scopedList .MachineTypes ) > 1 {
99+ plog .Warningf ("Unexpected: got %d machine types for filter name=%s" , len (scopedList .MachineTypes ), opts .MachineType )
100+ continue
101+ }
102+ zone := scopedList .MachineTypes [0 ].Zone
103+ if region , err := extractRegionFromZone (zone ); err == nil && region == targetRegion {
104+ // If the preferred zone can be used, it should be the first zone that we use,
105+ // so we will make add it to the start of the list, rather than the end.
106+ if zone == opts .Zone {
107+ zones = append ([]string {zone }, zones ... )
108+ } else {
109+ zones = append (zones , zone )
110+ }
111+ }
112+ }
113+
114+ if len (zones ) == 0 {
115+ return zones , fmt .Errorf ("no zones in region %s for machine type %s were found" , targetRegion , opts .MachineType )
116+ }
117+ return zones , nil
53118}
54119
55120func New (opts * Options ) (* API , error ) {
@@ -83,6 +148,12 @@ func New(opts *Options) (*API, error) {
83148 return nil , err
84149 }
85150
151+ zones , err := getAvailableZones (computeService , opts )
152+ if err != nil {
153+ plog .Warningf ("Failed to discover available zones: %v. Falling back to preferred zone (%s) only." , err , opts .Zone )
154+ zones = []string {opts .Zone }
155+ }
156+
86157 if opts .ServiceAcct == "" {
87158 proj , err := computeService .Projects .Get (opts .Project ).Do ()
88159 if err != nil {
@@ -95,6 +166,7 @@ func New(opts *Options) (*API, error) {
95166 client : client ,
96167 compute : computeService ,
97168 options : opts ,
169+ zones : zones ,
98170 }
99171
100172 return api , nil
0 commit comments