From df904f6812be2e2d750b6a3ad24a34925c2aa2e4 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Mon, 9 Jan 2017 13:16:33 -0500 Subject: [PATCH] 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",