From 9352b649e800ab27140b0683283ddf16f6379f6e Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Thu, 5 Jan 2017 13:08:13 -0500 Subject: [PATCH 1/5] Preserve whether an instance's ports are enabled When transcribing the fargo.Instance struct to and from both JSON and XML, indicate and retain whether each port is enabled or not. Note that in the Java Eureka library, by default the insecure port is enabled and the secure port is disabled. Here the zero value of a fargo.Instance has the insecure port disabled as well. While it would be possible to invert the sense of the corresponding field to indicate whether the insecure port is disabled, so that the zero value of false matches the Java library's behavior, the resulting asymmetry with the secure port's field is too awkward. Instead, require registrants to enable the ports explicitly. This change removes the exported fargo.Port type and several exported fields of the fargo.Instance type: XMLName PortJ SecurePortJ None of these were exposed deliberately for use by callers; they were all exposed only as accidental consequences of the JSON and XML marshalling library's idiosyncrasies. --- marshal.go | 137 ++++++++++++++++++++++++++++++++++++------ net.go | 3 - struct.go | 31 ++++------ tests/marshal_test.go | 135 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 262 insertions(+), 44 deletions(-) diff --git a/marshal.go b/marshal.go index 5f72971..a6f9eac 100644 --- a/marshal.go +++ b/marshal.go @@ -76,22 +76,32 @@ func (a *Application) UnmarshalJSON(b []byte) error { return err } -type instance Instance +// jsonFormatPort describes an instance's network port, including whether its registrant considers +// the port to be enabled or disabled. +// +// Example JSON encoding: +// +// "port":{"@enabled":"true", "$":"7101"} +// +// Note that later versions of Eureka write the port number as a JSON number rather than as a +// decimal-formatted string. +type jsonFormatPort struct { + Number string `json:"$"` + Enabled string `json:"@enabled"` +} -// UnmarshalJSON is a custom JSON unmarshaler for Instance to deal with the -// different Port encodings between XML and JSON. Here we copy the values from the JSON -// Port struct into the simple XML int field. -func (i *Instance) UnmarshalJSON(b []byte) error { - var err error - var ii instance - if err = json.Unmarshal(b, &ii); err == nil { - marshalLog.Debug("Instance.UnmarshalJSON ii:%+v\n", ii) - *i = Instance(ii) - i.Port = parsePort(ii.PortJ.Number) - i.SecurePort = parsePort(ii.SecurePortJ.Number) - return nil +func boolAsString(b bool) string { + if b { + return "true" + } + return "false" +} + +func makeJSONFormatPort(port int, enabled bool) jsonFormatPort { + return jsonFormatPort{ + strconv.Itoa(port), + boolAsString(enabled), } - return err } func parsePort(s string) int { @@ -102,6 +112,100 @@ func parsePort(s string) int { return n } +func stringAsBool(s string) bool { + return s == "true" +} + +// UnmarshalJSON is a custom JSON unmarshaler for Instance, transcribing the two composite port +// specifications up to top-level fields. +func (i *Instance) UnmarshalJSON(b []byte) error { + type instance Instance + aux := struct { + *instance + Port jsonFormatPort `json:"port"` + SecurePort jsonFormatPort `json:"securePort"` + }{ + instance: (*instance)(i), + } + if err := json.Unmarshal(b, &aux); err != nil { + return err + } + i.Port = parsePort(aux.Port.Number) + i.PortEnabled = stringAsBool(aux.Port.Enabled) + i.SecurePort = parsePort(aux.SecurePort.Number) + i.SecurePortEnabled = stringAsBool(aux.SecurePort.Enabled) + return nil +} + +// MarshalJSON is a custom JSON marshaler for Instance, adapting the top-level raw port values to +// the composite port specifications. +func (i *Instance) MarshalJSON() ([]byte, error) { + type instance Instance + aux := struct { + *instance + Port jsonFormatPort `json:"port"` + SecurePort jsonFormatPort `json:"securePort"` + }{ + (*instance)(i), + makeJSONFormatPort(i.Port, i.PortEnabled), + makeJSONFormatPort(i.SecurePort, i.SecurePortEnabled), + } + return json.Marshal(&aux) +} + +// xmlFormatPort describes an instance's network port, including whether its registrant considers +// the port to be enabled or disabled. +// +// Example XML encoding: +// +// 7101 +type xmlFormatPort struct { + Number int `xml:",chardata"` + Enabled bool `xml:"enabled,attr"` +} + +// UnmarshalXML is a custom XML unmarshaler for Instance, transcribing the two composite port +// specifications up to top-level fields. +func (i *Instance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type instance Instance + aux := struct { + *instance + Port xmlFormatPort `xml:"port"` + SecurePort xmlFormatPort `xml:"securePort"` + }{ + instance: (*instance)(i), + } + if err := d.DecodeElement(&aux, &start); err != nil { + return err + } + i.Port = aux.Port.Number + i.PortEnabled = aux.Port.Enabled + i.SecurePort = aux.SecurePort.Number + i.SecurePortEnabled = aux.SecurePort.Enabled + return nil +} + +// startLocalName creates a start-tag of an XML element with the given local name and no namespace name. +func startLocalName(local string) xml.StartElement { + return xml.StartElement{Name: xml.Name{Space: "", Local: local}} +} + +// MarshalXML is a custom XML marshaler for Instance, adapting the top-level raw port values to +// the composite port specifications. +func (i *Instance) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + type instance Instance + aux := struct { + *instance + Port xmlFormatPort `xml:"port"` + SecurePort xmlFormatPort `xml:"securePort"` + }{ + instance: (*instance)(i), + Port: xmlFormatPort{i.Port, i.PortEnabled}, + SecurePort: xmlFormatPort{i.SecurePort, i.SecurePortEnabled}, + } + return e.EncodeElement(&aux, startLocalName("instance")) +} + // UnmarshalJSON is a custom JSON unmarshaler for InstanceMetadata to handle squirreling away // the raw JSON for later parsing. func (i *InstanceMetadata) UnmarshalJSON(b []byte) error { @@ -123,11 +227,6 @@ func (i *InstanceMetadata) MarshalJSON() ([]byte, error) { return i.Raw, nil } -// startLocalName creates a start-tag of an XML element with the given local name and no namespace name. -func startLocalName(local string) xml.StartElement { - return xml.StartElement{Name: xml.Name{Space: "", Local: local}} -} - // MarshalXML is a custom XML marshaler for InstanceMetadata. func (i InstanceMetadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error { tokens := []xml.Token{start} diff --git a/net.go b/net.go index 3e26c7c..c6c1b15 100644 --- a/net.go +++ b/net.go @@ -9,7 +9,6 @@ import ( "fmt" "math/rand" "net/http" - "strconv" "strings" "sync" "time" @@ -658,8 +657,6 @@ func (e *EurekaConnection) ReregisterInstance(ins *Instance) error { var out []byte var err error if e.UseJson { - ins.PortJ.Number = strconv.Itoa(ins.Port) - ins.SecurePortJ.Number = strconv.Itoa(ins.SecurePort) out, err = e.marshal(&RegisterInstanceJson{ins}) } else { out, err = e.marshal(ins) diff --git a/struct.go b/struct.go index 60feb75..d1ab7c1 100644 --- a/struct.go +++ b/struct.go @@ -40,7 +40,7 @@ type GetAppsResponse struct { VersionsDelta int `xml:"versions__delta" json:"versions__delta"` } -// Application deserializeable from Eureka JSON. +// GetAppResponseJson wraps an Application for deserializing from Eureka JSON. type GetAppResponseJson struct { Application Application `json:"application"` } @@ -74,22 +74,21 @@ type RegisterInstanceJson struct { Instance *Instance `json:"instance"` } -// Instance [de]serializeable [to|from] Eureka XML. +// Instance [de]serializeable [to|from] Eureka [XML|JSON]. type Instance struct { - XMLName struct{} `xml:"instance" json:"-"` - HostName string `xml:"hostName" json:"hostName"` - App string `xml:"app" json:"app"` - IPAddr string `xml:"ipAddr" json:"ipAddr"` - VipAddress string `xml:"vipAddress" json:"vipAddress"` - SecureVipAddress string `xml:"secureVipAddress" json:"secureVipAddress"` + HostName string `xml:"hostName" json:"hostName"` + App string `xml:"app" json:"app"` + IPAddr string `xml:"ipAddr" json:"ipAddr"` + VipAddress string `xml:"vipAddress" json:"vipAddress"` + SecureVipAddress string `xml:"secureVipAddress" json:"secureVipAddress"` Status StatusType `xml:"status" json:"status"` Overriddenstatus StatusType `xml:"overriddenstatus" json:"overriddenstatus"` - Port int `xml:"port" json:"-"` - PortJ Port `json:"port" xml:"-"` - SecurePort int `xml:"securePort" json:"-"` - SecurePortJ Port `json:"securePort" xml:"-"` + Port int `xml:"-" json:"-"` + PortEnabled bool `xml:"-" json:"-"` + SecurePort int `xml:"-" json:"-"` + SecurePortEnabled bool `xml:"-" json:"-"` HomePageUrl string `xml:"homePageUrl" json:"homePageUrl"` StatusPageUrl string `xml:"statusPageUrl" json:"statusPageUrl"` @@ -104,14 +103,6 @@ type Instance struct { UniqueID func(i Instance) string `xml:"-" json:"-"` } -// Port struct used for JSON [un]marshaling only. -// An example: -// "port":{"@enabled":"true", "$":"7101"} -type Port struct { - Number string `json:"$"` - Enabled string `json:"@enabled"` -} - // InstanceMetadata represents the eureka metadata, which is arbitrary XML. // See metadata.go for more info. type InstanceMetadata struct { diff --git a/tests/marshal_test.go b/tests/marshal_test.go index 7b4ec3c..1619459 100644 --- a/tests/marshal_test.go +++ b/tests/marshal_test.go @@ -45,9 +45,140 @@ func TestJsonMarshal(t *testing.T) { } } +func portsEqual(actual, expected *fargo.Instance) { + Convey("For the insecure port", func() { + So(actual.Port, ShouldEqual, expected.Port) + So(actual.PortEnabled, ShouldEqual, expected.PortEnabled) + + Convey("For the secure port", func() { + So(actual.SecurePort, ShouldEqual, expected.SecurePort) + So(actual.SecurePortEnabled, ShouldEqual, expected.SecurePortEnabled) + }) + }) +} + +func jsonEncodedInstanceHasPortsEqualTo(b []byte, expected *fargo.Instance) { + Convey("And reading them back should yield the equivalent value", func() { + var decoded fargo.Instance + err := json.Unmarshal(b, &decoded) + So(err, ShouldBeNil) + portsEqual(&decoded, expected) + }) +} + +func xmlEncodedInstanceHasPortsEqualTo(b []byte, expected *fargo.Instance) { + Convey("And reading them back should yield the equivalent value", func() { + var decoded fargo.Instance + err := xml.Unmarshal(b, &decoded) + So(err, ShouldBeNil) + portsEqual(&decoded, expected) + }) +} + +func TestPortsMarshal(t *testing.T) { + Convey("Given an Instance with only the insecure port enabled", t, func() { + ins := fargo.Instance{ + Port: 80, + PortEnabled: true, + } + + Convey("When the ports are marshalled as JSON", func() { + b, err := json.Marshal(&ins) + + Convey("The marshalled JSON should have these values", func() { + So(err, ShouldBeNil) + s := string(b) + So(s, ShouldContainSubstring, `,"port":{"$":"80","@enabled":"true"}`) + So(s, ShouldContainSubstring, `,"securePort":{"$":"0","@enabled":"false"}`) + + jsonEncodedInstanceHasPortsEqualTo(b, &ins) + }) + }) + + Convey("When the ports are marshalled as XML", func() { + b, err := xml.Marshal(&ins) + + Convey("The marshalled XML should have these values", func() { + So(err, ShouldBeNil) + s := string(b) + So(s, ShouldContainSubstring, `80`) + So(s, ShouldContainSubstring, `0`) + + xmlEncodedInstanceHasPortsEqualTo(b, &ins) + }) + }) + }) + Convey("Given an Instance with only the secure port enabled", t, func() { + ins := fargo.Instance{ + SecurePort: 443, + SecurePortEnabled: true, + } + + Convey("When the ports are marshalled as JSON", func() { + b, err := json.Marshal(&ins) + + Convey("The marshalled JSON should have these values", func() { + So(err, ShouldBeNil) + s := string(b) + So(s, ShouldContainSubstring, `,"port":{"$":"0","@enabled":"false"}`) + So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`) + + jsonEncodedInstanceHasPortsEqualTo(b, &ins) + }) + }) + + Convey("When the ports are marshalled as XML", func() { + b, err := xml.Marshal(&ins) + + Convey("The marshalled XML should have these values", func() { + So(err, ShouldBeNil) + s := string(b) + So(s, ShouldContainSubstring, `0`) + So(s, ShouldContainSubstring, `443`) + + xmlEncodedInstanceHasPortsEqualTo(b, &ins) + }) + }) + }) + Convey("Given an Instance with only the both ports enabled", t, func() { + ins := fargo.Instance{ + Port: 80, + PortEnabled: true, + SecurePort: 443, + SecurePortEnabled: true, + } + + Convey("When the ports are marshalled as JSON", func() { + b, err := json.Marshal(&ins) + + Convey("The marshalled JSON should have these values", func() { + So(err, ShouldBeNil) + s := string(b) + So(s, ShouldContainSubstring, `,"port":{"$":"80","@enabled":"true"}`) + So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`) + + jsonEncodedInstanceHasPortsEqualTo(b, &ins) + }) + }) + + Convey("When the ports are marshalled as XML", func() { + b, err := xml.Marshal(&ins) + + Convey("The marshalled XML should have these values", func() { + So(err, ShouldBeNil) + s := string(b) + So(s, ShouldContainSubstring, `80`) + So(s, ShouldContainSubstring, `443`) + + xmlEncodedInstanceHasPortsEqualTo(b, &ins) + }) + }) + }) +} + func TestMetadataMarshal(t *testing.T) { Convey("Given an Instance with metadata", t, func() { - ins := &fargo.Instance{} + ins := fargo.Instance{} ins.SetMetadataString("key1", "value1") ins.SetMetadataString("key2", "value2") @@ -75,7 +206,7 @@ func TestMetadataMarshal(t *testing.T) { func TestDataCenterInfoMarshal(t *testing.T) { Convey("Given an Instance situated in a data center", t, func() { - ins := &fargo.Instance{} + ins := fargo.Instance{} Convey("When the data center name is \"Amazon\"", func() { ins.DataCenterInfo.Name = fargo.Amazon From df904f6812be2e2d750b6a3ad24a34925c2aa2e4 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Mon, 9 Jan 2017 13:16:33 -0500 Subject: [PATCH 2/5] Accommodate Eureka version 1.22's format for ports In version 1.22, the Eureka server changed its JSON encoding for an instance's port numbers: instead of writing the port numbers as JSON string values, it started writing them as JSON number values. In order to accommodate Eureka servers of versions on either side of this change, accept instance port numbers as both strings and numbers, but continue to emit them as strings, which Eureka servers of all versions continue to accept. --- marshal.go | 168 +++++++++++------- .../apps-sample-1-1-post-v122.json | 49 +++++ tests/marshal_test.go | 17 +- tests/net_test.go | 4 + 4 files changed, 168 insertions(+), 70 deletions(-) create mode 100644 tests/marshal_sample/apps-sample-1-1-post-v122.json diff --git a/marshal.go b/marshal.go index a6f9eac..8bd3938 100644 --- a/marshal.go +++ b/marshal.go @@ -5,42 +5,66 @@ package fargo import ( "encoding/json" "encoding/xml" + "fmt" "io" "strconv" ) -// Temporary structs used for GetAppsResponse unmarshalling -type getAppsResponseArray GetAppsResponse -type getAppsResponseSingle struct { - Application *Application `json:"application"` - AppsHashcode string `json:"apps__hashcode"` - VersionsDelta int `json:"versions__delta"` +func intFromJSONNumberOrString(jv interface{}, description string) (int, error) { + switch v := jv.(type) { + case float64: + return int(v), nil + case string: + n, err := strconv.Atoi(v) + if err != nil { + return 0, err + } + return n, nil + default: + return 0, fmt.Errorf("unexpected %s: %[2]v (type %[2]T)", description, jv) + } } // UnmarshalJSON is a custom JSON unmarshaler for GetAppsResponse to deal with // sometimes non-wrapped Application arrays when there is only a single Application item. func (r *GetAppsResponse) UnmarshalJSON(b []byte) error { marshalLog.Debugf("GetAppsResponse.UnmarshalJSON b:%s\n", string(b)) - var err error + resolveDelta := func(d interface{}) (int, error) { + return intFromJSONNumberOrString(d, "versions delta") + } // Normal array case - var ra getAppsResponseArray - if err = json.Unmarshal(b, &ra); err == nil { - marshalLog.Debug("GetAppsResponse.UnmarshalJSON ra:%+v\n", ra) - *r = GetAppsResponse(ra) - return nil + type getAppsResponse GetAppsResponse + auxArray := struct { + *getAppsResponse + VersionsDelta interface{} `json:"versions__delta"` + }{ + getAppsResponse: (*getAppsResponse)(r), } + var err error + if err = json.Unmarshal(b, &auxArray); err == nil { + marshalLog.Debugf("GetAppsResponse.UnmarshalJSON array:%+v\n", auxArray) + r.VersionsDelta, err = resolveDelta(auxArray.VersionsDelta) + return err + } + // Bogus non-wrapped case - var rs getAppsResponseSingle - if err = json.Unmarshal(b, &rs); err == nil { - marshalLog.Debug("GetAppsResponse.UnmarshalJSON rs:%+v\n", rs) - r.Applications = make([]*Application, 1, 1) - r.Applications[0] = rs.Application - r.AppsHashcode = rs.AppsHashcode - r.VersionsDelta = rs.VersionsDelta - return nil + auxSingle := struct { + Application *Application `json:"application"` + AppsHashcode string `json:"apps__hashcode"` + VersionsDelta interface{} `json:"versions__delta"` + }{} + if err := json.Unmarshal(b, &auxSingle); err != nil { + return err } - return err + marshalLog.Debugf("GetAppsResponse.UnmarshalJSON single:%+v\n", auxSingle) + if r.VersionsDelta, err = resolveDelta(auxSingle.VersionsDelta); err != nil { + return err + } + r.Applications = make([]*Application, 1, 1) + r.Applications[0] = auxSingle.Application + r.AppsHashcode = auxSingle.AppsHashcode + return nil } // Temporary structs used for Application unmarshalling @@ -59,7 +83,7 @@ func (a *Application) UnmarshalJSON(b []byte) error { // Normal array case var aa applicationArray if err = json.Unmarshal(b, &aa); err == nil { - marshalLog.Debug("Application.UnmarshalJSON aa:%+v\n", aa) + marshalLog.Debugf("Application.UnmarshalJSON aa:%+v\n", aa) *a = Application(aa) return nil } @@ -67,7 +91,7 @@ func (a *Application) UnmarshalJSON(b []byte) error { // Bogus non-wrapped case var as applicationSingle if err = json.Unmarshal(b, &as); err == nil { - marshalLog.Debug("Application.UnmarshalJSON as:%+v\n", as) + marshalLog.Debugf("Application.UnmarshalJSON as:%+v\n", as) a.Name = as.Name a.Instances = make([]*Instance, 1, 1) a.Instances[0] = as.Instance @@ -76,42 +100,6 @@ func (a *Application) UnmarshalJSON(b []byte) error { return err } -// jsonFormatPort describes an instance's network port, including whether its registrant considers -// the port to be enabled or disabled. -// -// Example JSON encoding: -// -// "port":{"@enabled":"true", "$":"7101"} -// -// Note that later versions of Eureka write the port number as a JSON number rather than as a -// decimal-formatted string. -type jsonFormatPort struct { - Number string `json:"$"` - Enabled string `json:"@enabled"` -} - -func boolAsString(b bool) string { - if b { - return "true" - } - return "false" -} - -func makeJSONFormatPort(port int, enabled bool) jsonFormatPort { - return jsonFormatPort{ - strconv.Itoa(port), - boolAsString(enabled), - } -} - -func parsePort(s string) int { - n, err := strconv.Atoi(s) - if err != nil { - log.Warningf("Invalid port error: %s", err.Error()) - } - return n -} - func stringAsBool(s string) bool { return s == "true" } @@ -119,36 +107,78 @@ func stringAsBool(s string) bool { // UnmarshalJSON is a custom JSON unmarshaler for Instance, transcribing the two composite port // specifications up to top-level fields. func (i *Instance) UnmarshalJSON(b []byte) error { + // Preclude recursive calls to MarshalJSON. type instance Instance + // inboundJSONFormatPort describes an instance's network port, including whether its registrant + // considers the port to be enabled or disabled. + // + // Example JSON encoding: + // + // Eureka versions 1.2.1 and prior: + // "port":{"@enabled":"true", "$":"7101"} + // + // Eureka version 1.2.2 and later: + // "port":{"@enabled":"true", "$":7101} + // + // Note that later versions of Eureka write the port number as a JSON number rather than as a + // decimal-formatted string. We accept it as either an integer or a string. Strangely, the + // "@enabled" field remains a string. + type inboundJSONFormatPort struct { + Number interface{} `json:"$"` + Enabled bool `json:"@enabled,string"` + } aux := struct { *instance - Port jsonFormatPort `json:"port"` - SecurePort jsonFormatPort `json:"securePort"` + Port inboundJSONFormatPort `json:"port"` + SecurePort inboundJSONFormatPort `json:"securePort"` }{ instance: (*instance)(i), } if err := json.Unmarshal(b, &aux); err != nil { return err } - i.Port = parsePort(aux.Port.Number) - i.PortEnabled = stringAsBool(aux.Port.Enabled) - i.SecurePort = parsePort(aux.SecurePort.Number) - i.SecurePortEnabled = stringAsBool(aux.SecurePort.Enabled) + resolvePort := func(port interface{}) (int, error) { + return intFromJSONNumberOrString(port, "port number") + } + var err error + if i.Port, err = resolvePort(aux.Port.Number); err != nil { + return err + } + i.PortEnabled = aux.Port.Enabled + if i.SecurePort, err = resolvePort(aux.SecurePort.Number); err != nil { + return err + } + i.SecurePortEnabled = aux.SecurePort.Enabled return nil } // MarshalJSON is a custom JSON marshaler for Instance, adapting the top-level raw port values to // the composite port specifications. func (i *Instance) MarshalJSON() ([]byte, error) { + // Preclude recursive calls to MarshalJSON. type instance Instance + // outboundJSONFormatPort describes an instance's network port, including whether its registrant + // considers the port to be enabled or disabled. + // + // Example JSON encoding: + // + // "port":{"@enabled":"true", "$":"7101"} + // + // Note that later versions of Eureka write the port number as a JSON number rather than as a + // decimal-formatted string. We emit the port number as a string, not knowing the Eureka + // server's version. Strangely, the "@enabled" field remains a string. + type outboundJSONFormatPort struct { + Number int `json:"$,string"` + Enabled bool `json:"@enabled,string"` + } aux := struct { *instance - Port jsonFormatPort `json:"port"` - SecurePort jsonFormatPort `json:"securePort"` + Port outboundJSONFormatPort `json:"port"` + SecurePort outboundJSONFormatPort `json:"securePort"` }{ (*instance)(i), - makeJSONFormatPort(i.Port, i.PortEnabled), - makeJSONFormatPort(i.SecurePort, i.SecurePortEnabled), + outboundJSONFormatPort{i.Port, i.PortEnabled}, + outboundJSONFormatPort{i.SecurePort, i.SecurePortEnabled}, } return json.Marshal(&aux) } diff --git a/tests/marshal_sample/apps-sample-1-1-post-v122.json b/tests/marshal_sample/apps-sample-1-1-post-v122.json new file mode 100644 index 0000000..d0bafe6 --- /dev/null +++ b/tests/marshal_sample/apps-sample-1-1-post-v122.json @@ -0,0 +1,49 @@ +{ + "applications":{ + "versions__delta":"1", + "apps__hashcode":"UP_24_", + "application":[ + { + "name":"TESTENG.METRICSD", + "instance":{ + "hostName":"192.168.40.94:7101", + "app":"TESTENG.METRICSD", + "ipAddr":"192.168.40.94", + "status":"UP", + "overriddenstatus":"UP", + "port":{ + "@enabled":"true", + "$":7101 + }, + "securePort":{ + "@enabled":"true", + "$":7101 + }, + "countryId":1, + "dataCenterInfo":{ + "@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name":"MyOwn" + }, + "leaseInfo":{ + "renewalIntervalInSecs":30, + "durationInSecs":90, + "registrationTimestamp":1402450388650, + "lastRenewalTimestamp":1402552694257, + "evictionTimestamp":0, + "serviceUpTimestamp":1402450388774 + }, + "metadata":{ + "service-query-uri":"https://192.168.40.94:7101", + "base-uri":"https://192.168.40.94:7101", + "swagger-api-docs":"https://192.168.40.94:7101/api-docs" + }, + "vipAddress":"[]", + "isCoordinatingDiscoveryServer":false, + "lastUpdatedTimestamp":1402450388774, + "lastDirtyTimestamp":1402450388774, + "actionType":"MODIFIED" + } + } + ] + } +} diff --git a/tests/marshal_test.go b/tests/marshal_test.go index 1619459..85d6368 100644 --- a/tests/marshal_test.go +++ b/tests/marshal_test.go @@ -3,6 +3,7 @@ package fargo_test // MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl> import ( + "bytes" "encoding/json" "encoding/xml" "fmt" @@ -58,7 +59,7 @@ func portsEqual(actual, expected *fargo.Instance) { } func jsonEncodedInstanceHasPortsEqualTo(b []byte, expected *fargo.Instance) { - Convey("And reading them back should yield the equivalent value", func() { + Convey("Reading them back should yield the equivalent value", func() { var decoded fargo.Instance err := json.Unmarshal(b, &decoded) So(err, ShouldBeNil) @@ -92,6 +93,10 @@ func TestPortsMarshal(t *testing.T) { So(s, ShouldContainSubstring, `,"securePort":{"$":"0","@enabled":"false"}`) jsonEncodedInstanceHasPortsEqualTo(b, &ins) + + Convey("When the Eureka server is version 1.22 or later", func() { + jsonEncodedInstanceHasPortsEqualTo(bytes.Replace(b, []byte(`"80"`), []byte("80"), -1), &ins) + }) }) }) @@ -124,6 +129,10 @@ func TestPortsMarshal(t *testing.T) { So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`) jsonEncodedInstanceHasPortsEqualTo(b, &ins) + + Convey("When the Eureka server is version 1.22 or later", func() { + jsonEncodedInstanceHasPortsEqualTo(bytes.Replace(b, []byte(`"443"`), []byte("443"), -1), &ins) + }) }) }) @@ -158,6 +167,12 @@ func TestPortsMarshal(t *testing.T) { So(s, ShouldContainSubstring, `,"securePort":{"$":"443","@enabled":"true"}`) jsonEncodedInstanceHasPortsEqualTo(b, &ins) + + Convey("When the Eureka server is version 1.22 or later", func() { + b = bytes.Replace(b, []byte(`"80"`), []byte("80"), -1) + b = bytes.Replace(b, []byte(`"443"`), []byte("443"), -1) + jsonEncodedInstanceHasPortsEqualTo(b, &ins) + }) }) }) diff --git a/tests/net_test.go b/tests/net_test.go index 8eaf15c..bb24c56 100644 --- a/tests/net_test.go +++ b/tests/net_test.go @@ -218,6 +218,7 @@ func TestRegistration(t *testing.T) { i := fargo.Instance{ HostName: "i-123456", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10", @@ -231,6 +232,7 @@ func TestRegistration(t *testing.T) { j := fargo.Instance{ HostName: "i-6543", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10", @@ -264,6 +266,7 @@ func TestReregistration(t *testing.T) { i := fargo.Instance{ HostName: "i-123456", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10", @@ -305,6 +308,7 @@ func DontTestDeregistration(t *testing.T) { i := fargo.Instance{ HostName: "i-123456", Port: 9090, + PortEnabled: true, App: "TESTAPP", IPAddr: "127.0.0.10", VipAddress: "127.0.0.10", From 2b3ac93a8af887abddac9f6b850a428ca61009b3 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Tue, 10 Jan 2017 11:38:28 -0500 Subject: [PATCH 3/5] Satisfy Eureka 1.2.2 and later when registering As of Eureka version 1.2.2 and later, when registering an instance encoded as JSON, one must supply a value for the instance.dataCenterInfo.@class field, which must be set appropriately for the sibling "name" field's value. Take care of setting this value for the "Amazon" and "MyOwn" data center types, but allow callers to specify the value for their own custom data center types for which we cannot anticipate a suitable value. --- marshal.go | 27 +++++++++++----- net.go | 3 ++ struct.go | 8 +++++ tests/marshal_test.go | 41 ++++++++++++++++++++---- tests/net_test.go | 73 ++++++++++++++++++++++++++----------------- 5 files changed, 110 insertions(+), 42 deletions(-) diff --git a/marshal.go b/marshal.go index 8bd3938..7355954 100644 --- a/marshal.go +++ b/marshal.go @@ -320,7 +320,7 @@ func (m metadataMap) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error return nil } -func metadataValue(i DataCenterInfo) interface{} { +func metadataValue(i *DataCenterInfo) interface{} { if i.Name == Amazon { return i.Metadata } @@ -334,7 +334,7 @@ var ( // MarshalXML is a custom XML marshaler for DataCenterInfo, writing either Metadata or AlternateMetadata // depending on the type of data center indicated by the Name. -func (i DataCenterInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (i *DataCenterInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if err := e.EncodeToken(start); err != nil { return err } @@ -351,6 +351,7 @@ func (i DataCenterInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error type preliminaryDataCenterInfo struct { Name string `xml:"name" json:"name"` + Class string `xml:"-" json:"@class"` Metadata metadataMap `xml:"metadata" json:"metadata"` } @@ -378,6 +379,7 @@ func populateAmazonMetadata(dst *AmazonMetadataType, src map[string]string) { func adaptDataCenterInfo(dst *DataCenterInfo, src preliminaryDataCenterInfo) { dst.Name = src.Name + dst.Class = src.Class if src.Name == Amazon { populateAmazonMetadata(&dst.Metadata, src.Metadata) } else { @@ -400,20 +402,31 @@ func (i *DataCenterInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er // MarshalJSON is a custom JSON marshaler for DataCenterInfo, writing either Metadata or AlternateMetadata // depending on the type of data center indicated by the Name. -func (i DataCenterInfo) MarshalJSON() ([]byte, error) { +func (i *DataCenterInfo) MarshalJSON() ([]byte, error) { type named struct { - Name string `json:"name"` + Name string `json:"name"` + Class string `json:"@class"` } if i.Name == Amazon { return json.Marshal(struct { named Metadata AmazonMetadataType `json:"metadata"` - }{named{i.Name}, i.Metadata}) + }{ + named{i.Name, "com.netflix.appinfo.AmazonInfo"}, + i.Metadata, + }) + } + class := "com.netflix.appinfo.MyDataCenterInfo" + if i.Name != MyOwn { + class = i.Class } return json.Marshal(struct { named - Metadata map[string]string `json:"metadata"` - }{named{i.Name}, i.AlternateMetadata}) + Metadata map[string]string `json:"metadata,omitempty"` + }{ + named{i.Name, class}, + i.AlternateMetadata, + }) } // UnmarshalJSON is a custom JSON unmarshaler for DataCenterInfo, populating either Metadata or AlternateMetadata diff --git a/net.go b/net.go index c6c1b15..0efcc27 100644 --- a/net.go +++ b/net.go @@ -661,6 +661,9 @@ func (e *EurekaConnection) ReregisterInstance(ins *Instance) error { } else { out, err = e.marshal(ins) } + if err != nil { + return err + } body, rcode, err := postBody(reqURL, out, e.UseJson) if err != nil { diff --git a/struct.go b/struct.go index d1ab7c1..28b1a5a 100644 --- a/struct.go +++ b/struct.go @@ -132,6 +132,14 @@ type AmazonMetadataType struct { type DataCenterInfo struct { // Name indicates which type of data center hosts this instance. Name string + // Class indicates the Java class name representing this structure in the Eureka server, + // noted only when encoding communication with JSON. + // + // When registering an instance, if the name is neither "Amazon" nor "MyOwn", this field's + // value is used. Otherwise, a suitable default value will be supplied to the server. This field + // is available for specifying custom data center types other than the two built-in ones, for + // which no suitable default value could be known. + Class string // Metadata provides details specific to an Amazon data center, // populated and honored when the Name field's value is "Amazon". Metadata AmazonMetadataType diff --git a/tests/marshal_test.go b/tests/marshal_test.go index 85d6368..cc20056 100644 --- a/tests/marshal_test.go +++ b/tests/marshal_test.go @@ -225,6 +225,7 @@ func TestDataCenterInfoMarshal(t *testing.T) { Convey("When the data center name is \"Amazon\"", func() { ins.DataCenterInfo.Name = fargo.Amazon + ins.DataCenterInfo.Class = "ignored" ins.DataCenterInfo.Metadata.InstanceID = "123" ins.DataCenterInfo.Metadata.HostName = "expected.local" @@ -233,14 +234,16 @@ func TestDataCenterInfoMarshal(t *testing.T) { Convey("The marshalled JSON should have these values", func() { So(err, ShouldBeNil) - So(string(b), ShouldEqual, `{"name":"Amazon","metadata":{"ami-launch-index":"","local-hostname":"","availability-zone":"","instance-id":"123","public-ipv4":"","public-hostname":"","ami-manifest-path":"","local-ipv4":"","hostname":"expected.local","ami-id":"","instance-type":""}}`) + So(string(b), ShouldEqual, `{"name":"Amazon","@class":"com.netflix.appinfo.AmazonInfo","metadata":{"ami-launch-index":"","local-hostname":"","availability-zone":"","instance-id":"123","public-ipv4":"","public-hostname":"","ami-manifest-path":"","local-ipv4":"","hostname":"expected.local","ami-id":"","instance-type":""}}`) Convey("The value unmarshalled from JSON should have the same values as the original", func() { d := fargo.DataCenterInfo{} err := json.Unmarshal(b, &d) So(err, ShouldBeNil) - So(d, ShouldResemble, ins.DataCenterInfo) + expected := ins.DataCenterInfo + expected.Class = "com.netflix.appinfo.AmazonInfo" + So(d, ShouldResemble, expected) }) }) }) @@ -257,7 +260,9 @@ func TestDataCenterInfoMarshal(t *testing.T) { err := xml.Unmarshal(b, &d) So(err, ShouldBeNil) - So(d, ShouldResemble, ins.DataCenterInfo) + expected := ins.DataCenterInfo + expected.Class = "" + So(d, ShouldResemble, expected) }) }) }) @@ -265,17 +270,39 @@ func TestDataCenterInfoMarshal(t *testing.T) { Convey("When the data center name is not \"Amazon\"", func() { ins.DataCenterInfo.Name = fargo.MyOwn + ins.DataCenterInfo.Class = "ignored" ins.DataCenterInfo.AlternateMetadata = map[string]string{ "instanceId": "123", "hostName": "expected.local", } - Convey("When the data center info is marshalled as JSON", func() { + Convey("When the data center info has no class specified and is marshalled as JSON", func() { + b, err := json.Marshal(&ins.DataCenterInfo) + + Convey("The marshalled JSON should have these values", func() { + So(err, ShouldBeNil) + So(string(b), ShouldEqual, `{"name":"MyOwn","@class":"com.netflix.appinfo.MyDataCenterInfo","metadata":{"hostName":"expected.local","instanceId":"123"}}`) + + Convey("The value unmarshalled from JSON should have the same values as the original", func() { + d := fargo.DataCenterInfo{} + err := json.Unmarshal(b, &d) + + So(err, ShouldBeNil) + expected := ins.DataCenterInfo + expected.Class = "com.netflix.appinfo.MyDataCenterInfo" + So(d, ShouldResemble, expected) + }) + }) + }) + + Convey("When the data center info has both a custom name and class specified and is marshalled as JSON", func() { + ins.DataCenterInfo.Name = "Custom" + ins.DataCenterInfo.Class = "custom" b, err := json.Marshal(&ins.DataCenterInfo) Convey("The marshalled JSON should have these values", func() { So(err, ShouldBeNil) - So(string(b), ShouldEqual, `{"name":"MyOwn","metadata":{"hostName":"expected.local","instanceId":"123"}}`) + So(string(b), ShouldEqual, `{"name":"Custom","@class":"custom","metadata":{"hostName":"expected.local","instanceId":"123"}}`) Convey("The value unmarshalled from JSON should have the same values as the original", func() { d := fargo.DataCenterInfo{} @@ -301,7 +328,9 @@ func TestDataCenterInfoMarshal(t *testing.T) { err := xml.Unmarshal(b, &d) So(err, ShouldBeNil) - So(d, ShouldResemble, ins.DataCenterInfo) + expected := ins.DataCenterInfo + expected.Class = "" + So(d, ShouldResemble, expected) }) }) }) diff --git a/tests/net_test.go b/tests/net_test.go index bb24c56..fa7de84 100644 --- a/tests/net_test.go +++ b/tests/net_test.go @@ -279,25 +279,25 @@ func TestReregistration(t *testing.T) { Convey("Instance registers correctly", func() { err := e.RegisterInstance(&i) So(err, ShouldBeNil) - }) - }) - Convey("Reregister the TESTAPP instance", t, func() { - Convey("Instance reregisters correctly", func() { - err := e.ReregisterInstance(&i) - So(err, ShouldBeNil) - }) + Convey("Reregister the TESTAPP instance", func() { + Convey("Instance reregisters correctly", func() { + err := e.ReregisterInstance(&i) + So(err, ShouldBeNil) - Convey("Instance can check in", func() { - err := e.HeartBeatInstance(&i) - So(err, ShouldBeNil) - }) + Convey("Instance can check in", func() { + err := e.HeartBeatInstance(&i) + So(err, ShouldBeNil) + }) - Convey("Instance can be gotten correctly", func() { - ii, err := e.GetInstance(i.App, i.HostName) - So(err, ShouldBeNil) - So(ii.App, ShouldEqual, i.App) - So(ii.HostName, ShouldEqual, i.HostName) + Convey("Instance can be gotten correctly", func() { + ii, err := e.GetInstance(i.App, i.HostName) + So(err, ShouldBeNil) + So(ii.App, ShouldEqual, i.App) + So(ii.HostName, ShouldEqual, i.HostName) + }) + }) + }) }) }) } @@ -370,6 +370,17 @@ func TestUpdateStatus(t *testing.T) { func TestMetadataReading(t *testing.T) { e, _ := fargo.NewConnFromConfigFile("./config_sample/local.gcfg") + i := fargo.Instance{ + HostName: "i-123456", + Port: 9090, + PortEnabled: true, + App: "TESTAPP", + IPAddr: "127.0.0.10", + VipAddress: "127.0.0.10", + DataCenterInfo: fargo.DataCenterInfo{Name: fargo.MyOwn}, + SecureVipAddress: "127.0.0.10", + Status: fargo.UP, + } for _, j := range []bool{false, true} { e.UseJson = j Convey("Read empty instance metadata", t, func() { @@ -379,19 +390,23 @@ func TestMetadataReading(t *testing.T) { _, err = i.Metadata.GetString("SomeProp") So(err, ShouldBeNil) }) - Convey("Read valid instance metadata", t, func() { - a, err := e.GetApp("TESTAPP") - So(err, ShouldBeNil) - So(len(a.Instances), ShouldBeGreaterThan, 0) - if len(a.Instances) == 0 { - return - } - i := a.Instances[0] - err = e.AddMetadataString(i, "SomeProp", "AValue") - So(err, ShouldBeNil) - v, err := i.Metadata.GetString("SomeProp") - So(err, ShouldBeNil) - So(v, ShouldEqual, "AValue") + Convey("Register an instance to TESTAPP", t, func() { + Convey("Instance registers correctly", func() { + err := e.RegisterInstance(&i) + So(err, ShouldBeNil) + + Convey("Read valid instance metadata", func() { + a, err := e.GetApp("TESTAPP") + So(err, ShouldBeNil) + So(len(a.Instances), ShouldBeGreaterThan, 0) + i := a.Instances[0] + err = e.AddMetadataString(i, "SomeProp", "AValue") + So(err, ShouldBeNil) + v, err := i.Metadata.GetString("SomeProp") + So(err, ShouldBeNil) + So(v, ShouldEqual, "AValue") + }) + }) }) } } From 6e72b26e3ddce6029906aa5f90611a062a9b9ff8 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Tue, 10 Jan 2017 16:40:23 -0500 Subject: [PATCH 4/5] Accommodate JSON data center metadata translation When the Eureka server encodes an instance as JSON, it will sometimes write the data center metadata values that look like numbers as JSON numbers, rather than writing all metadata values as strings. Demanding that all metadata values arrive as JSON strings precludes proper decoding of instances, so be more flexible by first decoding the metadata values as any JSON type, and then projecting the values that are not strings back to strings. Note that we only project decoded Go types bool, float64, and nil; while it's possible that Go can decode a value of type []interface{} or map[string]interface{}, we make no effort to project those values. --- marshal.go | 40 ++++++++++++++++++++++++++++++++++------ metadata.go | 2 +- tests/marshal_test.go | 10 ++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/marshal.go b/marshal.go index 7355954..7a634be 100644 --- a/marshal.go +++ b/marshal.go @@ -377,7 +377,7 @@ func populateAmazonMetadata(dst *AmazonMetadataType, src map[string]string) { bindValue(&dst.InstanceType, src, "instance-type") } -func adaptDataCenterInfo(dst *DataCenterInfo, src preliminaryDataCenterInfo) { +func adaptDataCenterInfo(dst *DataCenterInfo, src *preliminaryDataCenterInfo) { dst.Name = src.Name dst.Class = src.Class if src.Name == Amazon { @@ -396,7 +396,7 @@ func (i *DataCenterInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er if err := d.DecodeElement(&p, &start); err != nil { return err } - adaptDataCenterInfo(i, p) + adaptDataCenterInfo(i, &p) return nil } @@ -429,15 +429,43 @@ func (i *DataCenterInfo) MarshalJSON() ([]byte, error) { }) } +func jsonValueAsString(i interface{}) string { + switch v := i.(type) { + case string: + return v + case float64: + return fmt.Sprintf("%.f", v) + case bool: + return strconv.FormatBool(v) + case []interface{}, map[string]interface{}: + // Don't bother trying to decode these. + return "" + case nil: + return "" + default: + panic("type of unexpected value") + } +} + // UnmarshalJSON is a custom JSON unmarshaler for DataCenterInfo, populating either Metadata or AlternateMetadata // depending on the type of data center indicated by the Name. func (i *DataCenterInfo) UnmarshalJSON(b []byte) error { - p := preliminaryDataCenterInfo{ - Metadata: make(map[string]string, 11), + // The Eureka server will mistakenly convert metadata values that look like numbers to JSON numbers. + // Convert them back to strings. + aux := struct { + *preliminaryDataCenterInfo + PreliminaryMetadata map[string]interface{} `json:"metadata"` + }{ + PreliminaryMetadata: make(map[string]interface{}, 11), } - if err := json.Unmarshal(b, &p); err != nil { + if err := json.Unmarshal(b, &aux); err != nil { return err } - adaptDataCenterInfo(i, p) + metadata := make(map[string]string, len(aux.PreliminaryMetadata)) + for k, v := range aux.PreliminaryMetadata { + metadata[k] = jsonValueAsString(v) + } + aux.Metadata = metadata + adaptDataCenterInfo(i, aux.preliminaryDataCenterInfo) return nil } diff --git a/metadata.go b/metadata.go index f1af0be..2efec06 100644 --- a/metadata.go +++ b/metadata.go @@ -5,6 +5,7 @@ package fargo import ( "encoding/json" "fmt" + "github.com/clbanning/x2j" ) @@ -32,7 +33,6 @@ func (ins *Instance) SetMetadataString(key, value string) { func (im *InstanceMetadata) parse() error { if len(im.Raw) == 0 { im.parsed = make(map[string]interface{}) - log.Debug("len(Metadata)==0. Quitting parsing.") return nil } metadataLog.Debugf("InstanceMetadata.parse: %s", im.Raw) diff --git a/tests/marshal_test.go b/tests/marshal_test.go index cc20056..28896d5 100644 --- a/tests/marshal_test.go +++ b/tests/marshal_test.go @@ -310,6 +310,16 @@ func TestDataCenterInfoMarshal(t *testing.T) { So(err, ShouldBeNil) So(d, ShouldResemble, ins.DataCenterInfo) + + Convey("Even if the server translates strings to other types", func() { + translated := bytes.Replace(b, []byte(`"123"`), []byte("123"), 1) + + d := fargo.DataCenterInfo{} + err := json.Unmarshal(translated, &d) + + So(err, ShouldBeNil) + So(d, ShouldResemble, ins.DataCenterInfo) + }) }) }) }) From f1f7c88ad0e3cca61c8a774c5f3c58f3fae53498 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Tue, 10 Jan 2017 08:24:35 -0500 Subject: [PATCH 5/5] Prevent panic in tests whose queries fail Some tests were ignoring errors and proceeding to inspect query results, causing failure via panic. Instead, fail these tests via more explicit assertions. --- tests/net_test.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/net_test.go b/tests/net_test.go index fa7de84..3a06e43 100644 --- a/tests/net_test.go +++ b/tests/net_test.go @@ -92,7 +92,9 @@ func TestConnectionCreation(t *testing.T) { e := fargo.NewConnFromConfig(cfg) apps, err := e.GetApps() So(err, ShouldBeNil) - So(len(apps["EUREKA"].Instances), ShouldEqual, 2) + app := apps["EUREKA"] + So(app, ShouldNotBeNil) + So(len(app.Instances), ShouldEqual, 2) }) } @@ -101,15 +103,18 @@ func TestGetApps(t *testing.T) { for _, j := range []bool{false, true} { e.UseJson = j Convey("Pull applications", t, func() { - a, err := e.GetApps() + apps, err := e.GetApps() So(err, ShouldBeNil) - So(len(a["EUREKA"].Instances), ShouldEqual, 2) + app := apps["EUREKA"] + So(app, ShouldNotBeNil) + So(len(app.Instances), ShouldEqual, 2) }) Convey("Pull single application", t, func() { - a, err := e.GetApp("EUREKA") + app, err := e.GetApp("EUREKA") So(err, ShouldBeNil) - So(len(a.Instances), ShouldEqual, 2) - for _, ins := range a.Instances { + So(app, ShouldNotBeNil) + So(len(app.Instances), ShouldEqual, 2) + for _, ins := range app.Instances { So(ins.IPAddr, ShouldBeIn, []string{"172.17.0.2", "172.17.0.3"}) } }) @@ -165,7 +170,7 @@ func TestGetSingleInstanceByVIPAddress(t *testing.T) { So(instances, ShouldHaveLength, 1) Convey("And selecting instances with status UP should provide none", func() { // Ensure that we tolerate a nil option safely. - instances, err := e.GetInstancesByVIPAddress(vipAddress, fargo.ThatAreUp, nil) + instances, err := e.GetInstancesByVIPAddress(vipAddress, false, fargo.ThatAreUp, nil) So(err, ShouldBeNil) So(instances, ShouldBeEmpty) }) @@ -199,7 +204,7 @@ func TestGetMultipleInstancesByVIPAddress(t *testing.T) { Convey("requesting the instances by that VIP address should provide them", func() { time.Sleep(cacheDelay) vipAddress := "app" - instances, err := e.GetInstancesByVIPAddress(vipAddress) + instances, err := e.GetInstancesByVIPAddress(vipAddress, false) So(err, ShouldBeNil) So(instances, ShouldHaveLength, 2) for _, ins := range instances {