From d62037f26a1d65d04b2e723def9661fa4002b301 Mon Sep 17 00:00:00 2001 From: Carl Quinn Date: Tue, 17 Jun 2014 11:10:24 -0700 Subject: [PATCH] Add JSON support: - rpc extensions to support both XML and JSON requests and responses - net support for a Eureka connection using JSON or XML - net support for marshalling and unmarshalling of the apps, app and instance structs - custom unmarshallers and intermediate wrapper structs to deal with nasty Eureka JSON --- marshal.go | 104 ++++++++++ metadata.go | 30 ++- net.go | 225 +++++++++++++--------- rpc.go | 42 ++-- struct.go | 102 ++++++---- tests/marshal_sample/apps-sample-1-1.json | 49 +++++ tests/marshal_sample/apps-sample-1-2.json | 89 +++++++++ tests/marshal_sample/apps-sample-2-2.json | 131 +++++++++++++ tests/marshal_test.go | 44 +++++ tests/net_test.go | 194 ++++++++++--------- 10 files changed, 768 insertions(+), 242 deletions(-) create mode 100644 marshal.go create mode 100644 tests/marshal_sample/apps-sample-1-1.json create mode 100644 tests/marshal_sample/apps-sample-1-2.json create mode 100644 tests/marshal_sample/apps-sample-2-2.json create mode 100644 tests/marshal_test.go diff --git a/marshal.go b/marshal.go new file mode 100644 index 0000000..d8b5a1c --- /dev/null +++ b/marshal.go @@ -0,0 +1,104 @@ +package fargo + +// MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl> + +import ( + "encoding/json" + //"fmt" + "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"` +} + +// 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 { + //fmt.Printf("GetAppsResponse.UnmarshalJSON b:%s\n", string(b)) + var err error + + // Normal array case + var ra getAppsResponseArray + if err = json.Unmarshal(b, &ra); err == nil { + //fmt.Printf("GetAppsResponse.UnmarshalJSON ra:%+v\n", ra) + *r = GetAppsResponse(ra) + return nil + } + // Bogus non-wrapped case + var rs getAppsResponseSingle + if err = json.Unmarshal(b, &rs); err == nil { + //fmt.Printf("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 + } + return err +} + +// Temporary structs used for Application unmarshalling +type applicationArray Application +type applicationSingle struct { + Name string `json:"name"` + Instance *Instance `json:"instance"` +} + +// UnmarshalJSON is a custom JSON unmarshaler for Application to deal with +// sometimes non-wrapped Instance array when there is only a single Instance item. +func (a *Application) UnmarshalJSON(b []byte) error { + //fmt.Printf("Application.UnmarshalJSON b:%s\n", string(b)) + var err error + + // Normal array case + var aa applicationArray + if err = json.Unmarshal(b, &aa); err == nil { + //fmt.Printf("Application.UnmarshalJSON aa:%+v\n", aa) + *a = Application(aa) + return nil + } + + // Bogus non-wrapped case + var as applicationSingle + if err = json.Unmarshal(b, &as); err == nil { + //fmt.Printf("Application.UnmarshalJSON as:%+v\n", as) + a.Name = as.Name + a.Instances = make([]*Instance, 1, 1) + a.Instances[0] = as.Instance + return nil + } + return err +} + +type instance Instance + +// 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 { + //fmt.Printf("Instance.UnmarshalJSON ii:%+v\n", ii) + *i = Instance(ii) + i.Port, _ = strconv.Atoi(ii.PortJ.Number) + i.SecurePort, _ = strconv.Atoi(ii.SecurePortJ.Number) + //i.Port = ii.PortJ.Number + //i.SecurePort = ii.SecurePortJ.Number + return nil + } + return err +} + +// 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 { + i.Raw = b + // TODO(cq) could actually parse Raw here, and in a parallel UnmarshalXML as well. + return nil +} diff --git a/metadata.go b/metadata.go index e59dde4..ee29638 100644 --- a/metadata.go +++ b/metadata.go @@ -3,6 +3,7 @@ package fargo // MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl> import ( + "encoding/json" "fmt" "github.com/clbanning/x2j" ) @@ -12,7 +13,7 @@ func (a *Application) ParseAllMetadata() error { for _, instance := range a.Instances { err := instance.Metadata.parse() if err != nil { - log.Error("Failed parsing metadata for Instance=%s of Application=%s: ", instance.HostName, a.Name, err.Error()) + log.Error("Failed parsing metadata for Instance=%s of Application=%s: %s", instance.HostName, a.Name, err.Error()) return err } } @@ -20,19 +21,30 @@ func (a *Application) ParseAllMetadata() error { } func (im *InstanceMetadata) parse() error { - // wrap in a BS xml tag so all metadata tags are pulled if len(im.Raw) == 0 { im.parsed = make(map[string]interface{}) log.Debug("len(Metadata)==0. Quitting parsing.") return nil } - fullDoc := append(append([]byte(""), im.Raw...), []byte("")...) - parsedDoc, err := x2j.ByteDocToMap(fullDoc, true) - if err != nil { - log.Error("Error unmarshalling: ", err.Error()) - return fmt.Errorf("error unmarshalling: ", err.Error()) + //log.Debug("InstanceMetadata.parse: %s", im.Raw) + + if len(im.Raw) > 0 && im.Raw[0] == '{' { + // JSON + err := json.Unmarshal(im.Raw, &im.parsed) + if err != nil { + log.Error("Error unmarshalling: %s", err.Error()) + return fmt.Errorf("error unmarshalling: %s", err.Error()) + } + } else { + // XML: wrap in a BS xml tag so all metadata tags are pulled + fullDoc := append(append([]byte(""), im.Raw...), []byte("")...) + parsedDoc, err := x2j.ByteDocToMap(fullDoc, true) + if err != nil { + log.Error("Error unmarshalling: %s", err.Error()) + return fmt.Errorf("error unmarshalling: %s", err.Error()) + } + im.parsed = parsedDoc["d"].(map[string]interface{}) } - im.parsed = parsedDoc["d"].(map[string]interface{}) return nil } @@ -44,7 +56,7 @@ func (im *InstanceMetadata) GetMap() map[string]interface{} { func (im *InstanceMetadata) getItem(key string) (interface{}, bool, error) { err := im.parse() if err != nil { - return "", false, fmt.Errorf("parsing error: ", err.Error()) + return "", false, fmt.Errorf("parsing error: %s", err.Error()) } val, present := im.parsed[key] return val, present, nil diff --git a/net.go b/net.go index 1e53848..6f5132b 100644 --- a/net.go +++ b/net.go @@ -3,9 +3,11 @@ package fargo // MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl> import ( + "encoding/json" "encoding/xml" "fmt" "net/http" + "strconv" "strings" ) @@ -13,87 +15,109 @@ func (e *EurekaConnection) generateURL(slugs ...string) string { return strings.Join(append([]string{e.SelectServiceURL()}, slugs...), "/") } +func (e *EurekaConnection) marshal(v interface{}) ([]byte, error) { + if e.UseJson { + out, err := json.Marshal(v) + if err != nil { + // marshal the xml *with* indents so it's readable in the error message + out, _ := json.MarshalIndent(v, "", " ") + log.Error("Error marshalling JSON value=%v. Error:\"%s\" JSON body=\"%s\"", v, err.Error(), string(out)) + return nil, err + } + return out, nil + } else { + out, err := xml.Marshal(v) + if err != nil { + // marshal the xml *with* indents so it's readable in the error message + out, _ := xml.MarshalIndent(v, "", " ") + log.Error("Error marshalling XML value=%v. Error:\"%s\" JSON body=\"%s\"", v, err.Error(), string(out)) + return nil, err + } + return out, nil + } +} + // GetApp returns a single eureka application by name -func (e *EurekaConnection) GetApp(name string) (Application, error) { +func (e *EurekaConnection) GetApp(name string) (*Application, error) { slug := fmt.Sprintf("%s/%s", EurekaURLSlugs["Apps"], name) reqURL := e.generateURL(slug) log.Debug("Getting app %s from url %s", name, reqURL) - out, rcode, err := getXML(reqURL) + out, rcode, err := getBody(reqURL, e.UseJson) if err != nil { - log.Error("Couldn't get XML. Error: %s", err.Error()) - return Application{}, err + log.Error("Couldn't get app %s, error: %s", name, err.Error()) + return nil, err } if rcode == 404 { - log.Error("application %s not found (received 404)", name) - return Application{}, AppNotFoundError{specific: name} - } - var v Application - err = xml.Unmarshal(out, &v) - if err != nil { - log.Error("Unmarshalling error. Error: %s", err.Error()) - return Application{}, err + log.Error("App %s not found (received 404)", name) + return nil, AppNotFoundError{specific: name} } if rcode > 299 || rcode < 200 { log.Warning("Non-200 rcode of %d", rcode) } + + var v *Application + if e.UseJson { + var r GetAppResponseJson + err = json.Unmarshal(out, &r) + v = &r.Application + } else { + err = xml.Unmarshal(out, &v) + } + if err != nil { + log.Error("Unmarshalling error: %s", err.Error()) + return nil, err + } + v.ParseAllMetadata() return v, nil } +func (e *EurekaConnection) readAppInto(name string, app *Application) error { + tapp, err := e.GetApp(name) + if err == nil { + *app = *tapp + } + return err +} + // GetApps returns a map of all Applications func (e *EurekaConnection) GetApps() (map[string]*Application, error) { slug := EurekaURLSlugs["Apps"] reqURL := e.generateURL(slug) log.Debug("Getting all apps from url %s", reqURL) - out, rcode, err := getXML(reqURL) + body, rcode, err := getBody(reqURL, e.UseJson) if err != nil { - log.Error("Couldn't get XML: " + err.Error()) + log.Error("Couldn't get apps, error: %s", err.Error()) return nil, err } - var v GetAppsResponse - err = xml.Unmarshal(out, &v) + if rcode > 299 || rcode < 200 { + log.Warning("Non-200 rcode of %d", rcode) + } + + var r *GetAppsResponse + if e.UseJson { + var rj GetAppsResponseJson + err = json.Unmarshal(body, &rj) + r = rj.Response + } else { + err = xml.Unmarshal(body, &r) + } if err != nil { - log.Error("Unmarshalling error: " + err.Error()) + log.Error("Unmarshalling error: %s", err.Error()) return nil, err } + apps := map[string]*Application{} - for i, a := range v.Applications { - apps[a.Name] = &v.Applications[i] - } - if rcode > 299 || rcode < 200 { - log.Warning("Non-200 rcode of %d", rcode) + for i, a := range r.Applications { + apps[a.Name] = r.Applications[i] } for name, app := range apps { - log.Debug("Parsing metadata for Application=%s", name) + log.Debug("Parsing metadata for app %s", name) app.ParseAllMetadata() } return apps, nil } -// AddMetadataString to a given instance. Is immediately sent to Eureka server. -func (e EurekaConnection) AddMetadataString(ins *Instance, key, value string) error { - slug := fmt.Sprintf("%s/%s/%s/metadata", EurekaURLSlugs["Apps"], ins.App, ins.HostName) - reqURL := e.generateURL(slug) - - params := map[string]string{key: value} - if ins.Metadata.parsed == nil { - ins.Metadata.parsed = map[string]interface{}{} - } - ins.Metadata.parsed[key] = value - - log.Debug("Updating instance metadata url=%s metadata=%s", reqURL, params) - body, rcode, err := putKV(reqURL, params) - if err != nil { - log.Error("Could not complete update with Error: ", err.Error()) - return err - } - if rcode < 200 || rcode >= 300 { - log.Warning("HTTP returned %d updating metadata Instance=%s App=%s Body=\"%s\"", rcode, ins.HostName, ins.App, string(body)) - return fmt.Errorf("http returned %d possible failure updating instance metadata ", rcode) - } - return nil -} - // RegisterInstance will register the given Instance with eureka if it is not already registered, // but DOES NOT automatically send heartbeats. See HeartBeatInstance for that // functionality @@ -101,17 +125,17 @@ func (e *EurekaConnection) RegisterInstance(ins *Instance) error { slug := fmt.Sprintf("%s/%s", EurekaURLSlugs["Apps"], ins.App) reqURL := e.generateURL(slug) log.Debug("Registering instance with url %s", reqURL) - _, rcode, err := getXML(reqURL + "/" + ins.HostName) + _, rcode, err := getBody(reqURL+"/"+ins.HostName, e.UseJson) if err != nil { - log.Error("Failed check if Instance=%s exists in App=%s. Error=\"%s\"", + log.Error("Failed check if Instance=%s exists in app=%s, error: %s", ins.HostName, ins.App, err.Error()) return err } if rcode == 200 { - log.Notice("Instance=%s already exists in App=%s aborting registration", ins.HostName, ins.App) + log.Notice("Instance=%s already exists in App=%s, aborting registration", ins.HostName, ins.App) return nil } - log.Notice("Instance=%s not yet registered with App=%s. Registering.", ins.HostName, ins.App) + log.Notice("Instance=%s not yet registered with App=%s, registering.", ins.HostName, ins.App) return e.ReregisterInstance(ins) } @@ -121,26 +145,40 @@ func (e *EurekaConnection) RegisterInstance(ins *Instance) error { func (e *EurekaConnection) ReregisterInstance(ins *Instance) error { slug := fmt.Sprintf("%s/%s", EurekaURLSlugs["Apps"], ins.App) reqURL := e.generateURL(slug) - out, err := xml.Marshal(ins) - if err != nil { - // marshal the xml *with* indents so it's readable in the error message - out, _ := xml.MarshalIndent(ins, "", " ") - log.Error("Error marshalling XML Instance=%s App=%s. Error:\"%s\" XML body=\"%s\"", err.Error(), ins.HostName, ins.App, string(out)) - return err + + 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) } - body, rcode, err := postXML(reqURL, out) + + body, rcode, err := postBody(reqURL, out, e.UseJson) if err != nil { - log.Error("Could not complete registration Error: ", err.Error()) + log.Error("Could not complete registration, error: %s", err.Error()) return err } if rcode != 204 { - log.Warning("HTTP returned %d registering Instance=%s App=%s Body=\"%s\"", rcode, ins.HostName, ins.App, string(body)) + log.Warning("HTTP returned %d registering Instance=%s App=%s Body=\"%s\"", rcode, + ins.HostName, ins.App, string(body)) return fmt.Errorf("http returned %d possible failure registering instance\n", rcode) } // read back our registration to ensure that it stuck - body, rcode, err = getXML(reqURL + "/" + ins.HostName) - xml.Unmarshal(body, ins) + // TODO(cq) is this really needed? Especially the unmarshal over our existing struct... + /* + body, rcode, err = getBody(reqURL + "/" + ins.HostName, e.UseJson) + if e.UseJson { + fmt.Printf("ReregisterInstance, reread JSON: %+v: %s\n", ins, string(body)) + // TODO: would need to unmarshal RegisterInstanceJson here instead... + return json.Unmarshal(body, ins) + } else { + return xml.Unmarshal(body, ins) + } + */ return nil } @@ -153,7 +191,7 @@ func (e *EurekaConnection) DeregisterInstance(ins *Instance) error { rcode, err := deleteReq(reqURL) if err != nil { - log.Error("Could not complete deregistration Error: ", err.Error()) + log.Error("Could not complete deregistration, error: %s", err.Error()) return err } if rcode != 204 { @@ -164,6 +202,31 @@ func (e *EurekaConnection) DeregisterInstance(ins *Instance) error { return nil } +// AddMetadataString to a given instance. Is immediately sent to Eureka server. +func (e EurekaConnection) AddMetadataString(ins *Instance, key, value string) error { + slug := fmt.Sprintf("%s/%s/%s/metadata", EurekaURLSlugs["Apps"], ins.App, ins.HostName) + reqURL := e.generateURL(slug) + + params := map[string]string{key: value} + if ins.Metadata.parsed == nil { + ins.Metadata.parsed = map[string]interface{}{} + } + ins.Metadata.parsed[key] = value + + log.Debug("Updating instance metadata url=%s metadata=%s", reqURL, params) + body, rcode, err := putKV(reqURL, params) + if err != nil { + log.Error("Could not complete update, error: ", err.Error()) + return err + } + if rcode < 200 || rcode >= 300 { + log.Warning("HTTP returned %d updating metadata Instance=%s App=%s Body=\"%s\"", rcode, + ins.HostName, ins.App, string(body)) + return fmt.Errorf("http returned %d possible failure updating instance metadata ", rcode) + } + return nil +} + // UpdateInstanceStatus updates the status of a given instance with eureka. func (e EurekaConnection) UpdateInstanceStatus(ins *Instance, status StatusType) error { slug := fmt.Sprintf("%s/%s/%s/status", EurekaURLSlugs["Apps"], ins.App, ins.HostName) @@ -174,11 +237,12 @@ func (e EurekaConnection) UpdateInstanceStatus(ins *Instance, status StatusType) log.Debug("Updating instance status url=%s value=%s", reqURL, status) body, rcode, err := putKV(reqURL, params) if err != nil { - log.Error("Could not complete update with Error: ", err.Error()) + log.Error("Could not complete update, error: ", err.Error()) return err } if rcode < 200 || rcode >= 300 { - log.Warning("HTTP returned %d updating status Instance=%s App=%s Body=\"%s\"", rcode, ins.HostName, ins.App, string(body)) + log.Warning("HTTP returned %d updating status Instance=%s App=%s Body=\"%s\"", rcode, + ins.HostName, ins.App, string(body)) return fmt.Errorf("http returned %d possible failure updating instance status ", rcode) } return nil @@ -192,40 +256,17 @@ func (e *EurekaConnection) HeartBeatInstance(ins *Instance) error { log.Debug("Sending heartbeat with url %s", reqURL) req, err := http.NewRequest("PUT", reqURL, nil) if err != nil { - log.Error("Could not create request for heartbeat. Error: %s", err.Error()) + log.Error("Could not create request for heartbeat, error: %s", err.Error()) return err } - _, rcode, err := reqXML(req) + _, rcode, err := netReq(req) if err != nil { - log.Error("Error sending heartbeat for Instance=%s App=%s error: %s", ins.HostName, ins.App, err.Error()) + log.Error("Error sending heartbeat for Instance=%s App=%s, error: %s", ins.HostName, ins.App, err.Error()) return err } if rcode != 200 { - log.Error("Sending heartbeat for Instance=%s App=%s returned code %d\n", ins.HostName, ins.App, rcode) + log.Error("Sending heartbeat for Instance=%s App=%s returned code %d", ins.HostName, ins.App, rcode) return fmt.Errorf("heartbeat returned code %d\n", rcode) } return nil } - -func (e *EurekaConnection) readAppInto(name string, app *Application) error { - slug := fmt.Sprintf("%s/%s", EurekaURLSlugs["Apps"], name) - reqURL := e.generateURL(slug) - log.Debug("Getting app %s from url %s", name, reqURL) - out, rcode, err := getXML(reqURL) - if err != nil { - log.Error("Couldn't get XML. Error: %s", err.Error()) - return err - } - oldInstances := app.Instances - app.Instances = []*Instance{} - err = xml.Unmarshal(out, app) - if err != nil { - app.Instances = oldInstances - log.Error("Unmarshalling error. Error: %s", err.Error()) - return err - } - if rcode > 299 || rcode < 200 { - log.Warning("Non-200 rcode of %d", rcode) - } - return nil -} diff --git a/rpc.go b/rpc.go index 3e2c2d1..8b374dd 100644 --- a/rpc.go +++ b/rpc.go @@ -12,15 +12,16 @@ import ( "time" ) -func postXML(reqURL string, reqBody []byte) ([]byte, int, error) { +func postBody(reqURL string, reqBody []byte, isJson bool) ([]byte, int, error) { req, err := http.NewRequest("POST", reqURL, bytes.NewReader(reqBody)) if err != nil { - log.Error("Could not create POST %s with body %s Error: %s", reqURL, string(reqBody), err.Error()) + log.Error("Could not create POST %s with body %s, error: %s", reqURL, string(reqBody), err.Error()) return nil, -1, err } - body, rcode, err := reqXML(req) + log.Debug("postBody: %s %s : %s\n", req.Method, req.URL, string(reqBody)) + body, rcode, err := netReqTyped(req, isJson) if err != nil { - log.Error("Could not complete POST %s with body %s Error: %s", reqURL, string(reqBody), err.Error()) + log.Error("Could not complete POST %s with body %s, error: %s", reqURL, string(reqBody), err.Error()) return nil, rcode, err } //eurekaCache.Flush() @@ -36,26 +37,26 @@ func putKV(reqURL string, pairs map[string]string) ([]byte, int, error) { log.Notice("Sending KV request with URL %s", parameterizedURL) req, err := http.NewRequest("PUT", parameterizedURL, nil) if err != nil { - log.Error("Could not create PUT %s with Error: %s", reqURL, err.Error()) + log.Error("Could not create PUT %s, error: %s", reqURL, err.Error()) return nil, -1, err } - body, rcode, err := reqXML(req) + body, rcode, err := netReq(req) // TODO(cq) I think this can just be netReq() since there is no body if err != nil { - log.Error("Could not complete PUT %s with Error: %s", reqURL, err.Error()) + log.Error("Could not complete PUT %s, error: %s", reqURL, err.Error()) return nil, rcode, err } return body, rcode, nil } -func getXML(reqURL string) ([]byte, int, error) { +func getBody(reqURL string, isJson bool) ([]byte, int, error) { req, err := http.NewRequest("GET", reqURL, nil) if err != nil { - log.Error("Could not create GET %s with Error: %s", reqURL, err.Error()) + log.Error("Could not create GET %s, error: %s", reqURL, err.Error()) return nil, -1, err } - body, rcode, err := reqXML(req) + body, rcode, err := netReqTyped(req, isJson) if err != nil { - log.Error("Could not complete GET %s with Error: %s", reqURL, err.Error()) + log.Error("Could not complete GET %s, error: %s", reqURL, err.Error()) return nil, rcode, err } return body, rcode, nil @@ -64,20 +65,25 @@ func getXML(reqURL string) ([]byte, int, error) { func deleteReq(reqURL string) (int, error) { req, err := http.NewRequest("DELETE", reqURL, nil) if err != nil { - log.Error("Could not create DELETE %s with Error: %s", reqURL, err.Error()) + log.Error("Could not create DELETE %s, error: %s", reqURL, err.Error()) return -1, err } _, rcode, err := netReq(req) if err != nil { - log.Error("Could not complete DELETE %s with Error: %s", reqURL, err.Error()) + log.Error("Could not complete DELETE %s, error: %s", reqURL, err.Error()) return rcode, err } return rcode, nil } -func reqXML(req *http.Request) ([]byte, int, error) { - req.Header.Set("Content-Type", "application/xml") - req.Header.Set("Accept", "application/xml") +func netReqTyped(req *http.Request, isJson bool) ([]byte, int, error) { + if isJson { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + } else { + req.Header.Set("Content-Type", "application/xml") + req.Header.Set("Accept", "application/xml") + } return netReq(req) } @@ -111,10 +117,10 @@ func netReq(req *http.Request) ([]byte, int, error) { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - log.Error("Failure reading request body Error: %s", err.Error()) + log.Error("Failure reading request body, error: %s", err.Error()) return nil, -1, err } // At this point we're done and shit worked, simply return the bytes - log.Info("Got eureka info from url=%v", req.URL) + log.Info("Got eureka response from url=%v", req.URL) return body, resp.StatusCode, nil } diff --git a/struct.go b/struct.go index 6cd4747..dabaa04 100644 --- a/struct.go +++ b/struct.go @@ -19,19 +19,30 @@ type EurekaConnection struct { PollInterval time.Duration PreferSameZone bool Retries int + UseJson bool +} + +// GetAppsResponseJson lets us deserialize the eureka/v2/apps response JSON--a wrapped GetAppsResponse +type GetAppsResponseJson struct { + Response *GetAppsResponse `json:"applications"` } // GetAppsResponse lets us deserialize the eureka/v2/apps response XML type GetAppsResponse struct { - VersionDelta int `xml:"versions__delta"` - AppsHashCode string `xml:"apps__hashcode"` - Applications []Application `xml:"application"` + Applications []*Application `xml:"application" json:"application"` + AppsHashcode string `xml:"apps__hashcode" json:"apps__hashcode"` + VersionsDelta int `xml:"versions__delta" json:"versions__delta"` +} + +// Application deserializeable from Eureka JSON +type GetAppResponseJson struct { + Application Application `json:"application"` } // Application deserializeable from Eureka XML type Application struct { - Name string `xml:"name"` - Instances []*Instance `xml:"instance"` + Name string `xml:"name" json:"name"` + Instances []*Instance `xml:"instance" json:"instance"` } // StatusType is an enum of the different statuses allowed by Eureka @@ -52,20 +63,39 @@ const ( MyOwn = "MyOwn" ) +// RegisterInstanceJson lets us serialize the eureka/v2/apps/ request JSON--a wrapped Instance +type RegisterInstanceJson struct { + Instance *Instance `json:"instance"` +} + // Instance [de]serializeable [to|from] Eureka XML type Instance struct { - XMLName struct{} `xml:"instance"` - HostName string `xml:"hostName"` - App string `xml:"app"` - IPAddr string `xml:"ipAddr"` - VipAddress string `xml:"vipAddress"` - SecureVipAddress string `xml:"secureVipAddress"` - Status StatusType `xml:"status"` - Port int `xml:"port"` - SecurePort int `xml:"securePort"` - DataCenterInfo DataCenterInfo `xml:"dataCenterInfo"` - LeaseInfo LeaseInfo `xml:"leaseInfo"` - Metadata InstanceMetadata `xml:"metadata"` + 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"` + + 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:"-"` + + CountryId int64 `xml:"countryId" json:"countryId"` + DataCenterInfo DataCenterInfo `xml:"dataCenterInfo" json:"dataCenterInfo"` + + LeaseInfo LeaseInfo `xml:"leaseInfo" json:"leaseInfo"` + Metadata InstanceMetadata `xml:"metadata" json:"metadata"` +} + +// Port struct used for JSON [un]marshaling only +type Port struct { // json port looks like: "port":{"@enabled":"true", "$":"7101"}, + Number string `json:"$"` + Enabled string `json:"@enabled"` } // InstanceMetadata represents the eureka metadata, which is arbitrary XML. See @@ -79,31 +109,31 @@ type InstanceMetadata struct { // // from http://docs.amazonwebservices.com/AWSEC2/latest/DeveloperGuide/index.html?AESDG-chapter-instancedata.html type AmazonMetadataType struct { - AmiLaunchIndex string `xml:"ami-launch-index"` - LocalHostname string `xml:"local-hostname"` - AvailabilityZone string `xml:"availability-zone"` - InstanceID string `xml:"instance-id"` - PublicIpv4 string `xml:"public-ipv4"` - PublicHostname string `xml:"public-hostname"` - AmiManifestPath string `xml:"ami-manifest-path"` - LocalIpv4 string `xml:"local-ipv4"` - HostName string `xml:"hostname"` - AmiID string `xml:"ami-id"` - InstanceType string `xml:"instance-type"` + AmiLaunchIndex string `xml:"ami-launch-index" json:"ami-launch-index"` + LocalHostname string `xml:"local-hostname" json:"local-hostname"` + AvailabilityZone string `xml:"availability-zone" json:"availability-zone"` + InstanceID string `xml:"instance-id" json:"instance-id"` + PublicIpv4 string `xml:"public-ipv4" json:"public-ipv4"` + PublicHostname string `xml:"public-hostname" json:"public-hostname"` + AmiManifestPath string `xml:"ami-manifest-path" json:"ami-manifest-path"` + LocalIpv4 string `xml:"local-ipv4" json:"local-ipv4"` + HostName string `xml:"hostname" json:"hostname"` + AmiID string `xml:"ami-id" json:"ami-id"` + InstanceType string `xml:"instance-type" json:"instance-type"` } // DataCenterInfo is only really useful when running in AWS. type DataCenterInfo struct { - Name string `xml:"name"` - Metadata AmazonMetadataType `xml:"metadata"` + Name string `xml:"name" json:"name"` + Metadata AmazonMetadataType `xml:"metadata" json:"metadata"` } // LeaseInfo tells us about the renewal from Eureka, including how old it is type LeaseInfo struct { - RenewalIntervalInSecs int32 `xml:"renewalIntervalInSecs"` - DurationInSecs int32 `xml:"durationInSecs"` - RegistrationTimestamp int64 `xml:"registrationTimestamp"` - LastRenewalTimestamp int64 `xml:"lastRenewalTimestamp"` - EvictionTimestamp int32 `xml:"evictionTimestamp"` - ServiceUpTimestamp int64 `xml:"serviceUpTimestamp"` + RenewalIntervalInSecs int32 `xml:"renewalIntervalInSecs" json:"renewalIntervalInSecs"` + DurationInSecs int32 `xml:"durationInSecs" json:"durationInSecs"` + RegistrationTimestamp int64 `xml:"registrationTimestamp" json:"registrationTimestamp"` + LastRenewalTimestamp int64 `xml:"lastRenewalTimestamp" json:"lastRenewalTimestamp"` + EvictionTimestamp int64 `xml:"evictionTimestamp" json:"evictionTimestamp"` + ServiceUpTimestamp int64 `xml:"serviceUpTimestamp" json:"serviceUpTimestamp"` } diff --git a/tests/marshal_sample/apps-sample-1-1.json b/tests/marshal_sample/apps-sample-1-1.json new file mode 100644 index 0000000..2aaeaba --- /dev/null +++ b/tests/marshal_sample/apps-sample-1-1.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_sample/apps-sample-1-2.json b/tests/marshal_sample/apps-sample-1-2.json new file mode 100644 index 0000000..c253d8f --- /dev/null +++ b/tests/marshal_sample/apps-sample-1-2.json @@ -0,0 +1,89 @@ +{ + "applications":{ + "versions__delta":1, + "apps__hashcode":"UP_24_", + "application":[ + { + "name":"TESTENG.LSM", + "instance":[ + { + "hostName":"qa16gs.qa.somecompany.com:7100", + "app":"TESTENG.LSM", + "ipAddr":"192.168.191.150", + "status":"UP", + "overriddenstatus":"UNKNOWN", + "port":{ + "@enabled":"true", + "$":"7100" + }, + "securePort":{ + "@enabled":"true", + "$":"7100" + }, + "countryId":1, + "dataCenterInfo":{ + "@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name":"MyOwn" + }, + "leaseInfo":{ + "renewalIntervalInSecs":30, + "durationInSecs":90, + "registrationTimestamp":1402357291394, + "lastRenewalTimestamp":1402552702008, + "evictionTimestamp":0, + "serviceUpTimestamp":1402357291394 + }, + "metadata":{ + "service-query-uri":"https://qa16gs.qa.somecompany.com:7100", + "base-uri":"https://qa16gs.qa.somecompany.com:7100", + "swagger-api-docs":"https://qa16gs.qa.somecompany.com:7100/api-docs" + }, + "vipAddress":"[test.lax1.qa16]", + "isCoordinatingDiscoveryServer":false, + "lastUpdatedTimestamp":1402357291394, + "lastDirtyTimestamp":1402357289731, + "actionType":"ADDED" + }, + { + "hostName":"qa22gs:7100", + "app":"TESTENG.LSM", + "ipAddr":"192.168.191.235", + "status":"UP", + "overriddenstatus":"UNKNOWN", + "port":{ + "@enabled":"true", + "$":"7100" + }, + "securePort":{ + "@enabled":"true", + "$":"7100" + }, + "countryId":1, + "dataCenterInfo":{ + "@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name":"MyOwn" + }, + "leaseInfo":{ + "renewalIntervalInSecs":30, + "durationInSecs":90, + "registrationTimestamp":1402096233497, + "lastRenewalTimestamp":1402552691629, + "evictionTimestamp":0, + "serviceUpTimestamp":1402096233497 + }, + "metadata":{ + "service-query-uri":"https://qa22gs:7100", + "base-uri":"https://qa22gs:7100", + "swagger-api-docs":"https://qa22gs:7100/api-docs" + }, + "vipAddress":"[test.lax1.qa22]", + "isCoordinatingDiscoveryServer":false, + "lastUpdatedTimestamp":1402096233497, + "lastDirtyTimestamp":1401527685391, + "actionType":"ADDED" + } + ] + } + ] + } +} diff --git a/tests/marshal_sample/apps-sample-2-2.json b/tests/marshal_sample/apps-sample-2-2.json new file mode 100644 index 0000000..54ffd5b --- /dev/null +++ b/tests/marshal_sample/apps-sample-2-2.json @@ -0,0 +1,131 @@ +{ + "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" + } + }, + { + "name":"TESTENG.LSM", + "instance":[ + { + "hostName":"qa16gs.qa.somecompany.com:7100", + "app":"TESTENG.LSM", + "ipAddr":"192.168.191.150", + "status":"UP", + "overriddenstatus":"UNKNOWN", + "port":{ + "@enabled":"true", + "$":"7100" + }, + "securePort":{ + "@enabled":"true", + "$":"7100" + }, + "countryId":1, + "dataCenterInfo":{ + "@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name":"MyOwn" + }, + "leaseInfo":{ + "renewalIntervalInSecs":30, + "durationInSecs":90, + "registrationTimestamp":1402357291394, + "lastRenewalTimestamp":1402552702008, + "evictionTimestamp":0, + "serviceUpTimestamp":1402357291394 + }, + "metadata":{ + "service-query-uri":"https://qa16gs.qa.somecompany.com:7100", + "base-uri":"https://qa16gs.qa.somecompany.com:7100", + "swagger-api-docs":"https://qa16gs.qa.somecompany.com:7100/api-docs" + }, + "vipAddress":"[test.lax1.qa16]", + "isCoordinatingDiscoveryServer":false, + "lastUpdatedTimestamp":1402357291394, + "lastDirtyTimestamp":1402357289731, + "actionType":"ADDED" + }, + { + "hostName":"qa22gs:7100", + "app":"TESTENG.LSM", + "ipAddr":"192.168.191.235", + "status":"UP", + "overriddenstatus":"UNKNOWN", + "port":{ + "@enabled":"true", + "$":"7100" + }, + "securePort":{ + "@enabled":"true", + "$":"7100" + }, + "countryId":1, + "dataCenterInfo":{ + "@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + "name":"MyOwn" + }, + "leaseInfo":{ + "renewalIntervalInSecs":30, + "durationInSecs":90, + "registrationTimestamp":1402096233497, + "lastRenewalTimestamp":1402552691629, + "evictionTimestamp":0, + "serviceUpTimestamp":1402096233497 + }, + "metadata":{ + "service-query-uri":"https://qa22gs:7100", + "base-uri":"https://qa22gs:7100", + "swagger-api-docs":"https://qa22gs:7100/api-docs" + }, + "vipAddress":"[test.lax1.qa22]", + "isCoordinatingDiscoveryServer":false, + "lastUpdatedTimestamp":1402096233497, + "lastDirtyTimestamp":1401527685391, + "actionType":"ADDED" + } + ] + } + ] + } +} + diff --git a/tests/marshal_test.go b/tests/marshal_test.go new file mode 100644 index 0000000..2b152fb --- /dev/null +++ b/tests/marshal_test.go @@ -0,0 +1,44 @@ +package fargo_test + +// MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl> + +import ( + "encoding/json" + "fmt" + "github.com/hudl/fargo" + . "github.com/smartystreets/goconvey/convey" + "io/ioutil" + "testing" +) + +func TestJsonMarshal(t *testing.T) { + for _, f := range []string{"apps-sample-1-1.json", "apps-sample-1-2.json", "apps-sample-2-2.json"} { + Convey("Reading .", t, func() { + blob, err := ioutil.ReadFile("marshal_sample/" + f) + + var v fargo.GetAppsResponseJson + err = json.Unmarshal(blob, &v) + + // Handy dump for debugging funky JSON + fmt.Printf("v:\n%+v\n", v.Response.Applications) + for _, app := range v.Response.Applications { + fmt.Printf(" %+v\n", *app) + for _, ins := range app.Instances { + fmt.Printf(" %+v\n", *ins) + } + } + + if err != nil { + // Print a little more details when there are unmarshalling problems + switch ute := err.(type) { + case *json.UnmarshalTypeError: + fmt.Printf("\nUnmarshalling type error val:%s type:%s: %s\n", ute.Value, ute.Type, err.Error()) + fmt.Printf("UTE:\n%+v\n", ute) + default: + fmt.Printf("\nUnmarshalling error: %s\n", err.Error()) + } + } + So(err, ShouldBeNil) + }) + } +} diff --git a/tests/net_test.go b/tests/net_test.go index e6f74c2..f9a822d 100644 --- a/tests/net_test.go +++ b/tests/net_test.go @@ -3,6 +3,7 @@ package fargo_test // MIT Licensed (see README.md) - Copyright (c) 2013 Hudl <@Hudl> import ( + "fmt" "github.com/hudl/fargo" . "github.com/smartystreets/goconvey/convey" "testing" @@ -21,23 +22,26 @@ func TestConnectionCreation(t *testing.T) { func TestGetApps(t *testing.T) { e, _ := fargo.NewConnFromConfigFile("./config_sample/local.gcfg") - Convey("Pull applications", t, func() { - a, _ := e.GetApps() - So(len(a["EUREKA"].Instances), ShouldEqual, 2) - }) - Convey("Pull single application", t, func() { - a, _ := e.GetApp("EUREKA") - So(len(a.Instances), ShouldEqual, 2) - for idx, ins := range a.Instances { - if ins.HostName == "node1.localdomain" { - So(a.Instances[idx].IPAddr, ShouldEqual, "172.16.0.11") - So(a.Instances[idx].HostName, ShouldEqual, "node1.localdomain") - } else { - So(a.Instances[idx].IPAddr, ShouldEqual, "172.16.0.22") - So(a.Instances[idx].HostName, ShouldEqual, "node2.localdomain") + for _, j := range []bool{false, true} { + e.UseJson = j + Convey("Pull applications", t, func() { + a, _ := e.GetApps() + So(len(a["EUREKA"].Instances), ShouldEqual, 2) + }) + Convey("Pull single application", t, func() { + a, _ := e.GetApp("EUREKA") + So(len(a.Instances), ShouldEqual, 2) + for idx, ins := range a.Instances { + if ins.HostName == "node1.localdomain" { + So(a.Instances[idx].IPAddr, ShouldEqual, "172.16.0.11") + So(a.Instances[idx].HostName, ShouldEqual, "node1.localdomain") + } else { + So(a.Instances[idx].IPAddr, ShouldEqual, "172.16.0.22") + So(a.Instances[idx].HostName, ShouldEqual, "node2.localdomain") + } } - } - }) + }) + } } func TestRegistration(t *testing.T) { @@ -52,30 +56,33 @@ func TestRegistration(t *testing.T) { SecureVipAddress: "127.0.0.10", Status: fargo.UP, } - Convey("Fail to heartbeat a non-existent instance", t, func() { - j := fargo.Instance{ - HostName: "i-6543", - Port: 9090, - 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, - } - err := e.HeartBeatInstance(&j) - So(err, ShouldNotBeNil) - }) - Convey("Register an instance to TESTAPP", t, func() { - Convey("Instance registers correctly", func() { - err := e.RegisterInstance(&i) - So(err, ShouldBeNil) + for _, j := range []bool{false, true} { + e.UseJson = j + Convey("Fail to heartbeat a non-existent instance", t, func() { + j := fargo.Instance{ + HostName: "i-6543", + Port: 9090, + 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, + } + err := e.HeartBeatInstance(&j) + So(err, ShouldNotBeNil) }) - Convey("Instance can check in", func() { - err := e.HeartBeatInstance(&i) - So(err, ShouldBeNil) + Convey("Register an instance to TESTAPP", t, func() { + Convey("Instance registers correctly", func() { + err := e.RegisterInstance(&i) + So(err, ShouldBeNil) + }) + Convey("Instance can check in", func() { + err := e.HeartBeatInstance(&i) + So(err, ShouldBeNil) + }) }) - }) + } } func TestReregistration(t *testing.T) { @@ -90,22 +97,29 @@ func TestReregistration(t *testing.T) { SecureVipAddress: "127.0.0.10", Status: fargo.UP, } - Convey("Register a TESTAPP instance", t, func() { - Convey("Instance registers correctly", func() { - err := e.RegisterInstance(&i) - So(err, ShouldBeNil) + //fmt.Printf("\nTestReregistration0: ins:%s/%s\n", i.App, i.HostName) + for _, j := range []bool{false, true} { + e.UseJson = j + Convey("Register a TESTAPP instance", t, func() { + Convey("Instance registers correctly", func() { + //fmt.Printf("\nTestReregistration1: ins:%s/%s\n", i.App, i.HostName) + 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("Instance can check in", func() { - err := e.HeartBeatInstance(&i) - So(err, ShouldBeNil) + Convey("Reregister the TESTAPP instance", t, func() { + //fmt.Printf("\nTestReregistration2: ins:%s/%s\n", i.App, i.HostName) + Convey("Instance reregisters correctly", func() { + err := e.ReregisterInstance(&i) + So(err, ShouldBeNil) + }) + fmt.Printf("\nTestReregistration3: ins:%s/%s\n", i.App, i.HostName) + Convey("Instance can check in", func() { + err := e.HeartBeatInstance(&i) + So(err, ShouldBeNil) + }) }) - }) + } } func DontTestDeregistration(t *testing.T) { @@ -150,45 +164,51 @@ func TestUpdateStatus(t *testing.T) { SecureVipAddress: "127.0.0.10", Status: fargo.UP, } - Convey("Register an instance to TESTAPP", t, func() { - Convey("Instance registers correctly", func() { - err := e.RegisterInstance(&i) - So(err, ShouldBeNil) + for _, j := range []bool{false, true} { + e.UseJson = j + Convey("Register an instance to TESTAPP", t, func() { + Convey("Instance registers correctly", func() { + err := e.RegisterInstance(&i) + So(err, ShouldBeNil) + }) }) - }) - Convey("Update an instance status", t, func() { - Convey("Instance updates to OUT_OF_SERVICE correctly", func() { - err := e.UpdateInstanceStatus(&i, fargo.OUTOFSERVICE) - So(err, ShouldBeNil) + Convey("Update an instance status", t, func() { + Convey("Instance updates to OUT_OF_SERVICE correctly", func() { + err := e.UpdateInstanceStatus(&i, fargo.OUTOFSERVICE) + So(err, ShouldBeNil) + }) + Convey("Instance updates to UP corectly", func() { + err := e.UpdateInstanceStatus(&i, fargo.UP) + So(err, ShouldBeNil) + }) }) - Convey("Instance updates to UP corectly", func() { - err := e.UpdateInstanceStatus(&i, fargo.UP) - So(err, ShouldBeNil) - }) - }) + } } func TestMetadataReading(t *testing.T) { e, _ := fargo.NewConnFromConfigFile("./config_sample/local.gcfg") - Convey("Read empty instance metadata", t, func() { - a, err := e.GetApp("EUREKA") - So(err, ShouldBeNil) - i := a.Instances[0] - _, 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") - }) + for _, j := range []bool{false, true} { + e.UseJson = j + Convey("Read empty instance metadata", t, func() { + a, err := e.GetApp("EUREKA") + So(err, ShouldBeNil) + i := a.Instances[0] + _, 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") + }) + } }