cmd/browser: implement selection of Compute Zone and Bucket Location

Tested that this works, end to end.

Change-Id: Ice0c077d7af197d086fc2322206163e961af8aa8
Reviewed-on: https://upspin-review.googlesource.com/12300
Reviewed-by: Rob Pike <r@golang.org>
diff --git a/cmd/browser/gcp.go b/cmd/browser/gcp.go
index d4c3fd7..bda050e 100644
--- a/cmd/browser/gcp.go
+++ b/cmd/browser/gcp.go
@@ -16,6 +16,7 @@
 	"io/ioutil"
 	"net/http"
 	"path/filepath"
+	"sort"
 	"strings"
 	"time"
 
@@ -190,12 +191,63 @@
 	return err
 }
 
+func (s *gcpState) listZones() ([]string, error) {
+	client := s.JWTConfig.Client(context.Background())
+	svc, err := compute.New(client)
+	if err != nil {
+		return nil, err
+	}
+
+	list, err := svc.Regions.List(s.ProjectID).Do()
+	if err != nil {
+		return nil, err
+	}
+	var zones []string
+	for _, region := range list.Items {
+		if region.Status == "DOWN" {
+			continue
+		}
+		for _, z := range region.Zones {
+			i := strings.LastIndex(z, "/")
+			if i < 0 {
+				continue
+			}
+			zones = append(zones, region.Name+z[i:])
+		}
+	}
+	sort.Strings(zones)
+	return zones, nil
+}
+
+func (s *gcpState) listStorageLocations() ([]string, error) {
+	// There's no API for this. Scraped from:
+	// https://cloud.google.com/storage/docs/bucket-locations
+	return []string{
+		// Multi-regional locations.
+		"asia",
+		"eu",
+		"us",
+		// Regional locations.
+		"asia-east1",
+		"asia-northeast1",
+		"asia-southeast1",
+		"australia-southeast1",
+		"europe-west1",
+		"europe-west2",
+		"europe-west3",
+		"us-central1",
+		"us-east1",
+		"us-east4",
+		"us-west1",
+	}, nil
+}
+
 // create creates a Storage bucket with the given name, a service account to
 // access the bucket, and a Compute instance running on a static IP address.
-func (s *gcpState) create(bucketName string) error {
+func (s *gcpState) create(region, zone, bucketName, bucketLoc string) error {
+	s.Region = region
+	s.Zone = zone
 	// TODO: make these user-configurable.
-	s.Region = "us-central1"
-	s.Zone = "us-central1-a"
 	s.MachineType = "n1-standard-1"
 
 	if s.Storage.ServiceAccount == "" {
@@ -205,19 +257,19 @@
 		}
 		s.Storage.ServiceAccount = email
 		s.Storage.PrivateKeyData = key
-	}
-	if err := s.save(); err != nil {
-		return err
+		if err := s.save(); err != nil {
+			return err
+		}
 	}
 	if s.Storage.Bucket == "" {
-		err := s.createBucket(bucketName)
+		err := s.createBucket(bucketName, bucketLoc)
 		if err != nil {
 			return err
 		}
 		s.Storage.Bucket = bucketName
-	}
-	if err := s.save(); err != nil {
-		return err
+		if err := s.save(); err != nil {
+			return err
+		}
 	}
 	if s.Server.IPAddr == "" {
 		ip, err := s.createAddress()
@@ -225,9 +277,9 @@
 			return err
 		}
 		s.Server.IPAddr = ip
-	}
-	if err := s.save(); err != nil {
-		return err
+		if err := s.save(); err != nil {
+			return err
+		}
 	}
 	if !s.Server.Created {
 		err := s.createInstance()
@@ -235,8 +287,11 @@
 			return err
 		}
 		s.Server.Created = true
+		if err := s.save(); err != nil {
+			return err
+		}
 	}
-	return s.save()
+	return nil
 }
 
 // createAddress reserves a static IP address with the name "upspinserver".
@@ -368,7 +423,7 @@
 
 // createBucket creates the named Storage bucket, giving "owner" access to
 // Storage.ServiceAccount in gcpState.
