all: consolidate configuration for coordinator and gomote

buildenv.Environment type defines configuration options:

- Coordinator uses the GCE project name to lookup config. A custom
  config name can be provided at runtime to override.

- The conventional prod and stage project names ('symbolic-datum-552'
  and 'go-dashboard-dev') map to prod and staging configuration structs.

- Production and staging status is explicitly defined in configuration.

- GCS bucket names for buildlet, logs, and snapshots are
  configurable.

Change-Id: I7e6d7874eb0bdfe35dbdd5fcf6212ab50d576b88
Reviewed-on: https://go-review.googlesource.com/19502
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/buildenv/envs.go b/buildenv/envs.go
index b6b664e..48c446a 100644
--- a/buildenv/envs.go
+++ b/buildenv/envs.go
@@ -6,6 +6,11 @@
 // environments the Go build system can run in.
 package buildenv
 
+import (
+	"fmt"
+	"strings"
+)
+
 const (
 	prefix = "https://www.googleapis.com/compute/v1/projects/"
 )
@@ -18,6 +23,12 @@
 	// This field may be overridden as necessary without impacting other fields.
 	ProjectName string
 
+	// The IsProd flag indicates whether production functionality should be
+	// enabled. When true, GCE and Kubernetes builders are enabled and the
+	// coordinator serves on 443. Otherwise, GCE and Kubernetes builders are
+	// disabled and the coordinator serves on 8119.
+	IsProd bool
+
 	// Zone is the GCE zone that the coordinator instance and Kubernetes cluster
 	// will run in. This field may be overridden as necessary without impacting
 	// other fields.
@@ -60,24 +71,59 @@
 
 	// CoordinatorName is the hostname of the coordinator instance.
 	CoordinatorName string
+
+	// BuildletBucket is the GCS bucket that stores buildlet binaries.
+	BuildletBucket string
+
+	// The GCS bucket that logs are written to.
+	LogBucket string
+
+	// The GCS bucket that snapshots are written to.
+	SnapBucket string
 }
 
-// This method returns the URI for the environment's Machine Type.
+// MachineTypeURI returns the URI for the environment's Machine Type.
 func (e Environment) MachineTypeURI() string {
 	return e.ComputePrefix() + "/zones/" + e.Zone + "/machineTypes/" + e.MachineType
 }
 
-// This method returns the URI prefix for Compute Engine resources in a project.
+// ComputePrefix returns the URI prefix for Compute Engine resources in a project.
 func (e Environment) ComputePrefix() string {
 	return prefix + e.ProjectName
 }
 
+// Region returns the GCE region, derived from its zone.
+func (e Environment) Region() string {
+	return e.Zone[:strings.LastIndex(e.Zone, "-")]
+}
+
+// ByProjectID returns an Environment for the specified
+// project ID. It is currently limited to the symbolic-datum-552
+// and go-dashboard-dev projects.
+// ByProjectID will panic if the project ID is not known.
+func ByProjectID(projectID string) *Environment {
+	var envKeys []string
+
+	for k := range possibleEnvs {
+		envKeys = append(envKeys, k)
+	}
+
+	var env *Environment
+	env, ok := possibleEnvs[projectID]
+	if !ok {
+		panic(fmt.Sprintf("Can't get buildenv for unknown project %q. Possible envs are %s", projectID, envKeys))
+	}
+
+	return env
+}
+
 // Staging defines the environment that the coordinator and build
 // infrastructure is deployed to before it is released to production.
 // For local dev, override the project with the program's flag to set
 // a custom project.
 var Staging = &Environment{
 	ProjectName:     "go-dashboard-dev",
+	IsProd:          true,
 	Zone:            "us-central1-f",
 	ZonesToClean:    []string{"us-central1-a", "us-central1-b", "us-central1-f"},
 	StaticIP:        "104.154.113.235",
@@ -85,15 +131,19 @@
 	KubeMinNodes:    1,
 	KubeMaxNodes:    5,
 	KubeName:        "buildlets",
-	KubeMachineType: "n1-standard-8",
+	KubeMachineType: "n1-standard-32",
 	CoordinatorURL:  "https://storage.googleapis.com/dev-go-builder-data/coordinator",
 	CoordinatorName: "farmer",
+	BuildletBucket:  "dev-go-builder-data",
+	LogBucket:       "dev-go-build-log",
+	SnapBucket:      "dev-go-build-snap",
 }
 
 // Production defines the environment that the coordinator and build
 // infrastructure is deployed to for production usage at build.golang.org.
 var Production = &Environment{
 	ProjectName:     "symbolic-datum-552",
+	IsProd:          true,
 	Zone:            "us-central1-f",
 	ZonesToClean:    []string{"us-central1-f"},
 	StaticIP:        "107.178.219.46",
@@ -101,7 +151,16 @@
 	KubeMinNodes:    1,
 	KubeMaxNodes:    10,
 	KubeName:        "buildlets",
-	KubeMachineType: "n1-standard-8",
+	KubeMachineType: "n1-standard-32",
 	CoordinatorURL:  "https://storage.googleapis.com/go-builder-data/coordinator",
 	CoordinatorName: "farmer",
+	BuildletBucket:  "go-builder-data",
+	LogBucket:       "go-build-log",
+	SnapBucket:      "go-build-snap",
+}
+
+// possibleEnvs enumerate the known buildenv.Environment definitions.
+var possibleEnvs = map[string]*Environment{
+	"symbolic-datum-552": Production,
+	"go-dashboard-dev":   Staging,
 }
