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)
+ }
+}