-func (s *gcpState) createBucket(bucket string) error {
+func (s *gcpState) createBucket(name, loc string) error {
 	client := s.JWTConfig.Client(context.Background())
 	svc, err := storage.New(client)
 	if err != nil {
@@ -377,13 +432,13 @@
 
 	_, err = svc.Buckets.Insert(s.ProjectID, &storage.Bucket{
 		Acl: []*storage.BucketAccessControl{{
-			Bucket: bucket,
+			Bucket: name,
 			Entity: "user-" + s.Storage.ServiceAccount,
 			Email:  s.Storage.ServiceAccount,
 			Role:   "OWNER",
 		}},
-		Name: bucket,
-		// TODO(adg): region
+		Name:     name,
+		Location: loc,
 	}).Do()
 	if isExists(err) {
 		// Bucket already exists.
diff --git a/cmd/browser/startup.go b/cmd/browser/startup.go
index 6680f9d..cd3da1b 100644
--- a/cmd/browser/startup.go
+++ b/cmd/browser/startup.go
@@ -57,7 +57,9 @@
 
 	// Step: "gcpDetails"
 	BucketName string
-	// TODO: region, zone, instance size
+	Zones      []string
+	Locations  []string
+	// TODO: machine type
 
 	// Step: "serverUserName"
 	UserNamePrefix string // Includes trailing "+".
@@ -269,9 +271,16 @@
 
 	case "createGCP":
 		bucketName := req.FormValue("bucketName")
+		bucketLoc := req.FormValue("bucketLoc")
+		regionZone := req.FormValue("regionZone")
+		p := strings.SplitN(regionZone, "/", 2)
+		if len(p) != 2 {
+			return nil, nil, errors.Errorf("invalid region/zone %q", regionZone)
+		}
+		region, zone := p[0], p[1]
 
 		// Create the bucket, VM instance, and other associated bits.
-		if err := st.create(bucketName); err != nil {
+		if err := st.create(region, zone, bucketName, bucketLoc); err != nil {
 			return nil, nil, err
 		}
 
@@ -472,9 +481,20 @@
 		bucketName := st.ProjectID + "-upspin"
 		// TODO: check bucketName is available
 
+		zones, err := st.listZones()
+		if err != nil {
+			return nil, nil, err
+		}
+		locs, err := st.listStorageLocations()
+		if err != nil {
+			return nil, nil, err
+		}
+
 		return &startupResponse{
 			Step:       "gcpDetails",
 			BucketName: bucketName,
+			Zones:      zones,
+			Locations:  locs,
 		}, nil, nil
 
 	case "serverUserName":
diff --git a/cmd/browser/static/index.html b/cmd/browser/static/index.html
index 0de0800..b023720 100644
--- a/cmd/browser/static/index.html
+++ b/cmd/browser/static/index.html
@@ -380,14 +380,37 @@
       </div>
       <div class="modal-body">
 		<p>
-		GCP particulars...
+		TODO: explanation
 		</p>
 		<div class="form-group">
 			<label for="gcpDetailsBucketName">Storage Bucket Name</label>
 			<input type="text" class="form-control" id="gcpDetailsBucketName" placeholder="Bucket name">
+			<p class="help-block">
+			This name must be globally unique, so we suggest using your Project ID as a prefix.
+			</p>
+		</div>
+		<div class="form-group">
+			<label for="gcpDetailsBucketLoc">Storage Bucket Location</label>
+			<select id="gcpDetailsBucketLoc" class="form-control">
+			</select>
+			<p class="help-block">
+			See the
+			<a href="https://cloud.google.com/storage/docs/bucket-locations" target="_blank">documentation</a>
+			to learn about Bucket Locations.
+			</p>
+		</div>
+		<div class="form-group">
+			<label for="gcpDetailsRegionZone">Compute Zone</label>
+			<select id="gcpDetailsRegionZone" class="form-control">
+			</select>
+			<p class="help-block">
+			See the
+			<a href="https://cloud.google.com/compute/docs/regions-zones/regions-zones" target="_blank">documentation</a>
+			to learn about Regions and Zones.
+			</p>
 		</div>
 		<p>
-		TODO: Storage region, Compute zone
+		TODO: machine type
 		</p>
 		<div class="panel panel-danger up-error">
 			<div class="panel-heading">Error</div>
diff --git a/cmd/browser/static/script.js b/cmd/browser/static/script.js
index ee77a39..a11b6e8 100644
--- a/cmd/browser/static/script.js
+++ b/cmd/browser/static/script.js
@@ -384,7 +384,9 @@
 	$("#mGCPDetails").find("button").click(function() {
 		action({
 			action: "createGCP",
-			bucketName: $("#gcpDetailsBucketName").val()
+			bucketName: $("#gcpDetailsBucketName").val(),
+			bucketLoc: $("#gcpDetailsBucketLoc").val(),
+			regionZone: $("#gcpDetailsRegionZone").val()
 		});
 	});
 
@@ -403,7 +405,10 @@
 	});
 
 	$("#mServerSecretSeed").find("button").click(function() {
-		show({Step: "serverHostName"});
+		// Performing an empty action will bounce the user to the next
+		// screen, serverHostName, with the server IP address populated
+		// by the server side.
+		action({});
 	});
 
 	$("#mServerWriters").find("button").click(function() {
@@ -449,7 +454,36 @@
 			break;
 		case "gcpDetails":
 			el = $("#mGCPDetails");
+
 			$("#gcpDetailsBucketName").val(data.BucketName);
+
+			var locs = $("#gcpDetailsBucketLoc").empty();
+			for (var i=0; i < data.Locations.length; i++) {
+				var loc = data.Locations[i];
+				var label = loc;
+				if (loc.indexOf("-") >= 0) {
+					label += " (Regional)";
+				} else {
+					label += " (Multi-regional)";
+				}
+				var opt = $("<option/>").attr("value", loc).text(label);
+				if (loc == "us") {
+					opt.attr("selected", true); // A sane default.
+				}
+				locs.append(opt);
+			}
+
+			var zones = $("#gcpDetailsRegionZone").empty();
+			for (var i=0; i < data.Zones.length; i++) {
+				var zone = data.Zones[i];
+				var label = zone.slice(zone.indexOf("/")+1);
+				var opt = $("<option/>").attr("value", zone).text(label);
+				if (zone == "us-central1/us-central1-c") {
+					opt.attr("selected", true); // As sane default.
+				}
+				zones.append(opt);
+			}
+
 			break;
 		case "serverUserName":
 			el = $("#mServerUserName");
@@ -474,7 +508,7 @@
 		step = data.Step;
 
 		// Re-enable buttons, hide old errors, show the dialog.
-		el.find("button, input").prop("disabled", false);
+		el.find("button, input, select").prop("disabled", false);
 		el.find(".up-error").hide();
 		el.modal("show");
 	}