diff --git a/buildlet/gce.go b/buildlet/gce.go
index df5d166..be99551 100644
--- a/buildlet/gce.go
+++ b/buildlet/gce.go
@@ -13,6 +13,7 @@
 	"strings"
 	"time"
 
+	"golang.org/x/build/buildenv"
 	"golang.org/x/build/dashboard"
 	"golang.org/x/oauth2"
 	"google.golang.org/api/compute/v1"
@@ -158,7 +159,7 @@
 	// which the VMs are configured to download at boot and run.
 	// This lets us/ update the buildlet more easily than
 	// rebuilding the whole VM image.
-	addMeta("buildlet-binary-url", conf.BuildletBinaryURL())
+	addMeta("buildlet-binary-url", conf.BuildletBinaryURL(buildenv.ByProjectID(opts.ProjectID)))
 	addMeta("builder-type", builderType)
 	if !opts.TLS.IsZero() {
 		addMeta("tls-cert", opts.TLS.CertPEM)
diff --git a/buildlet/kube.go b/buildlet/kube.go
index 0a89d82..1d57351 100644
--- a/buildlet/kube.go
+++ b/buildlet/kube.go
@@ -12,6 +12,7 @@
 	"strings"
 	"time"
 
+	"golang.org/x/build/buildenv"
 	"golang.org/x/build/dashboard"
 	"golang.org/x/build/kubernetes"
 	"golang.org/x/build/kubernetes/api"
@@ -28,6 +29,9 @@
 
 // PodOpts control how new pods are started.
 type PodOpts struct {
+	// ProjectID is the GCE project ID. Required.
+	ProjectID string
+
 	// ImageRegistry specifies the Docker registry Kubernetes
 	// will use to create the pod. Required.
 	ImageRegistry string
@@ -122,7 +126,7 @@
 	// which the pods are configured to download at boot and run.
 	// This lets us/ update the buildlet more easily than
 	// rebuilding the whole pod image.
-	addEnv("META_BUILDLET_BINARY_URL", conf.BuildletBinaryURL())
+	addEnv("META_BUILDLET_BINARY_URL", conf.BuildletBinaryURL(buildenv.ByProjectID(opts.ProjectID)))
 	addEnv("META_BUILDER_TYPE", builderType)
 	if !opts.TLS.IsZero() {
 		addEnv("META_TLS_CERT", opts.TLS.CertPEM)
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 3140629..b71db145 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -62,28 +62,19 @@
 const devPause = false
 
 var (
-	role = flag.String("role", "coordinator", "Which role this binary should run as. Valid options: coordinator, watcher")
-
+	role          = flag.String("role", "coordinator", "Which role this binary should run as. Valid options: coordinator, watcher")
 	masterKeyFile = flag.String("masterkey", "", "Path to builder master key. Else fetched using GCE project attribute 'builder-master-key'.")
 
 	// TODO(bradfitz): remove this list and just query it from the compute API:
 	// http://godoc.org/google.golang.org/api/compute/v1#RegionsService.Get
 	// and Region.Zones: http://godoc.org/google.golang.org/api/compute/v1#Region
-	cleanZones = flag.String("zones", "us-central1-a,us-central1-b,us-central1-f", "Comma-separated list of zones to periodically clean of stale build VMs (ones that failed to shut themselves down)")
-
-	mode            = flag.String("mode", "", "valid modes are 'dev', 'prod', or '' for auto-detect. dev means localhost development, not be confused with staging on go-dashboard-dev, which is still the 'prod' mode.")
+	cleanZones      = flag.String("zones", "us-central1-a,us-central1-b,us-central1-f", "Comma-separated list of zones to periodically clean of stale build VMs (ones that failed to shut themselves down)")
+	mode            = flag.String("mode", "", "Valid modes are 'dev', 'prod', or '' for auto-detect. dev means localhost development, not be confused with staging on go-dashboard-dev, which is still the 'prod' mode.")
+	buildEnvName    = flag.String("env", "", "The build environment configuration to use. Not required if running on GCE, but will override GCE default config if set.")
 	devEnableGCE    = flag.Bool("dev_gce", false, "Whether or not to enable the GCE pool when in dev mode. The pool is enabled by default in prod mode.")
 	enableDebugProd = flag.Bool("debug_prod", false, "Enable the /dosomework URL to manually schedule a build on a prod coordinator. Enabled by default in dev mode.")
 )
 
-func buildLogBucket() string {
-	return stagingPrefix() + "go-build-log"
-}
-
-func snapBucket() string {
-	return stagingPrefix() + "go-build-snap"
-}
-
 // LOCK ORDER:
 //   statusMu, buildStatus.mu, trySet.mu
 // (Other locks, such as subrepoHead.Mutex or the remoteBuildlet mutex should
@@ -160,7 +151,7 @@
 		return []byte(b), nil
 	}
 
-	r, err := storageClient.Bucket(stagingPrefix() + "go-builder-data").Object(name).NewReader(serviceCtx)
+	r, err := storageClient.Bucket(buildEnv.BuildletBucket).Object(name).NewReader(serviceCtx)
 	if err != nil {
 		return nil, err
 	}
@@ -315,7 +306,6 @@
 		}
 
 		if inStaging {
-			dashboard.BuildletBucket = "dev-go-builder-data"
 			dashboard.Builders = stagingClusterBuilders()
 		}
 		initTryBuilders()
@@ -1031,7 +1021,7 @@
 		s1 := sha1.New()
 		io.WriteString(s1, buildLog)
 		objName := fmt.Sprintf("%s/%s_%x.log", bs.rev[:8], bs.name, s1.Sum(nil)[:4])
-		wr := storageClient.Bucket(buildLogBucket()).Object(objName).NewWriter(serviceCtx)
+		wr := storageClient.Bucket(buildEnv.LogBucket).Object(objName).NewWriter(serviceCtx)
 		wr.ContentType = "text/plain; charset=utf-8"
 		wr.ACL = append(wr.ACL, storage.ACLRule{Entity: storage.AllUsers, Role: storage.RoleReader})
 		if _, err := io.WriteString(wr, buildLog); err != nil {
@@ -1042,7 +1032,7 @@
 			log.Printf("Failed to write to GCS: %v", err)
 			return
 		}
-		failLogURL := fmt.Sprintf("https://storage.googleapis.com/%s/%s", buildLogBucket(), objName)
+		failLogURL := fmt.Sprintf("https://storage.googleapis.com/%s/%s", buildEnv.LogBucket, objName)
 
 		bs.mu.Lock()
 		bs.failURL = failLogURL
@@ -1669,7 +1659,7 @@
 
 // snapshotURL is the absolute URL of the snapshot object (see above).
 func (br *builderRev) snapshotURL() string {
-	return fmt.Sprintf("https://storage.googleapis.com/%s/%s", snapBucket(), br.snapshotObjectName())
+	return fmt.Sprintf("https://storage.googleapis.com/%s/%s", buildEnv.SnapBucket, br.snapshotObjectName())
 }
 
 func (st *buildStatus) writeSnapshot() error {
@@ -1682,7 +1672,7 @@
 	}
 	defer tgz.Close()
 
-	wr := storageClient.Bucket(snapBucket()).Object(st.snapshotObjectName()).NewWriter(serviceCtx)
+	wr := storageClient.Bucket(buildEnv.SnapBucket).Object(st.snapshotObjectName()).NewWriter(serviceCtx)
 	wr.ContentType = "application/octet-stream"
 	wr.ACL = append(wr.ACL, storage.ACLRule{Entity: storage.AllUsers, Role: storage.RoleReader})
 	if _, err := io.Copy(wr, tgz); err != nil {
diff --git a/cmd/coordinator/gce.go b/cmd/coordinator/gce.go
index 2e9213c..f9163bd 100644
--- a/cmd/coordinator/gce.go
+++ b/cmd/coordinator/gce.go
@@ -9,6 +9,7 @@
 
 import (
 	"crypto/rand"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -20,6 +21,7 @@
 	"sync"
 	"time"
 
+	"golang.org/x/build/buildenv"
 	"golang.org/x/build/buildlet"
 	"golang.org/x/build/dashboard"
 	"golang.org/x/build/gerrit"
@@ -48,16 +50,10 @@
 	<-apiCallTicker.C
 }
 
-const (
-	stagingProjectID   = "go-dashboard-dev"
-	stagingProjectZone = "us-central1-f"
-)
-
 // Initialized by initGCE:
 var (
-	projectID      string
-	projectZone    string
-	projectRegion  string
+	buildEnv *buildenv.Environment
+
 	computeService *compute.Service
 	externalIP     string
 	tokenSource    oauth2.TokenSource
@@ -70,57 +66,49 @@
 	initGCECalled bool
 )
 
-func stagingPrefix() string {
-	if !initGCECalled {
-		panic("stagingPrefix called before initGCE")
-	}
-	if inStaging {
-		return "dev-" // legacy prefix; must match resource names
-	}
-	return ""
-}
-
 func initGCE() error {
 	initGCECalled = true
 	var err error
-	// Use the staging project if not on GCE. This assumes the DefaultTokenSource
-	// credential used below has access to that project.
-	if !metadata.OnGCE() {
-		projectID = stagingProjectID
-		projectZone = stagingProjectZone
-	} else {
-		projectID, err = metadata.ProjectID()
-		if err != nil {
-			return fmt.Errorf("failed to get current GCE ProjectID: %v", err)
-		}
-		projectZone, err = metadata.Get("instance/zone")
+
+	// If the coordinator is running on a GCE instance and a
+	// buildEnv was not specified with the env flag, set the
+	// buildEnvName to the project ID
+	if metadata.OnGCE() && *buildEnvName == "" {
+		*buildEnvName, _ = metadata.ProjectID()
+	}
+
+	buildEnv := buildenv.ByProjectID(*buildEnvName)
+
+	// If running on GCE, override the zone and static IP, and check service account permissions.
+	if metadata.OnGCE() {
+		projectZone, err := metadata.Get("instance/zone")
 		if err != nil || projectZone == "" {
 			return fmt.Errorf("failed to get current GCE zone: %v", err)
 		}
 		// Convert the zone from "projects/1234/zones/us-central1-a" to "us-central1-a".
 		projectZone = path.Base(projectZone)
+		buildEnv.Zone = projectZone
+
+		buildEnv.StaticIP, err = metadata.ExternalIP()
+		if err != nil {
+			return fmt.Errorf("ExternalIP: %v", err)
+		}
+
 		if !hasComputeScope() {
 			return errors.New("The coordinator is not running with access to read and write Compute resources. VM support disabled.")
 
 		}
-		externalIP, err = metadata.ExternalIP()
-		if err != nil {
-			return fmt.Errorf("ExternalIP: %v", err)
-		}
 	}
 
-	inStaging = projectID == stagingProjectID
-	if inStaging {
-		log.Printf("Running in staging cluster (%q)", projectID)
-	}
-	projectRegion = projectZone[:strings.LastIndex(projectZone, "-")] // "us-central1"
+	cfgDump, _ := json.MarshalIndent(buildEnv, "", "  ")
+	log.Printf("Loaded configuration %q for project %q:\n%s", *buildEnvName, buildEnv.ProjectName, cfgDump)
 
 	tokenSource, err = google.DefaultTokenSource(oauth2.NoContext, compute.CloudPlatformScope, monitoring.MonitoringScope)
 	if err != nil {
 		log.Fatalf("failed to get a token source: %v", err)
 	}
 	httpClient := oauth2.NewClient(oauth2.NoContext, tokenSource)
-	serviceCtx = cloud.NewContext(projectID, httpClient)
+	serviceCtx = cloud.NewContext(buildEnv.ProjectName, httpClient)
 	storageClient, err = storage.NewClient(serviceCtx, cloud.WithBaseHTTP(httpClient))
 	if err != nil {
 		log.Fatalf("storage.NewClient: %v", err)
@@ -142,10 +130,10 @@
 	if !hasStorageScope() {
 		return errors.New("coordinator's GCE instance lacks the storage service scope")
 	}
-	wr := storageClient.Bucket(buildLogBucket()).Object("hello.txt").NewWriter(serviceCtx)
+	wr := storageClient.Bucket(buildEnv.LogBucket).Object("hello.txt").NewWriter(serviceCtx)
 	fmt.Fprintf(wr, "Hello, world! Coordinator start-up at %v", time.Now())
 	if err := wr.Close(); err != nil {
-		return fmt.Errorf("test write of a GCS object to bucket %q failed: %v", buildLogBucket(), err)
+		return fmt.Errorf("test write of a GCS object to bucket %q failed: %v", buildEnv.LogBucket, err)
 	}
 	if inStaging {
 		// Don't expect to write to Gerrit in staging mode.
@@ -194,9 +182,9 @@
 
 func (p *gceBuildletPool) pollQuota() {
 	gceAPIGate()
-	reg, err := computeService.Regions.Get(projectID, projectRegion).Do()
+	reg, err := computeService.Regions.Get(buildEnv.ProjectName, buildEnv.Region()).Do()
 	if err != nil {
-		log.Printf("Failed to get quota for %s/%s: %v", projectID, projectRegion, err)
+		log.Printf("Failed to get quota for %s/%s: %v", buildEnv.ProjectName, buildEnv.Region(), err)
 		return
 	}
 	p.mu.Lock()
@@ -253,8 +241,8 @@
 	el.logEventTime("creating_gce_instance", instName)
 	log.Printf("Creating GCE VM %q for %s at %s", instName, typ, rev)
 	bc, err := buildlet.StartNewVM(tokenSource, instName, typ, buildlet.VMOpts{
-		ProjectID:   projectID,
-		Zone:        projectZone,
+		ProjectID:   buildEnv.ProjectName,
+		Zone:        buildEnv.Zone,
 		Description: fmt.Sprintf("Go Builder for %s at %s", typ, rev),
 		DeleteIn:    deleteIn,
 		OnInstanceRequested: func() {
@@ -282,7 +270,7 @@
 		el.logEventTime("gce_buildlet_create_failure", fmt.Sprintf("%s: %v", instName, err))
 		log.Printf("Failed to create VM for %s, %s: %v", typ, rev, err)
 		if needDelete {
-			deleteVM(projectZone, instName)
+			deleteVM(buildEnv.Zone, instName)
 			p.putVMCountQuota(conf.GCENumCPU())
 		}
 		p.setInstanceUsed(instName, false)
@@ -305,7 +293,7 @@
 	// buildlet client library between Close, Destroy/Halt, and
 	// tracking execution errors.  That was all half-baked before
 	// and thus removed. Now Close always destroys everything.
-	deleteVM(projectZone, instName)
+	deleteVM(buildEnv.Zone, instName)
 	p.setInstanceUsed(instName, false)
 
 	conf, ok := dashboard.Builders[typ]
@@ -474,7 +462,7 @@
 	// TODO(bradfitz): revist this code if we ever start running
 	// thousands of VMs.
 	gceAPIGate()
-	list, err := computeService.Instances.List(projectID, zone).Do()
+	list, err := computeService.Instances.List(buildEnv.ProjectName, zone).Do()
 	if err != nil {
 		return fmt.Errorf("listing instances: %v", err)
 	}
@@ -523,7 +511,7 @@
 func deleteVM(zone, instName string) (operation string, err error) {
 	deletedVMCache.Add(instName, token{})
 	gceAPIGate()
-	op, err := computeService.Instances.Delete(projectID, zone, instName).Do()
+	op, err := computeService.Instances.Delete(buildEnv.ProjectName, zone, instName).Do()
 	apiErr, ok := err.(*googleapi.Error)
 	if ok {
 		if apiErr.Code == 404 {
diff --git a/cmd/coordinator/kube.go b/cmd/coordinator/kube.go
index 3dd1ec3..a913d13 100644
--- a/cmd/coordinator/kube.go
+++ b/cmd/coordinator/kube.go
@@ -60,7 +60,7 @@
 	initKubeCalled = true
 
 	// projectID was set by initGCE
-	registryPrefix += "/" + projectID
+	registryPrefix += "/" + buildEnv.ProjectName
 	if !hasCloudPlatformScope() {
 		return errors.New("coordinator not running with access to the Cloud Platform scope.")
 	}
@@ -79,9 +79,9 @@
 	tsService = monitoring.NewTimeseriesService(monService)
 	metricDescService = monitoring.NewMetricDescriptorsService(monService)
 
-	kubeCluster, err = containerService.Projects.Zones.Clusters.Get(projectID, projectZone, clusterName).Do()
+	kubeCluster, err = containerService.Projects.Zones.Clusters.Get(buildEnv.ProjectName, buildEnv.Zone, clusterName).Do()
 	if err != nil {
-		return fmt.Errorf("cluster %q could not be found in project %q, zone %q: %v", clusterName, projectID, projectZone, err)
+		return fmt.Errorf("cluster %q could not be found in project %q, zone %q: %v", clusterName, buildEnv.ProjectName, buildEnv.Zone, err)
 	}
 
 	// Decode certs
@@ -163,18 +163,18 @@
 			{Key: clusterNameLabelKey},
 			{Key: serviceLabelKey},
 		},
-		Project: projectID,
+		Project: buildEnv.ProjectName,
 		TypeDescriptor: &monitoring.MetricDescriptorTypeDescriptor{
 			MetricType: "gauge",
 			ValueType:  "double",
 		},
 	}
-	_, err := metricDescService.Create(projectID, metric).Do()
+	_, err := metricDescService.Create(buildEnv.ProjectName, metric).Do()
 	if err != nil {
 		if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 403 {
 			log.Printf("Error creating CPU metric: could not authenticate to Google Cloud Monitoring. If you are running the coordinator on a local machine in dev mode, configure service account credentials for authentication as described at https://cloud.google.com/monitoring/api/authentication#service_account_authorization. Error message: %v\n", err)
 		} else {
-			log.Fatalf("Failed to create CPU metric for project. Ensure the Google Cloud Monitoring API is enabled for project %v: %v.", projectID, err)
+			log.Fatalf("Failed to create CPU metric for project. Ensure the Google Cloud Monitoring API is enabled for project %v: %v.", buildEnv.ProjectName, err)
 		}
 	}
 
@@ -185,18 +185,18 @@
 			{Key: clusterNameLabelKey},
 			{Key: serviceLabelKey},
 		},
-		Project: projectID,
+		Project: buildEnv.ProjectName,
 		TypeDescriptor: &monitoring.MetricDescriptorTypeDescriptor{
 			MetricType: "gauge",
 			ValueType:  "double",
 		},
 	}
-	_, err = metricDescService.Create(projectID, metric).Do()
+	_, err = metricDescService.Create(buildEnv.ProjectName, metric).Do()
 	if err != nil {
 		if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 403 {
 			log.Printf("Error creating memory metric: could not authenticate to Google Cloud Monitoring. If you are running the coordinator on a local machine in dev mode, configure service account credentials for authentication as described at https://cloud.google.com/monitoring/api/authentication#service_account_authorization. Error message: %v\n", err)
 		} else {
-			log.Fatalf("Failed to create memory metric for project. Ensure the Google Cloud Monitoring API is enabled for project %v: %v.", projectID, err)
+			log.Fatalf("Failed to create memory metric for project. Ensure the Google Cloud Monitoring API is enabled for project %v: %v.", buildEnv.ProjectName, err)
 		}
 	}
 }
@@ -212,12 +212,12 @@
 func (p *kubeBuildletPool) pollCapacity(ctx context.Context) {
 	nodes, err := kubeClient.GetNodes(ctx)
 	if err != nil {
-		log.Printf("failed to retrieve nodes to calculate cluster capacity for %s/%s: %v", projectID, projectRegion, err)
+		log.Printf("failed to retrieve nodes to calculate cluster capacity for %s/%s: %v", buildEnv.ProjectName, buildEnv.Region(), err)
 		return
 	}
 	pods, err := kubeClient.GetPods(ctx)
 	if err != nil {
-		log.Printf("failed to retrieve pods to calculate cluster capacity for %s/%s: %v", projectID, projectRegion, err)
+		log.Printf("failed to retrieve pods to calculate cluster capacity for %s/%s: %v", buildEnv.ProjectName, buildEnv.Region(), err)
 		return
 	}
 
@@ -261,7 +261,7 @@
 				},
 				TimeseriesDesc: &monitoring.TimeseriesDescriptor{
 					Metric:  cpuUsedMetric,
-					Project: projectID,
+					Project: buildEnv.ProjectName,
 					Labels: map[string]string{
 						clusterNameLabelKey: clusterName,
 						serviceLabelKey:     "container",
@@ -276,7 +276,7 @@
 				},
 				TimeseriesDesc: &monitoring.TimeseriesDescriptor{
 					Metric:  memoryUsedMetric,
-					Project: projectID,
+					Project: buildEnv.ProjectName,
 					Labels: map[string]string{
 						clusterNameLabelKey: clusterName,
 						serviceLabelKey:     "container",
@@ -286,7 +286,7 @@
 		},
 	}
 
-	_, err = tsService.Write(projectID, &wtr).Do()
+	_, err = tsService.Write(buildEnv.ProjectName, &wtr).Do()
 	if err != nil {
 		log.Printf("custom cluster utilization metric could not be written to Google Cloud Monitoring: %v", err)
 	}
@@ -327,6 +327,7 @@
 	log.Printf("Creating Kubernetes pod %q for %s at %s", podName, typ, rev)
 
 	bc, err := buildlet.StartPod(ctx, kubeClient, podName, typ, buildlet.PodOpts{
+		ProjectID:     buildEnv.ProjectName,
 		ImageRegistry: registryPrefix,
 		Description:   fmt.Sprintf("Go Builder for %s at %s", typ, rev),
 		DeleteIn:      deleteIn,
diff --git a/cmd/gomote/gomote.go b/cmd/gomote/gomote.go
index e1499fb..5d175eb 100644
--- a/cmd/gomote/gomote.go
+++ b/cmd/gomote/gomote.go
@@ -26,14 +26,16 @@
 	"sort"
 
 	"golang.org/x/build"
+	"golang.org/x/build/buildenv"
 	"golang.org/x/build/buildlet"
-	"golang.org/x/build/dashboard"
 )
 
 var (
 	user = flag.String("user", username(), "gomote server username. You must have the token for user-$USER. The error message will say where to put it.")
 
 	staging = flag.Bool("staging", false, "if true, use the staging build coordinator and buildlets")
+
+	buildEnv = buildenv.Production
 )
 
 type command struct {
@@ -111,8 +113,7 @@
 	flag.Usage = usage
 	flag.Parse()
 	if *staging {
-		// TODO(cmang): make this configurable.
-		dashboard.BuildletBucket = "dev-go-builder-data"
+		buildEnv = buildenv.Staging
 	}
 	args := flag.Args()
 	if len(args) == 0 {
diff --git a/dashboard/builders.go b/dashboard/builders.go
index 3cbdd7c..1759cb9 100644
--- a/dashboard/builders.go
+++ b/dashboard/builders.go
@@ -9,6 +9,8 @@
 import (
 	"strconv"
 	"strings"
+
+	"golang.org/x/build/buildenv"
 )
 
 // Builders are the different build configurations.
@@ -80,28 +82,13 @@
 	return strings.Join(x, "/")
 }
 
-// BuildletBucket is the GCS storage bucket which holds the buildlet binaries.
-// Tools working in the dev project may change this.
-var BuildletBucket = "go-builder-data"
-
-func fixBuildletBucket(u string) string {
-	if BuildletBucket == "go-builder-data" {
-		// Prod. Default case.
-		return u
-	}
-	// Dev project remapping:
-	return strings.Replace(u,
-		"//storage.googleapis.com/go-builder-data/",
-		"//storage.googleapis.com/"+BuildletBucket+"/",
-		1)
-}
-
 // BuildletBinaryURL returns the public URL of this builder's buildlet.
-func (c *BuildConfig) BuildletBinaryURL() string {
-	if c.buildletURL != "" {
-		return fixBuildletBucket(c.buildletURL)
+func (c *BuildConfig) BuildletBinaryURL(e *buildenv.Environment) string {
+	tmpl := c.buildletURL
+	if tmpl == "" {
+		return "http://storage.googleapis.com/" + e.BuildletBucket + "/buildlet." + c.GOOS() + "-" + c.GOARCH()
 	}
-	return fixBuildletBucket("http://storage.googleapis.com/go-builder-data/buildlet." + c.GOOS() + "-" + c.GOARCH())
+	return strings.Replace(tmpl, "$BUCKET", e.BuildletBucket, 1)
 }
 
 // SetBuildletBinaryURL sets the public URL of this builder's buildlet.
@@ -262,7 +249,7 @@
 		Name:           "freebsd-amd64-gce93",
 		VMImage:        "freebsd-amd64-gce93",
 		machineType:    "n1-highcpu-2",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-freebsd-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
 		NumTestHelpers: 3,
 	})
 	addBuilder(BuildConfig{
@@ -270,7 +257,7 @@
 		Notes:          "FreeBSD 10.1; GCE VM is built from script in build/env/freebsd-amd64",
 		VMImage:        "freebsd-amd64-gce101",
 		machineType:    "n1-highcpu-2",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-freebsd-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
 		env:            []string{"CC=clang"},
 		NumTestHelpers: 3,
 	})
@@ -278,7 +265,7 @@
 		Name:        "freebsd-amd64-race",
 		VMImage:     "freebsd-amd64-gce101",
 		machineType: "n1-highcpu-4",
-		Go14URL:     "https://storage.googleapis.com/go-builder-data/go1.4-freebsd-amd64.tar.gz",
+		Go14URL:     "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
 		env:         []string{"CC=clang"},
 	})
 	addBuilder(BuildConfig{
@@ -286,8 +273,8 @@
 		VMImage: "freebsd-amd64-gce101",
 		//BuildletType: "freebsd-amd64-gce101",
 		machineType:    "n1-highcpu-2",
-		buildletURL:    "http://storage.googleapis.com/go-builder-data/buildlet.freebsd-amd64",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-freebsd-amd64.tar.gz",
+		buildletURL:    "http://storage.googleapis.com/$BUCKET/buildlet.freebsd-amd64",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-freebsd-amd64.tar.gz",
 		env:            []string{"GOARCH=386", "GOHOSTARCH=386", "CC=clang"},
 		NumTestHelpers: 3,
 	})
@@ -295,7 +282,7 @@
 		Name:    "linux-386",
 		VMImage: "linux-buildlet-std",
 		//BuildletType:   "linux-amd64",
-		buildletURL:    "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL:    "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:            []string{"GOROOT_BOOTSTRAP=/go1.4", "GOARCH=386", "GOHOSTARCH=386"},
 		NumTestHelpers: 3,
 	})
@@ -304,7 +291,7 @@
 		Notes:   "GO386=387",
 		VMImage: "linux-buildlet-std",
 		//BuildletType: "linux-amd64",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "GOARCH=386", "GOHOSTARCH=386", "GO386=387"},
 	})
 	addBuilder(BuildConfig{
@@ -316,6 +303,7 @@
 	addBuilder(BuildConfig{
 		Name:           "linux-amd64-kube",
 		KubeImage:      "linux-x86-std:latest",
+		buildletURL:    "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:            []string{"GOROOT_BOOTSTRAP=/go1.4"},
 		NumTestHelpers: 3,
 	})
@@ -325,7 +313,7 @@
 		VMImage:     "linux-buildlet-std",
 		machineType: "n1-highcpu-16", // CPU-bound, uses it well.
 		Notes:       "Runs buildall.sh to compile stdlib for GOOS/GOARCH pairs not otherwise covered by trybots, but doesn't run any tests.",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4"},
 		allScriptArgs: []string{
 			// Filtering pattern to buildall.bash:
@@ -363,7 +351,7 @@
 		Name:    "linux-386-clang",
 		VMImage: "linux-buildlet-clang",
 		//BuildletType: "linux-amd64-clang",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "CC=/usr/bin/clang", "GOHOSTARCH=386"},
 	})
 	addBuilder(BuildConfig{
@@ -375,7 +363,7 @@
 	addBuilder(BuildConfig{
 		Name:        "linux-386-sid",
 		VMImage:     "linux-buildlet-sid",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "GOHOSTARCH=386"},
 	})
 	addBuilder(BuildConfig{
@@ -402,26 +390,26 @@
 	addBuilder(BuildConfig{
 		Name:        "nacl-386",
 		VMImage:     "linux-buildlet-nacl-v2",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "GOOS=nacl", "GOARCH=386", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
 		//BuildletType: "nacl-amd64p32",
 	})
 	addBuilder(BuildConfig{
 		Name:        "nacl-amd64p32",
 		VMImage:     "linux-buildlet-nacl-v2",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "GOOS=nacl", "GOARCH=amd64p32", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
 		Name:        "nacl-386-kube",
 		KubeImage:   "linux-x86-nacl:latest",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "GOOS=nacl", "GOARCH=386", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
 		Name:        "nacl-amd64p32-kube",
 		KubeImage:   "linux-x86-nacl:latest",
-		buildletURL: "http://storage.googleapis.com/go-builder-data/buildlet.linux-amd64",
+		buildletURL: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64",
 		env:         []string{"GOROOT_BOOTSTRAP=/go1.4", "GOOS=nacl", "GOARCH=amd64p32", "GOHOSTOS=linux", "GOHOSTARCH=amd64"},
 	})
 	addBuilder(BuildConfig{
@@ -429,7 +417,7 @@
 		Notes:          "OpenBSD 5.8; GCE VM is built from script in build/env/openbsd-amd64",
 		VMImage:        "openbsd-amd64-58",
 		machineType:    "n1-highcpu-2",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-openbsd-amd64-gce58.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-openbsd-amd64-gce58.tar.gz",
 		NumTestHelpers: 3,
 	})
 	addBuilder(BuildConfig{
@@ -437,14 +425,14 @@
 		Notes:          "OpenBSD 5.8; GCE VM is built from script in build/env/openbsd-386",
 		VMImage:        "openbsd-386-58",
 		machineType:    "n1-highcpu-2",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-openbsd-386-gce58.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-openbsd-386-gce58.tar.gz",
 		NumTestHelpers: 3,
 	})
 	addBuilder(BuildConfig{
 		Name:    "plan9-386",
 		Notes:   "Plan 9 from 0intro; GCE VM is built from script in build/env/plan9-386",
 		VMImage: "plan9-386-v2",
-		Go14URL: "https://storage.googleapis.com/go-builder-data/go1.4-plan9-386.tar.gz",
+		Go14URL: "https://storage.googleapis.com/$BUCKET/go1.4-plan9-386.tar.gz",
 
 		// We *were* using n1-standard-1 because Plan 9 can only
 		// reliably use a single CPU. Using 2 or 4 and we see
@@ -476,7 +464,7 @@
 		Name:           "windows-amd64-gce",
 		VMImage:        "windows-buildlet-v2",
 		machineType:    "n1-highcpu-2",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-windows-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-windows-amd64.tar.gz",
 		RegularDisk:    true,
 		env:            []string{"GOARCH=amd64", "GOHOSTARCH=amd64"},
 		NumTestHelpers: 3,
@@ -486,7 +474,7 @@
 		Notes:       "Only runs -race tests (./race.bat)",
 		VMImage:     "windows-buildlet-v2",
 		machineType: "n1-highcpu-4",
-		Go14URL:     "https://storage.googleapis.com/go-builder-data/go1.4-windows-amd64.tar.gz",
+		Go14URL:     "https://storage.googleapis.com/$BUCKET/go1.4-windows-amd64.tar.gz",
 		RegularDisk: true,
 		env:         []string{"GOARCH=amd64", "GOHOSTARCH=amd64"},
 	})
@@ -495,8 +483,8 @@
 		VMImage:     "windows-buildlet-v2",
 		machineType: "n1-highcpu-2",
 		// TODO(bradfitz): once buildlet type vs. config type is split: BuildletType:   "windows-amd64-gce",
-		buildletURL:    "http://storage.googleapis.com/go-builder-data/buildlet.windows-amd64",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-windows-386.tar.gz",
+		buildletURL:    "http://storage.googleapis.com/$BUCKET/buildlet.windows-amd64",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-windows-386.tar.gz",
 		RegularDisk:    true,
 		env:            []string{"GOARCH=386", "GOHOSTARCH=386"},
 		NumTestHelpers: 3,
@@ -504,14 +492,14 @@
 	addBuilder(BuildConfig{
 		Name:           "darwin-amd64-10_10",
 		Notes:          "Mac Mini running OS X 10.10 (Yosemite)",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse:      true,
 		NumTestHelpers: 0, // disabled per golang.org/issue/12979
 	})
 	addBuilder(BuildConfig{
 		Name:           "darwin-386-10_10",
 		Notes:          "Mac Mini running OS X 10.10 (Yosemite)",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse:      true,
 		env:            []string{"GOARCH=386", "GOHOSTARCH=386"},
 		NumTestHelpers: 0, // disabled per golang.org/issue/12979
@@ -519,7 +507,7 @@
 	addBuilder(BuildConfig{
 		Name:           "android-arm-sdk19",
 		Notes:          "Android ARM device running android-19 (KitKat 4.4), attatched to Mac Mini",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse:      true,
 		env:            []string{"GOOS=android", "GOARCH=arm"},
 		NumTestHelpers: 1, // limited resources
@@ -527,7 +515,7 @@
 	addBuilder(BuildConfig{
 		Name:           "android-arm64-sdk21",
 		Notes:          "Android arm64 device using the android-21 toolchain, attatched to Mac Mini",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse:      true,
 		env:            []string{"GOOS=android", "GOARCH=arm64"},
 		NumTestHelpers: 1, // limited resources
@@ -535,7 +523,7 @@
 	addBuilder(BuildConfig{
 		Name:           "android-386-sdk21",
 		Notes:          "Android 386 device using the android-21 toolchain, attatched to Mac Mini",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse:      true,
 		env:            []string{"GOOS=android", "GOARCH=386"},
 		NumTestHelpers: 1, // limited resources
@@ -543,7 +531,7 @@
 	addBuilder(BuildConfig{
 		Name:           "android-amd64-sdk21",
 		Notes:          "Android amd64 device using the android-21 toolchain, attatched to Mac Mini",
-		Go14URL:        "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:        "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse:      true,
 		env:            []string{"GOOS=android", "GOARCH=amd64"},
 		NumTestHelpers: 1, // limited resources
@@ -552,7 +540,7 @@
 		Name:      "darwin-arm-a5ios",
 		Notes:     "iPhone 4S (A5 processor), via a Mac Mini",
 		Owner:     "crawshaw@golang.org",
-		Go14URL:   "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:   "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse: true,
 		env:       []string{"GOARCH=arm", "GOHOSTARCH=amd64"},
 	})
@@ -560,7 +548,7 @@
 		Name:      "darwin-arm64-a7ios",
 		Notes:     "iPad Mini 3 (A7 processor), via a Mac Mini",
 		Owner:     "crawshaw@golang.org",
-		Go14URL:   "https://storage.googleapis.com/go-builder-data/go1.4-darwin-amd64.tar.gz",
+		Go14URL:   "https://storage.googleapis.com/$BUCKET/go1.4-darwin-amd64.tar.gz",
 		IsReverse: true,
 		env:       []string{"GOARCH=arm64", "GOHOSTARCH=amd64"},
 	})