cos-customizer: Support non-prod GCE endpoints

Users can now control the GCE endpoint through a flag that is available
on all subcommands. Every step in the workflow must set the flag for it
to work properly.

BUG=b/176990931
TEST=Run one workflow manually in non-prod GCE, run ./run_tests to
regression check

Change-Id: I0336418a3ad8a680e854085afcb6bbb5f5efce04
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/25821
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Reviewed-by: Varsha Teratipally <teratipally@google.com>
Reviewed-by: Roy Yang <royyang@google.com>
Tested-by: Robert Kolchmeyer <rkolchmeyer@google.com>
diff --git a/src/cmd/cos_customizer/finish_image_build.go b/src/cmd/cos_customizer/finish_image_build.go
index 6d052e7..1707bac 100644
--- a/src/cmd/cos_customizer/finish_image_build.go
+++ b/src/cmd/cos_customizer/finish_image_build.go
@@ -21,6 +21,7 @@
 	"log"
 	"os/exec"
 	"strconv"
+	"strings"
 	"time"
 
 	"cos.googlesource.com/cos/tools.git/src/pkg/config"
@@ -302,6 +303,10 @@
 		log.Println(err)
 		return subcommands.ExitFailure
 	}
+	// Set non-default GCE endpoints in the buildConfig.
+	if !strings.Contains(svc.BasePath, "compute.googleapis.com/compute/v1") {
+		buildConfig.GCEEndpoint = svc.BasePath
+	}
 	if err := validateOEM(buildConfig, provConfig); err != nil {
 		log.Println(err)
 		return subcommands.ExitFailure
diff --git a/src/cmd/cos_customizer/main.go b/src/cmd/cos_customizer/main.go
index 62e7580..320ca0e 100644
--- a/src/cmd/cos_customizer/main.go
+++ b/src/cmd/cos_customizer/main.go
@@ -35,6 +35,8 @@
 var persistentDir = flag.String("local-state-workdir", ".cos-customizer-workdir",
 	"Name of the directory in $HOME to use for storing local state.")
 
+var computeEndpoint = flag.String("compute-endpoint", "", "If set, used as the endpoint for the GCE API.")
+
 func clients(ctx context.Context, anonymousCreds bool) (*compute.Service, *storage.Client, error) {
 	var httpClient *http.Client
 	var err error
@@ -46,7 +48,11 @@
 			return nil, nil, err
 		}
 	}
-	svc, err := compute.New(httpClient)
+	computeOpts := []option.ClientOption{option.WithHTTPClient(httpClient)}
+	if *computeEndpoint != "" {
+		computeOpts = append(computeOpts, option.WithEndpoint(*computeEndpoint))
+	}
+	svc, err := compute.NewService(ctx, computeOpts...)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/src/pkg/config/config.go b/src/pkg/config/config.go
index 9f84d2e..d680a28 100644
--- a/src/pkg/config/config.go
+++ b/src/pkg/config/config.go
@@ -62,14 +62,15 @@
 
 // Build stores configuration data associated with the image build session.
 type Build struct {
-	GCSBucket string
-	GCSDir    string
-	Project   string
-	Zone      string
-	DiskSize  int
-	GPUType   string
-	Timeout   string
-	GCSFiles  []string
+	GCSBucket   string
+	GCSDir      string
+	Project     string
+	Zone        string
+	DiskSize    int
+	GPUType     string
+	Timeout     string
+	GCSFiles    []string
+	GCEEndpoint string
 }
 
 // SaveConfigToFile clears the target config file and then saves the new config
diff --git a/src/pkg/preloader/preload.go b/src/pkg/preloader/preload.go
index 860cfb6..e5aa5ab 100644
--- a/src/pkg/preloader/preload.go
+++ b/src/pkg/preloader/preload.go
@@ -23,6 +23,7 @@
 	"fmt"
 	"io/ioutil"
 	"log"
+	"net/url"
 	"os"
 	"os/exec"
 	"path"
@@ -270,6 +271,17 @@
 		return nil, err
 	}
 	var args []string
+	if buildSpec.GCEEndpoint != "" {
+		// Adding the "projects/" suffix is a Daisy-specific quirk.
+		u, err := url.Parse(buildSpec.GCEEndpoint)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing %q: %v", buildSpec.GCEEndpoint, err)
+		}
+		u.Path = path.Join(u.Path, "projects")
+		// We add a trailing "/" here because Daisy needs it, and u.String() trims
+		// it.
+		args = append(args, "-compute_endpoint_override="+u.String()+"/")
+	}
 	if provConfig.BootDisk.OEMSize == "" && buildSpec.DiskSize > 10 && !provConfig.BootDisk.ReclaimSDA3 {
 		// If the oem-size is set, or need to reclaim sda3,
 		// create the disk with default size,