veyron/products/lib/gce: Add a library to detect GCE

Add a library to detect if the current process is running on a GCE
instance, and if it is, determine the external IP address to use.

This is uses the GCE metadata API documented at
https://developers.google.com/compute/docs/metadata

Change-Id: I5de47506e8b244d9f42224aa29ba6bf91c011213
diff --git a/products/lib/gce/gce.go b/products/lib/gce/gce.go
new file mode 100644
index 0000000..2d1cd7b
--- /dev/null
+++ b/products/lib/gce/gce.go
@@ -0,0 +1,68 @@
+// Package gce provides a way to test whether the current process is running on
+// Google Compute Engine, and to extract settings from this environment.
+package gce
+
+import (
+	"io/ioutil"
+	"net"
+	"net/http"
+	"sync"
+	"time"
+)
+
+// This URL returns the external IP address assigned to the local GCE instance.
+// If a HTTP GET request fails for any reason, this is not a GCE instance. If
+// the result of the GET request doesn't contain a "Metadata-Flavor: Google"
+// header, it is also not a GCE instance. The body of the document contains the
+// external IP address, if present. Otherwise, the body is empty.
+const url = "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip"
+
+// How long to wait for the HTTP request to return.
+const timeout = time.Second
+
+var (
+	once       sync.Once
+	isGCE      bool
+	externalIP net.IP
+)
+
+// RunningOnGoogleComputeEngine returns true if the current process is running
+// on a Google Compute Engine instance.
+func RunningOnGoogleComputeEngine() bool {
+	once.Do(func() {
+		isGCE, externalIP = googleComputeEngineTest(url)
+	})
+	return isGCE
+}
+
+// GoogleComputeEngineExternalIPAddress returns the external IP address of this
+// Google Compute Engine instance, or nil if there is none. Must be called after
+// RunningOnGoogleComputeEngine.
+func GoogleComputeEngineExternalIPAddress() net.IP {
+	return externalIP
+}
+
+func googleComputeEngineTest(url string) (bool, net.IP) {
+	client := &http.Client{Timeout: timeout}
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return false, nil
+	}
+	req.Header.Add("Metadata-Flavor", "Google")
+	resp, err := client.Do(req)
+	if err != nil {
+		return false, nil
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		return false, nil
+	}
+	if flavor := resp.Header["Metadata-Flavor"]; len(flavor) != 1 || flavor[0] != "Google" {
+		return false, nil
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return true, nil
+	}
+	return true, net.ParseIP(string(body))
+}
diff --git a/products/lib/gce/gce_test.go b/products/lib/gce/gce_test.go
new file mode 100644
index 0000000..ba2591b
--- /dev/null
+++ b/products/lib/gce/gce_test.go
@@ -0,0 +1,62 @@
+package gce
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+	"testing"
+)
+
+func startServer(t *testing.T) (net.Addr, func()) {
+	l, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatal(err)
+	}
+	http.HandleFunc("/404", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusNotFound)
+	})
+	http.HandleFunc("/200_not_gce", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, "Hello")
+	})
+	http.HandleFunc("/gce_no_ip", func(w http.ResponseWriter, r *http.Request) {
+		// When a GCE instance doesn't have an external IP address, the
+		// request returns a 200 with an empty body.
+		w.Header().Add("Metadata-Flavor", "Google")
+		if m := r.Header["Metadata-Flavor"]; len(m) != 1 || m[0] != "Google" {
+			w.WriteHeader(http.StatusForbidden)
+			return
+		}
+	})
+	http.HandleFunc("/gce_with_ip", func(w http.ResponseWriter, r *http.Request) {
+		// When a GCE instance has an external IP address, the request
+		// returns the IP address as body.
+		w.Header().Add("Metadata-Flavor", "Google")
+		if m := r.Header["Metadata-Flavor"]; len(m) != 1 || m[0] != "Google" {
+			w.WriteHeader(http.StatusForbidden)
+			return
+		}
+		fmt.Fprintf(w, "1.2.3.4")
+	})
+
+	go http.Serve(l, nil)
+	return l.Addr(), func() { l.Close() }
+}
+
+func TestGCE(t *testing.T) {
+	addr, stop := startServer(t)
+	defer stop()
+	baseURL := "http://" + addr.String()
+
+	if isGCE, ip := googleComputeEngineTest(baseURL + "/404"); isGCE != false || ip != nil {
+		t.Errorf("Unexpected result. Got %v:%v, want false:nil", isGCE, ip)
+	}
+	if isGCE, ip := googleComputeEngineTest(baseURL + "/200_not_gce"); isGCE != false || ip != nil {
+		t.Errorf("Unexpected result. Got %v:%v, want false:nil", isGCE, ip)
+	}
+	if isGCE, ip := googleComputeEngineTest(baseURL + "/gce_no_ip"); isGCE != true || ip != nil {
+		t.Errorf("Unexpected result. Got %v:%v, want true:nil", isGCE, ip)
+	}
+	if isGCE, ip := googleComputeEngineTest(baseURL + "/gce_with_ip"); isGCE != true || ip.String() != "1.2.3.4" {
+		t.Errorf("Unexpected result. Got %v:%v, want true:1.2.3.4", isGCE, ip)
+	}
+}