// Copyright 2014 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package metadata provides access to Google Compute Engine (GCE) // metadata and API service accounts. // // This package is a wrapper around the GCE metadata service, // as documented at https://developers.google.com/compute/docs/metadata. package metadata import ( "encoding/json" "fmt" "io/ioutil" "net" "net/http" "strings" "sync" "time" "google.golang.org/cloud/internal" ) type cachedValue struct { k string trim bool mu sync.Mutex v string } var ( projID = &cachedValue{k: "project/project-id", trim: true} projNum = &cachedValue{k: "project/numeric-project-id", trim: true} instID = &cachedValue{k: "instance/id", trim: true} ) var metaClient = &http.Client{ Transport: &internal.Transport{ Base: &http.Transport{ Dial: dialer().Dial, ResponseHeaderTimeout: 750 * time.Millisecond, }, }, } // go13Dialer is nil until we're using Go 1.3+. // This is a workaround for https://github.com/golang/oauth2/issues/70, where // net.Dialer.KeepAlive is unavailable on Go 1.2 (which App Engine as of // Jan 2015 still runs). // // TODO(bradfitz,jbd,adg,dsymonds): remove this once App Engine supports Go // 1.3+ and go-app-builder also supports 1.3+, or when Go 1.2 is no longer an // option on App Engine. var go13Dialer func() *net.Dialer func dialer() *net.Dialer { if fn := go13Dialer; fn != nil { return fn() } return &net.Dialer{ Timeout: 750 * time.Millisecond, } } // NotDefinedError is returned when requested metadata is not defined. // // The underlying string is the suffix after "/computeMetadata/v1/". // // This error is not returned if the value is defined to be the empty // string. type NotDefinedError string func (suffix NotDefinedError) Error() string { return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix)) } // Get returns a value from the metadata service. // The suffix is appended to "http://metadata/computeMetadata/v1/". // // If the requested metadata is not defined, the returned error will // be of type NotDefinedError. func Get(suffix string) (string, error) { // Using 169.254.169.254 instead of "metadata" here because Go // binaries built with the "netgo" tag and without cgo won't // know the search suffix for "metadata" is // ".google.internal", and this IP address is documented as // being stable anyway. url := "http://169.254.169.254/computeMetadata/v1/" + suffix req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Metadata-Flavor", "Google") res, err := metaClient.Do(req) if err != nil { return "", err } defer res.Body.Close() if res.StatusCode == http.StatusNotFound { return "", NotDefinedError(suffix) } if res.StatusCode != 200 { return "", fmt.Errorf("status code %d trying to fetch %s", res.StatusCode, url) } all, err := ioutil.ReadAll(res.Body) if err != nil { return "", err } return string(all), nil } func getTrimmed(suffix string) (s string, err error) { s, err = Get(suffix) s = strings.TrimSpace(s) return } func (c *cachedValue) get() (v string, err error) { defer c.mu.Unlock() c.mu.Lock() if c.v != "" { return c.v, nil } if c.trim { v, err = getTrimmed(c.k) } else { v, err = Get(c.k) } if err == nil { c.v = v } return } var onGCE struct { sync.Mutex set bool v bool } // OnGCE reports whether this process is running on Google Compute Engine. func OnGCE() bool { defer onGCE.Unlock() onGCE.Lock() if onGCE.set { return onGCE.v } onGCE.set = true // We use the DNS name of the metadata service here instead of the IP address // because we expect that to fail faster in the not-on-GCE case. res, err := metaClient.Get("http://metadata.google.internal") if err != nil { return false } onGCE.v = res.Header.Get("Metadata-Flavor") == "Google" return onGCE.v } // ProjectID returns the current instance's project ID string. func ProjectID() (string, error) { return projID.get() } // NumericProjectID returns the current instance's numeric project ID. func NumericProjectID() (string, error) { return projNum.get() } // InternalIP returns the instance's primary internal IP address. func InternalIP() (string, error) { return getTrimmed("instance/network-interfaces/0/ip") } // ExternalIP returns the instance's primary external (public) IP address. func ExternalIP() (string, error) { return getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip") } // Hostname returns the instance's hostname. This will probably be of // the form "INSTANCENAME.c.PROJECT.internal" but that isn't // guaranteed. // // TODO: what is this defined to be? Docs say "The host name of the // instance." func Hostname() (string, error) { return getTrimmed("network-interfaces/0/ip") } // InstanceTags returns the list of user-defined instance tags, // assigned when initially creating a GCE instance. func InstanceTags() ([]string, error) { var s []string j, err := Get("instance/tags") if err != nil { return nil, err } if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil { return nil, err } return s, nil } // InstanceID returns the current VM's numeric instance ID. func InstanceID() (string, error) { return instID.get() } // InstanceAttributes returns the list of user-defined attributes, // assigned when initially creating a GCE VM instance. The value of an // attribute can be obtained with InstanceAttributeValue. func InstanceAttributes() ([]string, error) { return lines("instance/attributes/") } // ProjectAttributes returns the list of user-defined attributes // applying to the project as a whole, not just this VM. The value of // an attribute can be obtained with ProjectAttributeValue. func ProjectAttributes() ([]string, error) { return lines("project/attributes/") } func lines(suffix string) ([]string, error) { j, err := Get(suffix) if err != nil { return nil, err } s := strings.Split(strings.TrimSpace(j), "\n") for i := range s { s[i] = strings.TrimSpace(s[i]) } return s, nil } // InstanceAttributeValue returns the value of the provided VM // instance attribute. // // If the requested attribute is not defined, the returned error will // be of type NotDefinedError. // // InstanceAttributeValue may return ("", nil) if the attribute was // defined to be the empty string. func InstanceAttributeValue(attr string) (string, error) { return Get("instance/attributes/" + attr) } // ProjectAttributeValue returns the value of the provided // project attribute. // // If the requested attribute is not defined, the returned error will // be of type NotDefinedError. // // ProjectAttributeValue may return ("", nil) if the attribute was // defined to be the empty string. func ProjectAttributeValue(attr string) (string, error) { return Get("project/attributes/" + attr) } // Scopes returns the service account scopes for the given account. // The account may be empty or the string "default" to use the instance's // main account. func Scopes(serviceAccount string) ([]string, error) { if serviceAccount == "" { serviceAccount = "default" } return lines("instance/service-accounts/" + serviceAccount + "/scopes") }