Skip to content

Commit b8c9783

Browse files
author
Damian Turczyński
authored
Merge pull request #59 from seh/preserve-port-enabled-status
Preserve an instance's enabled status for its two ports
2 parents e8c3b0f + f1f7c88 commit b8c9783

File tree

7 files changed

+554
-127
lines changed

7 files changed

+554
-127
lines changed

marshal.go

Lines changed: 228 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,66 @@ package fargo
55
import (
66
"encoding/json"
77
"encoding/xml"
8+
"fmt"
89
"io"
910
"strconv"
1011
)
1112

12-
// Temporary structs used for GetAppsResponse unmarshalling
13-
type getAppsResponseArray GetAppsResponse
14-
type getAppsResponseSingle struct {
15-
Application *Application `json:"application"`
16-
AppsHashcode string `json:"apps__hashcode"`
17-
VersionsDelta int `json:"versions__delta"`
13+
func intFromJSONNumberOrString(jv interface{}, description string) (int, error) {
14+
switch v := jv.(type) {
15+
case float64:
16+
return int(v), nil
17+
case string:
18+
n, err := strconv.Atoi(v)
19+
if err != nil {
20+
return 0, err
21+
}
22+
return n, nil
23+
default:
24+
return 0, fmt.Errorf("unexpected %s: %[2]v (type %[2]T)", description, jv)
25+
}
1826
}
1927

2028
// UnmarshalJSON is a custom JSON unmarshaler for GetAppsResponse to deal with
2129
// sometimes non-wrapped Application arrays when there is only a single Application item.
2230
func (r *GetAppsResponse) UnmarshalJSON(b []byte) error {
2331
marshalLog.Debugf("GetAppsResponse.UnmarshalJSON b:%s\n", string(b))
24-
var err error
32+
resolveDelta := func(d interface{}) (int, error) {
33+
return intFromJSONNumberOrString(d, "versions delta")
34+
}
2535

2636
// Normal array case
27-
var ra getAppsResponseArray
28-
if err = json.Unmarshal(b, &ra); err == nil {
29-
marshalLog.Debug("GetAppsResponse.UnmarshalJSON ra:%+v\n", ra)
30-
*r = GetAppsResponse(ra)
31-
return nil
37+
type getAppsResponse GetAppsResponse
38+
auxArray := struct {
39+
*getAppsResponse
40+
VersionsDelta interface{} `json:"versions__delta"`
41+
}{
42+
getAppsResponse: (*getAppsResponse)(r),
3243
}
44+
var err error
45+
if err = json.Unmarshal(b, &auxArray); err == nil {
46+
marshalLog.Debugf("GetAppsResponse.UnmarshalJSON array:%+v\n", auxArray)
47+
r.VersionsDelta, err = resolveDelta(auxArray.VersionsDelta)
48+
return err
49+
}
50+
3351
// Bogus non-wrapped case
34-
var rs getAppsResponseSingle
35-
if err = json.Unmarshal(b, &rs); err == nil {
36-
marshalLog.Debug("GetAppsResponse.UnmarshalJSON rs:%+v\n", rs)
37-
r.Applications = make([]*Application, 1, 1)
38-
r.Applications[0] = rs.Application
39-
r.AppsHashcode = rs.AppsHashcode
40-
r.VersionsDelta = rs.VersionsDelta
41-
return nil
52+
auxSingle := struct {
53+
Application *Application `json:"application"`
54+
AppsHashcode string `json:"apps__hashcode"`
55+
VersionsDelta interface{} `json:"versions__delta"`
56+
}{}
57+
if err := json.Unmarshal(b, &auxSingle); err != nil {
58+
return err
4259
}
43-
return err
60+
marshalLog.Debugf("GetAppsResponse.UnmarshalJSON single:%+v\n", auxSingle)
61+
if r.VersionsDelta, err = resolveDelta(auxSingle.VersionsDelta); err != nil {
62+
return err
63+
}
64+
r.Applications = make([]*Application, 1, 1)
65+
r.Applications[0] = auxSingle.Application
66+
r.AppsHashcode = auxSingle.AppsHashcode
67+
return nil
4468
}
4569

4670
// Temporary structs used for Application unmarshalling
@@ -59,15 +83,15 @@ func (a *Application) UnmarshalJSON(b []byte) error {
5983
// Normal array case
6084
var aa applicationArray
6185
if err = json.Unmarshal(b, &aa); err == nil {
62-
marshalLog.Debug("Application.UnmarshalJSON aa:%+v\n", aa)
86+
marshalLog.Debugf("Application.UnmarshalJSON aa:%+v\n", aa)
6387
*a = Application(aa)
6488
return nil
6589
}
6690

6791
// Bogus non-wrapped case
6892
var as applicationSingle
6993
if err = json.Unmarshal(b, &as); err == nil {
70-
marshalLog.Debug("Application.UnmarshalJSON as:%+v\n", as)
94+
marshalLog.Debugf("Application.UnmarshalJSON as:%+v\n", as)
7195
a.Name = as.Name
7296
a.Instances = make([]*Instance, 1, 1)
7397
a.Instances[0] = as.Instance
@@ -76,30 +100,140 @@ func (a *Application) UnmarshalJSON(b []byte) error {
76100
return err
77101
}
78102

79-
type instance Instance
103+
func stringAsBool(s string) bool {
104+
return s == "true"
105+
}
80106

81-
// UnmarshalJSON is a custom JSON unmarshaler for Instance to deal with the
82-
// different Port encodings between XML and JSON. Here we copy the values from the JSON
83-
// Port struct into the simple XML int field.
107+
// UnmarshalJSON is a custom JSON unmarshaler for Instance, transcribing the two composite port
108+
// specifications up to top-level fields.
84109
func (i *Instance) UnmarshalJSON(b []byte) error {
110+
// Preclude recursive calls to MarshalJSON.
111+
type instance Instance
112+
// inboundJSONFormatPort describes an instance's network port, including whether its registrant
113+
// considers the port to be enabled or disabled.
114+
//
115+
// Example JSON encoding:
116+
//
117+
// Eureka versions 1.2.1 and prior:
118+
// "port":{"@enabled":"true", "$":"7101"}
119+
//
120+
// Eureka version 1.2.2 and later:
121+
// "port":{"@enabled":"true", "$":7101}
122+
//
123+
// Note that later versions of Eureka write the port number as a JSON number rather than as a
124+
// decimal-formatted string. We accept it as either an integer or a string. Strangely, the
125+
// "@enabled" field remains a string.
126+
type inboundJSONFormatPort struct {
127+
Number interface{} `json:"$"`
128+
Enabled bool `json:"@enabled,string"`
129+
}
130+
aux := struct {
131+
*instance
132+
Port inboundJSONFormatPort `json:"port"`
133+
SecurePort inboundJSONFormatPort `json:"securePort"`
134+
}{
135+
instance: (*instance)(i),
136+
}
137+
if err := json.Unmarshal(b, &aux); err != nil {
138+
return err
139+
}
140+
resolvePort := func(port interface{}) (int, error) {
141+
return intFromJSONNumberOrString(port, "port number")
142+
}
85143
var err error
86-
var ii instance
87-
if err = json.Unmarshal(b, &ii); err == nil {
88-
marshalLog.Debug("Instance.UnmarshalJSON ii:%+v\n", ii)
89-
*i = Instance(ii)
90-
i.Port = parsePort(ii.PortJ.Number)
91-
i.SecurePort = parsePort(ii.SecurePortJ.Number)
92-
return nil
144+
if i.Port, err = resolvePort(aux.Port.Number); err != nil {
145+
return err
93146
}
94-
return err
147+
i.PortEnabled = aux.Port.Enabled
148+
if i.SecurePort, err = resolvePort(aux.SecurePort.Number); err != nil {
149+
return err
150+
}
151+
i.SecurePortEnabled = aux.SecurePort.Enabled
152+
return nil
95153
}
96154

97-
func parsePort(s string) int {
98-
n, err := strconv.Atoi(s)
99-
if err != nil {
100-
log.Warningf("Invalid port error: %s", err.Error())
155+
// MarshalJSON is a custom JSON marshaler for Instance, adapting the top-level raw port values to
156+
// the composite port specifications.
157+
func (i *Instance) MarshalJSON() ([]byte, error) {
158+
// Preclude recursive calls to MarshalJSON.
159+
type instance Instance
160+
// outboundJSONFormatPort describes an instance's network port, including whether its registrant
161+
// considers the port to be enabled or disabled.
162+
//
163+
// Example JSON encoding:
164+
//
165+
// "port":{"@enabled":"true", "$":"7101"}
166+
//
167+
// Note that later versions of Eureka write the port number as a JSON number rather than as a
168+
// decimal-formatted string. We emit the port number as a string, not knowing the Eureka
169+
// server's version. Strangely, the "@enabled" field remains a string.
170+
type outboundJSONFormatPort struct {
171+
Number int `json:"$,string"`
172+
Enabled bool `json:"@enabled,string"`
173+
}
174+
aux := struct {
175+
*instance
176+
Port outboundJSONFormatPort `json:"port"`
177+
SecurePort outboundJSONFormatPort `json:"securePort"`
178+
}{
179+
(*instance)(i),
180+
outboundJSONFormatPort{i.Port, i.PortEnabled},
181+
outboundJSONFormatPort{i.SecurePort, i.SecurePortEnabled},
101182
}
102-
return n
183+
return json.Marshal(&aux)
184+
}
185+
186+
// xmlFormatPort describes an instance's network port, including whether its registrant considers
187+
// the port to be enabled or disabled.
188+
//
189+
// Example XML encoding:
190+
//
191+
// <port enabled="true">7101</port>
192+
type xmlFormatPort struct {
193+
Number int `xml:",chardata"`
194+
Enabled bool `xml:"enabled,attr"`
195+
}
196+
197+
// UnmarshalXML is a custom XML unmarshaler for Instance, transcribing the two composite port
198+
// specifications up to top-level fields.
199+
func (i *Instance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
200+
type instance Instance
201+
aux := struct {
202+
*instance
203+
Port xmlFormatPort `xml:"port"`
204+
SecurePort xmlFormatPort `xml:"securePort"`
205+
}{
206+
instance: (*instance)(i),
207+
}
208+
if err := d.DecodeElement(&aux, &start); err != nil {
209+
return err
210+
}
211+
i.Port = aux.Port.Number
212+
i.PortEnabled = aux.Port.Enabled
213+
i.SecurePort = aux.SecurePort.Number
214+
i.SecurePortEnabled = aux.SecurePort.Enabled
215+
return nil
216+
}
217+
218+
// startLocalName creates a start-tag of an XML element with the given local name and no namespace name.
219+
func startLocalName(local string) xml.StartElement {
220+
return xml.StartElement{Name: xml.Name{Space: "", Local: local}}
221+
}
222+
223+
// MarshalXML is a custom XML marshaler for Instance, adapting the top-level raw port values to
224+
// the composite port specifications.
225+
func (i *Instance) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
226+
type instance Instance
227+
aux := struct {
228+
*instance
229+
Port xmlFormatPort `xml:"port"`
230+
SecurePort xmlFormatPort `xml:"securePort"`
231+
}{
232+
instance: (*instance)(i),
233+
Port: xmlFormatPort{i.Port, i.PortEnabled},
234+
SecurePort: xmlFormatPort{i.SecurePort, i.SecurePortEnabled},
235+
}
236+
return e.EncodeElement(&aux, startLocalName("instance"))
103237
}
104238

105239
// UnmarshalJSON is a custom JSON unmarshaler for InstanceMetadata to handle squirreling away
@@ -123,11 +257,6 @@ func (i *InstanceMetadata) MarshalJSON() ([]byte, error) {
123257
return i.Raw, nil
124258
}
125259

126-
// startLocalName creates a start-tag of an XML element with the given local name and no namespace name.
127-
func startLocalName(local string) xml.StartElement {
128-
return xml.StartElement{Name: xml.Name{Space: "", Local: local}}
129-
}
130-
131260
// MarshalXML is a custom XML marshaler for InstanceMetadata.
132261
func (i InstanceMetadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
133262
tokens := []xml.Token{start}
@@ -191,7 +320,7 @@ func (m metadataMap) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
191320
return nil
192321
}
193322

194-
func metadataValue(i DataCenterInfo) interface{} {
323+
func metadataValue(i *DataCenterInfo) interface{} {
195324
if i.Name == Amazon {
196325
return i.Metadata
197326
}
@@ -205,7 +334,7 @@ var (
205334

206335
// MarshalXML is a custom XML marshaler for DataCenterInfo, writing either Metadata or AlternateMetadata
207336
// depending on the type of data center indicated by the Name.
208-
func (i DataCenterInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
337+
func (i *DataCenterInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
209338
if err := e.EncodeToken(start); err != nil {
210339
return err
211340
}
@@ -222,6 +351,7 @@ func (i DataCenterInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error
222351

223352
type preliminaryDataCenterInfo struct {
224353
Name string `xml:"name" json:"name"`
354+
Class string `xml:"-" json:"@class"`
225355
Metadata metadataMap `xml:"metadata" json:"metadata"`
226356
}
227357

@@ -247,8 +377,9 @@ func populateAmazonMetadata(dst *AmazonMetadataType, src map[string]string) {
247377
bindValue(&dst.InstanceType, src, "instance-type")
248378
}
249379

250-
func adaptDataCenterInfo(dst *DataCenterInfo, src preliminaryDataCenterInfo) {
380+
func adaptDataCenterInfo(dst *DataCenterInfo, src *preliminaryDataCenterInfo) {
251381
dst.Name = src.Name
382+
dst.Class = src.Class
252383
if src.Name == Amazon {
253384
populateAmazonMetadata(&dst.Metadata, src.Metadata)
254385
} else {
@@ -265,37 +396,76 @@ func (i *DataCenterInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er
265396
if err := d.DecodeElement(&p, &start); err != nil {
266397
return err
267398
}
268-
adaptDataCenterInfo(i, p)
399+
adaptDataCenterInfo(i, &p)
269400
return nil
270401
}
271402

272403
// MarshalJSON is a custom JSON marshaler for DataCenterInfo, writing either Metadata or AlternateMetadata
273404
// depending on the type of data center indicated by the Name.
274-
func (i DataCenterInfo) MarshalJSON() ([]byte, error) {
405+
func (i *DataCenterInfo) MarshalJSON() ([]byte, error) {
275406
type named struct {
276-
Name string `json:"name"`
407+
Name string `json:"name"`
408+
Class string `json:"@class"`
277409
}
278410
if i.Name == Amazon {
279411
return json.Marshal(struct {
280412
named
281413
Metadata AmazonMetadataType `json:"metadata"`
282-
}{named{i.Name}, i.Metadata})
414+
}{
415+
named{i.Name, "com.netflix.appinfo.AmazonInfo"},
416+
i.Metadata,
417+
})
418+
}
419+
class := "com.netflix.appinfo.MyDataCenterInfo"
420+
if i.Name != MyOwn {
421+
class = i.Class
283422
}
284423
return json.Marshal(struct {
285424
named
286-
Metadata map[string]string `json:"metadata"`
287-
}{named{i.Name}, i.AlternateMetadata})
425+
Metadata map[string]string `json:"metadata,omitempty"`
426+
}{
427+
named{i.Name, class},
428+
i.AlternateMetadata,
429+
})
430+
}
431+
432+
func jsonValueAsString(i interface{}) string {
433+
switch v := i.(type) {
434+
case string:
435+
return v
436+
case float64:
437+
return fmt.Sprintf("%.f", v)
438+
case bool:
439+
return strconv.FormatBool(v)
440+
case []interface{}, map[string]interface{}:
441+
// Don't bother trying to decode these.
442+
return ""
443+
case nil:
444+
return ""
445+
default:
446+
panic("type of unexpected value")
447+
}
288448
}
289449

290450
// UnmarshalJSON is a custom JSON unmarshaler for DataCenterInfo, populating either Metadata or AlternateMetadata
291451
// depending on the type of data center indicated by the Name.
292452
func (i *DataCenterInfo) UnmarshalJSON(b []byte) error {
293-
p := preliminaryDataCenterInfo{
294-
Metadata: make(map[string]string, 11),
453+
// The Eureka server will mistakenly convert metadata values that look like numbers to JSON numbers.
454+
// Convert them back to strings.
455+
aux := struct {
456+
*preliminaryDataCenterInfo
457+
PreliminaryMetadata map[string]interface{} `json:"metadata"`
458+
}{
459+
PreliminaryMetadata: make(map[string]interface{}, 11),
295460
}
296-
if err := json.Unmarshal(b, &p); err != nil {
461+
if err := json.Unmarshal(b, &aux); err != nil {
297462
return err
298463
}
299-
adaptDataCenterInfo(i, p)
464+
metadata := make(map[string]string, len(aux.PreliminaryMetadata))
465+
for k, v := range aux.PreliminaryMetadata {
466+
metadata[k] = jsonValueAsString(v)
467+
}
468+
aux.Metadata = metadata
469+
adaptDataCenterInfo(i, aux.preliminaryDataCenterInfo)
300470
return nil
301471
}

0 commit comments

Comments
 (0)