From 9faa5530681c94908957e84f36d35aa970c73133 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 00:37:45 -0500 Subject: [PATCH 01/13] Fingerprinting code for GCE nodes This reads the following: * hostname * instance id * machine-type * zone * internal IP * external IP (if any) * tags * attributes Atributes are placed under the platform.gce.attr.* hierarchy. Tags are set up as platform.gce.tag.TagName=true. --- client/fingerprint/env_gce.go | 236 +++++++++++++++++++++++++++++ client/fingerprint/env_gce_test.go | 192 +++++++++++++++++++++++ client/fingerprint/fingerprint.go | 2 + 3 files changed, 430 insertions(+) create mode 100644 client/fingerprint/env_gce.go create mode 100644 client/fingerprint/env_gce_test.go diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go new file mode 100644 index 00000000000..ab6397eef9e --- /dev/null +++ b/client/fingerprint/env_gce.go @@ -0,0 +1,236 @@ +package fingerprint + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "time" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/nomad/structs" +) + +type GCEMetadataClient struct { + *http.Client + logger *log.Logger + metadataURL string +} + +type ReqError struct { + StatusCode int +} + +func (e ReqError) Error() string { + return http.StatusText(e.StatusCode) +} + +func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { + // Read the internal metadata URL from the environment, allowing test files to + // provide their own + metadataURL := os.Getenv("GCE_ENV_URL") + if metadataURL == "" { + metadataURL = "http://169.254.169.254/computeMetadata/v1/instance/" + } + + // assume 2 seconds is enough time for inside GCE network + client := &http.Client{ + Timeout: 2 * time.Second, + } + + return GCEMetadataClient{ + Client: client, + logger: logger, + metadataURL: metadataURL, + } +} + +func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) { + reqUrl := g.metadataURL + attribute + if recursive { + reqUrl = reqUrl + "?recursive=true" + } + + parsedUrl, err := url.Parse(reqUrl) + if err != nil { + return "", err + } + + req := &http.Request{ + Method: "GET", + URL: parsedUrl, + Header: http.Header{ + "Metadata-Flavor": []string{"Google"}, + }, + } + + res, err := g.Client.Do(req) + if err != nil { + return "", err + } + + resp, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + g.logger.Printf("[ERR]: fingerprint.env_gce: Error reading response body for GCE %s", attribute) + return "", err + } + + if res.StatusCode >= 400 { + return "", ReqError{res.StatusCode} + } + + return string(resp), nil +} + +// EnvGCEFingerprint is used to fingerprint the CPU +type EnvGCEFingerprint struct { + logger *log.Logger +} + +// NewEnvGCEFingerprint is used to create a CPU fingerprint +func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { + f := &EnvGCEFingerprint{logger: logger} + return f +} + +func checkError(err error, logger *log.Logger, desc string) error { + // If it's a URL error, assume we're not actually in an GCE environment. + // To the outer layers, this isn't an error so return nil. + if _, ok := err.(*url.Error); ok { + logger.Printf("[ERR] fingerprint.env_gce: Error querying GCE " + desc + ", skipping") + return nil + } + // Otherwise pass the error through. + return err +} + +func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { + if !f.isGCE() { + return false, nil + } + + // newNetwork is populated and addded to the Nodes resources + newNetwork := &structs.NetworkResource{ + Device: "eth0", + } + + if node.Links == nil { + node.Links = make(map[string]string) + } + + client := NewGCEMetadataClient(f.logger) + + keys := []string{ + "hostname", + "id", + } + for _, k := range keys { + value, err := client.Get(k, false) + if err != nil { + return false, checkError(err, f.logger, k) + } + + // assume we want blank entries + node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + } + + // These keys need everything before the final slash removed to be usable. + keys = []string{ + "machine-type", + "zone", + } + for _, k := range keys { + value, err := client.Get(k, false) + if err != nil { + return false, checkError(err, f.logger, k) + } + + index := strings.LastIndex(value, "/") + value = value[index+1:] + node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + } + + // Get internal and external IP (if it exits) + value, err := client.Get("network-interfaces/0/ip", false) + if err != nil { + return false, checkError(err, f.logger, "ip") + } + newNetwork.IP = strings.Trim(value, "\n") + newNetwork.CIDR = newNetwork.IP + "/32" + node.Attributes["network.ip-address"] = newNetwork.IP + + value, err = client.Get("network-interfaces/0/access-configs/0/external-ip", false) + if re, ok := err.(ReqError); err != nil && (!ok || re.StatusCode != 404) { + return false, checkError(err, f.logger, "external IP") + } + value = strings.Trim(value, "\n") + if len(value) > 0 { + node.Attributes["platform.gce.external-ip"] = value + } + + var tagList []string + value, err = client.Get("tags", false) + if err != nil { + return false, checkError(err, f.logger, "tags") + } + err = json.Unmarshal([]byte(value), &tagList) + if err == nil { + for _, tag := range tagList { + node.Attributes["platform.gce.tag."+tag] = "true" + } + } else { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) + } + + var attrDict map[string]string + value, err = client.Get("attributes/", true) + if err != nil { + return false, checkError(err, f.logger, "attributes/") + } + err = json.Unmarshal([]byte(value), &attrDict) + if err == nil { + for k, v := range attrDict { + node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") + } + } else { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) + } + + // populate Node Network Resources + if node.Resources == nil { + node.Resources = &structs.Resources{} + } + node.Resources.Networks = append(node.Resources.Networks, newNetwork) + + // populate Links + node.Links["gce"] = node.Attributes["platform.gce.id"] + + return true, nil +} + +func (f *EnvGCEFingerprint) isGCE() bool { + // TODO: better way to detect GCE? + + client := NewGCEMetadataClient(f.logger) + // Query the metadata url for the machine type, to verify we're on GCE + machineType, err := client.Get("machine-type", false) + if err != nil { + if re, ok := err.(ReqError); !ok || re.StatusCode != 404 { + // If it wasn't a 404 error, print an error message. + f.logger.Printf("[ERR] fingerprint.env_gce: Error querying GCE Metadata URL, skipping") + } + return false + } + + match, err := regexp.MatchString("projects/.+/machineTypes/.+", machineType) + if !match { + return false + } + + return true +} diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go new file mode 100644 index 00000000000..402f03d7ac9 --- /dev/null +++ b/client/fingerprint/env_gce_test.go @@ -0,0 +1,192 @@ +package fingerprint + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/nomad/structs" +) + +func TestGCEFingerprint_nonGCE(t *testing.T) { + f := NewEnvGCEFingerprint(testLogger()) + node := &structs.Node{ + Attributes: make(map[string]string), + } + + ok, err := f.Fingerprint(&config.Config{}, node) + if err != nil { + t.Fatalf("err: %v", err) + } + + if ok { + t.Fatalf("Should be false without test server") + } +} + +func testFingerprint_GCE(t *testing.T, withExternalIp bool) { + f := NewEnvGCEFingerprint(testLogger()) + node := &structs.Node{ + Attributes: make(map[string]string), + } + + // configure mock server with fixture routes, data + routes := routes{} + if err := json.Unmarshal([]byte(GCE_routes), &routes); err != nil { + t.Fatalf("Failed to unmarshal JSON in GCE ENV test: %s", err) + } + if withExternalIp { + routes.Endpoints = append(routes.Endpoints, &endpoint{ + Uri: "/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip", + ContentType: "text/plain", + Body: "104.44.55.66", + }) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + value, ok := r.Header["Metadata-Flavor"] + if !ok { + t.Fatal("Metadata-Flavor not present in HTTP request header") + } + if value[0] != "Google" { + t.Fatalf("Expected Metadata-Flavor Google, saw %s", value[0]) + } + + found := false + for _, e := range routes.Endpoints { + if r.RequestURI == e.Uri { + w.Header().Set("Content-Type", e.ContentType) + fmt.Fprintln(w, e.Body) + } + found = true + } + + if !found { + w.WriteHeader(404) + } + })) + defer ts.Close() + os.Setenv("GCE_ENV_URL", ts.URL+"/computeMetadata/v1/instance/") + + ok, err := f.Fingerprint(&config.Config{}, node) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !ok { + t.Fatalf("should apply") + } + + keys := []string{ + "platform.gce.id", + "platform.gce.hostname", + "platform.gce.zone", + "platform.gce.machine-type", + "platform.gce.zone", + "platform.gce.tag.abc", + "platform.gce.tag.def", + "platform.gce.attr.ghi", + "platform.gce.attr.jkl", + "network.ip-address", + } + + for _, k := range keys { + assertNodeAttributeContains(t, node, k) + } + + if len(node.Links) == 0 { + t.Fatalf("Empty links for Node in GCE Fingerprint test") + } + + // Make sure Links contains the GCE ID. + for _, k := range []string{"gce"} { + assertNodeLinksContains(t, node, k) + } + + assertNodeAttributeEquals(t, node, "platform.gce.id", "12345") + assertNodeAttributeEquals(t, node, "platform.gce.hostname", "instance-1.c.project.internal") + assertNodeAttributeEquals(t, node, "platform.gce.zone", "us-central1-f") + assertNodeAttributeEquals(t, node, "platform.gce.machine-type", "n1-standard-1") + + if node.Resources == nil || len(node.Resources.Networks) == 0 { + t.Fatal("Expected to find Network Resources") + } + + // Test at least the first Network Resource + net := node.Resources.Networks[0] + if net.IP != "10.240.0.5" { + t.Fatalf("Expected Network Resource to have IP 10.240.0.5, saw %s", net.IP) + } + if net.CIDR != "10.240.0.5/32" { + t.Fatalf("Expected Network Resource to have CIDR 10.240.0.5/32, saw %s", net.CIDR) + } + if net.Device == "" { + t.Fatal("Expected Network Resource to have a Device Name") + } + + assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") + if withExternalIp { + assertNodeAttributeEquals(t, node, "platform.gce.external-ip", "104.44.55.66") + } else if _, ok := node.Attributes["platform.gce.external-ip"]; ok { + t.Fatal("platform.gce.external-ip is set without an external IP") + } + + assertNodeAttributeEquals(t, node, "platform.gce.tag.abc", "true") + assertNodeAttributeEquals(t, node, "platform.gce.tag.def", "true") + assertNodeAttributeEquals(t, node, "platform.gce.attr.ghi", "111") + assertNodeAttributeEquals(t, node, "platform.gce.attr.jkl", "222") +} + +const GCE_routes = ` +{ + "endpoints": [ + { + "uri": "/computeMetadata/v1/instance/id", + "content-type": "text/plain", + "body": "12345" + }, + { + "uri": "/computeMetadata/v1/instance/hostname", + "content-type": "text/plain", + "body": "instance-1.c.project.internal" + }, + { + "uri": "/computeMetadata/v1/instance/zone", + "content-type": "text/plain", + "body": "projects/555555/zones/us-central1-f" + }, + { + "uri": "/computeMetadata/v1/instance/machine-type", + "content-type": "text/plain", + "body": "projects/555555/machineTypes/n1-standard-1" + }, + { + "uri": "/computeMetadata/v1/instance/network-interfaces/0/ip", + "content-type": "text/plain", + "body": "10.240.0.5" + }, + { + "uri": "/computeMetadata/v1/instance/tags", + "content-type": "application/json", + "body": "[\"abc\", \"def\"]" + }, + { + "uri": "/computeMetadata/v1/instance/attributes/?recursive=true", + "content-type": "application/json", + "body": "{\"ghi\":\"111\",\"jkl\":\"222\"}" + } + ] +} +` + +func TestFingerprint_GCEWithExternalIp(t *testing.T) { + testFingerprint_GCE(t, true) +} + +func TestFingerprint_GCEWithoutExternalIp(t *testing.T) { + testFingerprint_GCE(t, false) +} diff --git a/client/fingerprint/fingerprint.go b/client/fingerprint/fingerprint.go index ce69a8ac94c..4a42057b297 100644 --- a/client/fingerprint/fingerprint.go +++ b/client/fingerprint/fingerprint.go @@ -18,6 +18,7 @@ var BuiltinFingerprints = []string{ "storage", "network", "env_aws", + "env_gce", } // builtinFingerprintMap contains the built in registered fingerprints @@ -30,6 +31,7 @@ var builtinFingerprintMap = map[string]Factory{ "storage": NewStorageFingerprint, "network": NewNetworkFingerprinter, "env_aws": NewEnvAWSFingerprint, + "env_gce": NewEnvGCEFingerprint, } // NewFingerprint is used to instantiate and return a new fingerprint From 444d6f63e63006b312e13d5453fcb351680d0556 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 00:42:34 -0500 Subject: [PATCH 02/13] isAWS should return false on GCE GCE and AWS both expose metadata servers, and GCE's 404 response includes the URL in the content, which maatches the regex. So, check the response code as well and if a 4xx code comes back, take that to meanit's not AWS. --- client/fingerprint/env_aws.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index 0bf92410cde..d47ee1ba8bd 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -176,6 +176,11 @@ func isAWS() bool { } defer resp.Body.Close() + if resp.StatusCode >= 400 { + // URL not found, which indicates that this isn't AWS + return false + } + instanceID, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERR] fingerprint.env_aws: Error reading AWS Instance ID, skipping") From 458adfa1248c8bde562121bb666ac13ff1e0b0c9 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 12:57:45 -0500 Subject: [PATCH 03/13] Use a constant for the default GCE metadata URL --- client/fingerprint/env_gce.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index ab6397eef9e..c062efb49bc 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -15,6 +15,8 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) +const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" + type GCEMetadataClient struct { *http.Client logger *log.Logger @@ -34,7 +36,7 @@ func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { // provide their own metadataURL := os.Getenv("GCE_ENV_URL") if metadataURL == "" { - metadataURL = "http://169.254.169.254/computeMetadata/v1/instance/" + metadataURL = DEFAULT_GCE_URL } // assume 2 seconds is enough time for inside GCE network From 69ec26e9520a3c652e4a8fd626b201da47f71d1f Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 12:59:02 -0500 Subject: [PATCH 04/13] And add a comment to the constant. --- client/fingerprint/env_gce.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index c062efb49bc..92c7fec0f31 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -15,6 +15,8 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) +// This is where the GCE metadata server normally resides. We hardcode the +// "instance" path as well since it's the only one we access here. const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" type GCEMetadataClient struct { From 983e8516e4a8e4bdd2452cced2e6c9bda2696fc7 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 13:13:25 -0500 Subject: [PATCH 05/13] Consolidate GCEMetadataClient into EnvGCEFingerprint This allows easier reuse of the same client across multiple functions. --- client/fingerprint/env_gce.go | 53 +++++++++++++----------------- client/fingerprint/env_gce_test.go | 2 +- 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 92c7fec0f31..7f2fa37849f 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -20,9 +20,6 @@ import ( const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" type GCEMetadataClient struct { - *http.Client - logger *log.Logger - metadataURL string } type ReqError struct { @@ -33,7 +30,15 @@ func (e ReqError) Error() string { return http.StatusText(e.StatusCode) } -func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { +// EnvGCEFingerprint is used to fingerprint the CPU +type EnvGCEFingerprint struct { + client *http.Client + logger *log.Logger + metadataURL string +} + +// NewEnvGCEFingerprint is used to create a CPU fingerprint +func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { // Read the internal metadata URL from the environment, allowing test files to // provide their own metadataURL := os.Getenv("GCE_ENV_URL") @@ -46,15 +51,15 @@ func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { Timeout: 2 * time.Second, } - return GCEMetadataClient{ - Client: client, + return &EnvGCEFingerprint{ + client: client, logger: logger, metadataURL: metadataURL, } } -func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) { - reqUrl := g.metadataURL + attribute +func (f *EnvGCEFingerprint) Get(attribute string, recursive bool) (string, error) { + reqUrl := f.metadataURL + attribute if recursive { reqUrl = reqUrl + "?recursive=true" } @@ -72,7 +77,7 @@ func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) }, } - res, err := g.Client.Do(req) + res, err := f.client.Do(req) if err != nil { return "", err } @@ -80,7 +85,7 @@ func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) resp, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - g.logger.Printf("[ERR]: fingerprint.env_gce: Error reading response body for GCE %s", attribute) + f.logger.Printf("[ERR]: fingerprint.env_gce: Error reading response body for GCE %s", attribute) return "", err } @@ -91,17 +96,6 @@ func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) return string(resp), nil } -// EnvGCEFingerprint is used to fingerprint the CPU -type EnvGCEFingerprint struct { - logger *log.Logger -} - -// NewEnvGCEFingerprint is used to create a CPU fingerprint -func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { - f := &EnvGCEFingerprint{logger: logger} - return f -} - func checkError(err error, logger *log.Logger, desc string) error { // If it's a URL error, assume we're not actually in an GCE environment. // To the outer layers, this isn't an error so return nil. @@ -127,14 +121,12 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) node.Links = make(map[string]string) } - client := NewGCEMetadataClient(f.logger) - keys := []string{ "hostname", "id", } for _, k := range keys { - value, err := client.Get(k, false) + value, err := f.Get(k, false) if err != nil { return false, checkError(err, f.logger, k) } @@ -149,7 +141,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) "zone", } for _, k := range keys { - value, err := client.Get(k, false) + value, err := f.Get(k, false) if err != nil { return false, checkError(err, f.logger, k) } @@ -160,7 +152,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } // Get internal and external IP (if it exits) - value, err := client.Get("network-interfaces/0/ip", false) + value, err := f.Get("network-interfaces/0/ip", false) if err != nil { return false, checkError(err, f.logger, "ip") } @@ -168,7 +160,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) newNetwork.CIDR = newNetwork.IP + "/32" node.Attributes["network.ip-address"] = newNetwork.IP - value, err = client.Get("network-interfaces/0/access-configs/0/external-ip", false) + value, err = f.Get("network-interfaces/0/access-configs/0/external-ip", false) if re, ok := err.(ReqError); err != nil && (!ok || re.StatusCode != 404) { return false, checkError(err, f.logger, "external IP") } @@ -178,7 +170,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } var tagList []string - value, err = client.Get("tags", false) + value, err = f.Get("tags", false) if err != nil { return false, checkError(err, f.logger, "tags") } @@ -192,7 +184,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } var attrDict map[string]string - value, err = client.Get("attributes/", true) + value, err = f.Get("attributes/", true) if err != nil { return false, checkError(err, f.logger, "attributes/") } @@ -220,9 +212,8 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) func (f *EnvGCEFingerprint) isGCE() bool { // TODO: better way to detect GCE? - client := NewGCEMetadataClient(f.logger) // Query the metadata url for the machine type, to verify we're on GCE - machineType, err := client.Get("machine-type", false) + machineType, err := f.Get("machine-type", false) if err != nil { if re, ok := err.(ReqError); !ok || re.StatusCode != 404 { // If it wasn't a 404 error, print an error message. diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 402f03d7ac9..28889ba166d 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -29,7 +29,6 @@ func TestGCEFingerprint_nonGCE(t *testing.T) { } func testFingerprint_GCE(t *testing.T, withExternalIp bool) { - f := NewEnvGCEFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), } @@ -71,6 +70,7 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { })) defer ts.Close() os.Setenv("GCE_ENV_URL", ts.URL+"/computeMetadata/v1/instance/") + f := NewEnvGCEFingerprint(testLogger()) ok, err := f.Fingerprint(&config.Config{}, node) if err != nil { From a55bdd3430c4a3d4a899968aaf616472515f1a6c Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Tue, 6 Oct 2015 21:16:28 -0500 Subject: [PATCH 06/13] NonXXX tests should pass when actually running in their respective environments. Fixes #224 --- client/fingerprint/env_aws_test.go | 1 + client/fingerprint/env_gce_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index acc70e689a1..80a86d5da5d 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -13,6 +13,7 @@ import ( ) func TestEnvAWSFingerprint_nonAws(t *testing.T) { + os.Setenv("AWS_ENV_URL", "http://127.0.0.1/latest/meta-data/") f := NewEnvAWSFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 28889ba166d..c6a4868f681 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -13,6 +13,7 @@ import ( ) func TestGCEFingerprint_nonGCE(t *testing.T) { + os.Setenv("GCE_ENV_URL", "http://127.0.0.1/computeMetadata/v1/instance/") f := NewEnvGCEFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), From f985ef45fa3a4850c5dfe58294f956d3b64455e4 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Tue, 6 Oct 2015 21:21:42 -0500 Subject: [PATCH 07/13] TestNetworkFingerprint_notAWS passes even when actually on AWS --- client/fingerprint/env_aws_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index 80a86d5da5d..e8494c2633f 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -200,6 +200,7 @@ func TestNetworkFingerprint_AWS(t *testing.T) { } func TestNetworkFingerprint_notAWS(t *testing.T) { + os.Setenv("AWS_ENV_URL", "http://127.0.0.1/latest/meta-data/") f := NewEnvAWSFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), From 79531fe17c237d83b32392b8afce0db0664e9b1f Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Wed, 7 Oct 2015 12:39:23 -0500 Subject: [PATCH 08/13] Compact tags and attribute reading code. --- client/fingerprint/env_gce.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 7f2fa37849f..ffc6b76a572 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -174,13 +174,12 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) if err != nil { return false, checkError(err, f.logger, "tags") } - err = json.Unmarshal([]byte(value), &tagList) - if err == nil { + if err := json.Unmarshal([]byte(value), &tagList); err != nil { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) + } else { for _, tag := range tagList { node.Attributes["platform.gce.tag."+tag] = "true" } - } else { - f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) } var attrDict map[string]string @@ -188,13 +187,12 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) if err != nil { return false, checkError(err, f.logger, "attributes/") } - err = json.Unmarshal([]byte(value), &attrDict) - if err == nil { + if err := json.Unmarshal([]byte(value), &attrDict); err != nil { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) + } else { for k, v := range attrDict { node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") } - } else { - f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) } // populate Node Network Resources From a95cedbfe7caf64788af9a5278707519189d7c8e Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Wed, 7 Oct 2015 13:35:00 -0500 Subject: [PATCH 09/13] Parse information for all GCE network interface. * No longer setting Device name in the network interface since we can't match up the info here with real device names. * Add attributes for all external IPs if more than one exists. --- client/fingerprint/env_gce.go | 69 +++++++++++++++++------------- client/fingerprint/env_gce_test.go | 29 ++++++------- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index ffc6b76a572..ceff998d786 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "regexp" + "strconv" "strings" "time" @@ -19,7 +20,14 @@ import ( // "instance" path as well since it's the only one we access here. const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" -type GCEMetadataClient struct { +type GCEMetadataNetworkInterface struct { + AccessConfigs []struct { + ExternalIp string + Type string + } + ForwardedIps []string + Ip string + Network string } type ReqError struct { @@ -30,6 +38,11 @@ func (e ReqError) Error() string { return http.StatusText(e.StatusCode) } +func lastToken(s string) string { + index := strings.LastIndex(s, "/") + return s[index+1:] +} + // EnvGCEFingerprint is used to fingerprint the CPU type EnvGCEFingerprint struct { client *http.Client @@ -112,11 +125,6 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) return false, nil } - // newNetwork is populated and addded to the Nodes resources - newNetwork := &structs.NetworkResource{ - Device: "eth0", - } - if node.Links == nil { node.Links = make(map[string]string) } @@ -146,27 +154,36 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) return false, checkError(err, f.logger, k) } - index := strings.LastIndex(value, "/") - value = value[index+1:] - node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + node.Attributes["platform.gce."+k] = strings.Trim(lastToken(value), "\n") } - // Get internal and external IP (if it exits) - value, err := f.Get("network-interfaces/0/ip", false) - if err != nil { - return false, checkError(err, f.logger, "ip") + // Prepare to populate Node Network Resources + if node.Resources == nil { + node.Resources = &structs.Resources{} } - newNetwork.IP = strings.Trim(value, "\n") - newNetwork.CIDR = newNetwork.IP + "/32" - node.Attributes["network.ip-address"] = newNetwork.IP - value, err = f.Get("network-interfaces/0/access-configs/0/external-ip", false) - if re, ok := err.(ReqError); err != nil && (!ok || re.StatusCode != 404) { - return false, checkError(err, f.logger, "external IP") - } - value = strings.Trim(value, "\n") - if len(value) > 0 { - node.Attributes["platform.gce.external-ip"] = value + // Get internal and external IPs (if they exist) + value, err := f.Get("network-interfaces/", true) + var interfaces []GCEMetadataNetworkInterface + if err := json.Unmarshal([]byte(value), &interfaces); err != nil { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) + } else { + for _, intf := range interfaces { + prefix := "platform.gce.network." + lastToken(intf.Network) + + // newNetwork is populated and addded to the Nodes resources + newNetwork := &structs.NetworkResource{ + IP: strings.Trim(intf.Ip, "\n"), + } + newNetwork.CIDR = newNetwork.IP + "/32" + node.Resources.Networks = append(node.Resources.Networks, newNetwork) + + node.Attributes["network.ip-address"] = newNetwork.IP + node.Attributes[prefix+".ip"] = newNetwork.IP + for index, accessConfig := range intf.AccessConfigs { + node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp + } + } } var tagList []string @@ -195,12 +212,6 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } } - // populate Node Network Resources - if node.Resources == nil { - node.Resources = &structs.Resources{} - } - node.Resources.Networks = append(node.Resources.Networks, newNetwork) - // populate Links node.Links["gce"] = node.Attributes["platform.gce.id"] diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index c6a4868f681..460bd726fd3 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -39,13 +39,16 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { if err := json.Unmarshal([]byte(GCE_routes), &routes); err != nil { t.Fatalf("Failed to unmarshal JSON in GCE ENV test: %s", err) } + networkEndpoint := &endpoint{ + Uri: "/computeMetadata/v1/instance/network-interfaces/?recursive=true", + ContentType: "application/json", + } if withExternalIp { - routes.Endpoints = append(routes.Endpoints, &endpoint{ - Uri: "/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip", - ContentType: "text/plain", - Body: "104.44.55.66", - }) + networkEndpoint.Body = `[{"accessConfigs":[{"externalIp":"104.44.55.66","type":"ONE_TO_ONE_NAT"},{"externalIp":"104.44.55.67","type":"ONE_TO_ONE_NAT"}],"forwardedIps":[],"ip":"10.240.0.5","network":"projects/555555/networks/default"}]` + } else { + networkEndpoint.Body = `[{"accessConfigs":[],"forwardedIps":[],"ip":"10.240.0.5","network":"projects/555555/networks/default"}]` } + routes.Endpoints = append(routes.Endpoints, networkEndpoint) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { value, ok := r.Header["Metadata-Flavor"] @@ -125,15 +128,14 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { if net.CIDR != "10.240.0.5/32" { t.Fatalf("Expected Network Resource to have CIDR 10.240.0.5/32, saw %s", net.CIDR) } - if net.Device == "" { - t.Fatal("Expected Network Resource to have a Device Name") - } assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") + assertNodeAttributeEquals(t, node, "platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { - assertNodeAttributeEquals(t, node, "platform.gce.external-ip", "104.44.55.66") - } else if _, ok := node.Attributes["platform.gce.external-ip"]; ok { - t.Fatal("platform.gce.external-ip is set without an external IP") + assertNodeAttributeEquals(t, node, "platform.gce.network.default.external-ip.0", "104.44.55.66") + assertNodeAttributeEquals(t, node, "platform.gce.network.default.external-ip.1", "104.44.55.67") + } else if _, ok := node.Attributes["platform.gce.network.default.external-ip.0"]; ok { + t.Fatal("platform.gce.network.default.external-ip is set without an external IP") } assertNodeAttributeEquals(t, node, "platform.gce.tag.abc", "true") @@ -165,11 +167,6 @@ const GCE_routes = ` "content-type": "text/plain", "body": "projects/555555/machineTypes/n1-standard-1" }, - { - "uri": "/computeMetadata/v1/instance/network-interfaces/0/ip", - "content-type": "text/plain", - "body": "10.240.0.5" - }, { "uri": "/computeMetadata/v1/instance/tags", "content-type": "application/json", From 6acdd9da085090ca38af1f07bb21a1680bf9b858 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Fri, 9 Oct 2015 09:10:40 -0500 Subject: [PATCH 10/13] Add a few more GCE-specific attributes: * cpu-platform * scheduling.automatic-restart * scheduling.on-host-maintenance * network.NETWORKNAME=true --- client/fingerprint/env_gce.go | 10 ++++++++-- client/fingerprint/env_gce_test.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index ceff998d786..9c6135c823b 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -132,6 +132,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) keys := []string{ "hostname", "id", + "cpu-platform", + "scheduling/automatic-restart", + "scheduling/on-host-maintenance", } for _, k := range keys { value, err := f.Get(k, false) @@ -140,7 +143,8 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } // assume we want blank entries - node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + key := strings.Replace(k, "/", ".", -1) + node.Attributes["platform.gce."+key] = strings.Trim(string(value), "\n") } // These keys need everything before the final slash removed to be usable. @@ -169,7 +173,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) } else { for _, intf := range interfaces { - prefix := "platform.gce.network." + lastToken(intf.Network) + network := lastToken(intf.Network) + prefix := "platform.gce.network." + network + node.Attributes[prefix] = "true" // newNetwork is populated and addded to the Nodes resources newNetwork := &structs.NetworkResource{ diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 460bd726fd3..47c0dece8d5 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -130,6 +130,7 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { } assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") + assertNodeAttributeEquals(t, node, "platform.gce.network.default", "true") assertNodeAttributeEquals(t, node, "platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { assertNodeAttributeEquals(t, node, "platform.gce.network.default.external-ip.0", "104.44.55.66") @@ -138,6 +139,9 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { t.Fatal("platform.gce.network.default.external-ip is set without an external IP") } + assertNodeAttributeEquals(t, node, "platform.gce.scheduling.automatic-restart", "TRUE") + assertNodeAttributeEquals(t, node, "platform.gce.scheduling.on-host-maintenance", "MIGRATE") + assertNodeAttributeEquals(t, node, "platform.gce.cpu-platform", "Intel Ivy Bridge") assertNodeAttributeEquals(t, node, "platform.gce.tag.abc", "true") assertNodeAttributeEquals(t, node, "platform.gce.tag.def", "true") assertNodeAttributeEquals(t, node, "platform.gce.attr.ghi", "111") @@ -176,6 +180,21 @@ const GCE_routes = ` "uri": "/computeMetadata/v1/instance/attributes/?recursive=true", "content-type": "application/json", "body": "{\"ghi\":\"111\",\"jkl\":\"222\"}" + }, + { + "uri": "/computeMetadata/v1/instance/scheduling/automatic-restart", + "content-type": "text/plain", + "body": "TRUE" + }, + { + "uri": "/computeMetadata/v1/instance/scheduling/on-host-maintenance", + "content-type": "text/plain", + "body": "MIGRATE" + }, + { + "uri": "/computeMetadata/v1/instance/cpu-platform", + "content-type": "text/plain", + "body": "Intel Ivy Bridge" } ] } From 203906b94a9f42a53af48e5ee996343d9d9d0a85 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Fri, 9 Oct 2015 18:34:57 -0500 Subject: [PATCH 11/13] GCE fingerprinter no longer updates network resources It has nothing to add that the generic fingerprinters aren't finding on their own already. --- client/fingerprint/env_gce.go | 19 ++----------------- client/fingerprint/env_gce_test.go | 16 ---------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 9c6135c823b..9b3d3bd6f1a 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -161,11 +161,6 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) node.Attributes["platform.gce."+k] = strings.Trim(lastToken(value), "\n") } - // Prepare to populate Node Network Resources - if node.Resources == nil { - node.Resources = &structs.Resources{} - } - // Get internal and external IPs (if they exist) value, err := f.Get("network-interfaces/", true) var interfaces []GCEMetadataNetworkInterface @@ -173,19 +168,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) } else { for _, intf := range interfaces { - network := lastToken(intf.Network) - prefix := "platform.gce.network." + network + prefix := "platform.gce.network." + lastToken(intf.Network) node.Attributes[prefix] = "true" - - // newNetwork is populated and addded to the Nodes resources - newNetwork := &structs.NetworkResource{ - IP: strings.Trim(intf.Ip, "\n"), - } - newNetwork.CIDR = newNetwork.IP + "/32" - node.Resources.Networks = append(node.Resources.Networks, newNetwork) - - node.Attributes["network.ip-address"] = newNetwork.IP - node.Attributes[prefix+".ip"] = newNetwork.IP + node.Attributes[prefix+".ip"] = strings.Trim(intf.Ip, "\n") for index, accessConfig := range intf.AccessConfigs { node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp } diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 47c0dece8d5..159563576b8 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -95,7 +95,6 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { "platform.gce.tag.def", "platform.gce.attr.ghi", "platform.gce.attr.jkl", - "network.ip-address", } for _, k := range keys { @@ -115,21 +114,6 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { assertNodeAttributeEquals(t, node, "platform.gce.hostname", "instance-1.c.project.internal") assertNodeAttributeEquals(t, node, "platform.gce.zone", "us-central1-f") assertNodeAttributeEquals(t, node, "platform.gce.machine-type", "n1-standard-1") - - if node.Resources == nil || len(node.Resources.Networks) == 0 { - t.Fatal("Expected to find Network Resources") - } - - // Test at least the first Network Resource - net := node.Resources.Networks[0] - if net.IP != "10.240.0.5" { - t.Fatalf("Expected Network Resource to have IP 10.240.0.5, saw %s", net.IP) - } - if net.CIDR != "10.240.0.5/32" { - t.Fatalf("Expected Network Resource to have CIDR 10.240.0.5/32, saw %s", net.CIDR) - } - - assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") assertNodeAttributeEquals(t, node, "platform.gce.network.default", "true") assertNodeAttributeEquals(t, node, "platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { From c43978bc4b3d74c8939b2a3ac9e9874fc929880b Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 12 Oct 2015 16:56:33 -0500 Subject: [PATCH 12/13] Fix old comments and other syntax cleanup --- client/fingerprint/env_aws.go | 4 ++-- client/fingerprint/env_gce.go | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index d47ee1ba8bd..a65933a1e61 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -62,12 +62,12 @@ var ec2InstanceSpeedMap = map[string]int{ "d2.8xlarge": 10000, } -// EnvAWSFingerprint is used to fingerprint the CPU +// EnvAWSFingerprint is used to fingerprint AWS metadata type EnvAWSFingerprint struct { logger *log.Logger } -// NewEnvAWSFingerprint is used to create a CPU fingerprint +// NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata func NewEnvAWSFingerprint(logger *log.Logger) Fingerprint { f := &EnvAWSFingerprint{logger: logger} return f diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 9b3d3bd6f1a..041a11791ef 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -43,14 +43,14 @@ func lastToken(s string) string { return s[index+1:] } -// EnvGCEFingerprint is used to fingerprint the CPU +// EnvGCEFingerprint is used to fingerprint GCE metadata type EnvGCEFingerprint struct { client *http.Client logger *log.Logger metadataURL string } -// NewEnvGCEFingerprint is used to create a CPU fingerprint +// NewEnvGCEFingerprint is used to create a fingerprint from GCE metadata func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { // Read the internal metadata URL from the environment, allowing test files to // provide their own @@ -184,10 +184,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } if err := json.Unmarshal([]byte(value), &tagList); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) - } else { - for _, tag := range tagList { - node.Attributes["platform.gce.tag."+tag] = "true" - } + } + for _, tag := range tagList { + node.Attributes["platform.gce.tag."+tag] = "true" } var attrDict map[string]string @@ -197,10 +196,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } if err := json.Unmarshal([]byte(value), &attrDict); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) - } else { - for k, v := range attrDict { - node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") - } + } + for k, v := range attrDict { + node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") } // populate Links From 263c5f5727711576ec5e2eb511a359c55aa7a2a6 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 12 Oct 2015 17:57:45 -0500 Subject: [PATCH 13/13] More syntax cleanup --- client/fingerprint/env_gce.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 041a11791ef..b20978db008 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -166,14 +166,14 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) var interfaces []GCEMetadataNetworkInterface if err := json.Unmarshal([]byte(value), &interfaces); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) - } else { - for _, intf := range interfaces { - prefix := "platform.gce.network." + lastToken(intf.Network) - node.Attributes[prefix] = "true" - node.Attributes[prefix+".ip"] = strings.Trim(intf.Ip, "\n") - for index, accessConfig := range intf.AccessConfigs { - node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp - } + } + + for _, intf := range interfaces { + prefix := "platform.gce.network." + lastToken(intf.Network) + node.Attributes[prefix] = "true" + node.Attributes[prefix+".ip"] = strings.Trim(intf.Ip, "\n") + for index, accessConfig := range intf.AccessConfigs { + node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp } }