Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accommodate non-Amazon data center info metadata #44

Merged
merged 2 commits into from
Sep 27, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 158 additions & 3 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package fargo
import (
"encoding/json"
"encoding/xml"
"io"
"strconv"
)

Expand Down Expand Up @@ -122,17 +123,22 @@ 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}

if i.parsed != nil {
for key, value := range i.parsed {
t := xml.StartElement{Name: xml.Name{"", key}}
tokens = append(tokens, t, xml.CharData(value.(string)), xml.EndElement{t.Name})
t := startLocalName(key)
tokens = append(tokens, t, xml.CharData(value.(string)), xml.EndElement{Name: t.Name})
}
}
tokens = append(tokens, xml.EndElement{start.Name})
tokens = append(tokens, xml.EndElement{Name: start.Name})

for _, t := range tokens {
err := e.EncodeToken(t)
Expand All @@ -144,3 +150,152 @@ func (i InstanceMetadata) MarshalXML(e *xml.Encoder, start xml.StartElement) err
// flush to ensure tokens are written
return e.Flush()
}

type metadataMap map[string]string

// MarshalXML is a custom XML marshaler for metadataMap, mapping each metadata name/value pair to a
// correspondingly named XML element with the pair's value as character data content.
func (m metadataMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if err := e.EncodeToken(start); err != nil {
return err
}

for k, v := range m {
if err := e.EncodeElement(v, startLocalName(k)); err != nil {
return err
}
}

return e.EncodeToken(start.End())
Copy link
Contributor

@damtur damtur Sep 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentions creating an Encoder:

Callers that create an Encoder and then invoke EncodeToken directly, without using Encode or EncodeElement, need to call Flush when finished to ensure that the XML is written to the underlying writer.

Here, I didn't create the Encoder; it's created implicitly over in fargo.marshal in file net.go. Note that xml.Marshal creates an Encoder and calls Encode on it, which in turn flushes when it's done writing. Hence I don't think that calling Flush explicitly here is necessary.

}

// UnmarshalXML is a custom XML unmarshaler for metadataMap, mapping each XML element's name and
// character data content to a corresponding metadata name/value pair.
func (m metadataMap) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var v string
for {
t, err := d.Token()
if err != nil {
if err == io.EOF {
break
}
return err
}
if k, ok := t.(xml.StartElement); ok {
if err := d.DecodeElement(&v, &k); err != nil {
return err
}
m[k.Name.Local] = v
}
}
return nil
}

func metadataValue(i DataCenterInfo) interface{} {
if i.Name == Amazon {
return i.Metadata
}
return metadataMap(i.AlternateMetadata)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named i.AlternativeMetadata I'm not native speaker but I thought that alternate have different meaning? Please correct me if I'm wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My use of "alternate" here is apparently a North American meaning, which is nearly synonymous with "alternative". I did some reading, and found this explanation ("Alternate' in American English):

In American English, the word alternate is commonly used in the sense of “alternative” when the primary option is no longer available, as in

The road is closed. We will have to find an alternate route.

Some speakers of American English even consider the use of “alternative” to be incorrect in this case, claiming “alternative” implies that the original option is still available.

On the other hand, most Britons consider the usage of “alternate” in this sense to be a mistake and would say “alternative route” instead.

Another source has this to say:

An alternate is something or someone that serves in place of another. An alternative is a second option that does not replace the first. For example, when a road undergoing maintenance is closed to traffic, you have to take an alternate route. But when an under-construction road is still accessible to traffic, you might choose to take an alternative route to avoid congestion. The first option is still there, and the alternative gives you a choice.

Here, our first choice is to use Amazon-related metadata. Failing that, we fall back to using the alternate metadata. To my ear, "alternative" makes it sound like we could choose either of them, but they should never both be available or used at the same time.

It sounds like my choice might confuse our readers in UK, but their preferred form sounds strange to my North American ear. What to do?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Giving that we use either one or another and not both at a time, I'm happy with alternate then. Thanks for digging into that.

}

var (
startName = startLocalName("name")
startMetadata = startLocalName("metadata")
)

// 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 {
if err := e.EncodeToken(start); err != nil {
return err
}

if err := e.EncodeElement(i.Name, startName); err != nil {
return err
}
if err := e.EncodeElement(metadataValue(i), startMetadata); err != nil {
return err
}

return e.EncodeToken(start.End())
}

type preliminaryDataCenterInfo struct {
Name string `xml:"name" json:"name"`
Metadata metadataMap `xml:"metadata" json:"metadata"`
}

func bindValue(dst *string, src map[string]string, k string) bool {
if v, ok := src[k]; ok {
*dst = v
return true
}
return false
}

func populateAmazonMetadata(dst *AmazonMetadataType, src map[string]string) {
bindValue(&dst.AmiLaunchIndex, src, "ami-launch-index")
bindValue(&dst.LocalHostname, src, "local-hostname")
bindValue(&dst.AvailabilityZone, src, "availability-zone")
bindValue(&dst.InstanceID, src, "instance-id")
bindValue(&dst.PublicIpv4, src, "public-ipv4")
bindValue(&dst.PublicHostname, src, "public-hostname")
bindValue(&dst.AmiManifestPath, src, "ami-manifest-path")
bindValue(&dst.LocalIpv4, src, "local-ipv4")
bindValue(&dst.HostName, src, "hostname")
bindValue(&dst.AmiID, src, "ami-id")
bindValue(&dst.InstanceType, src, "instance-type")
}

func adaptDataCenterInfo(dst *DataCenterInfo, src preliminaryDataCenterInfo) {
dst.Name = src.Name
if src.Name == Amazon {
populateAmazonMetadata(&dst.Metadata, src.Metadata)
} else {
dst.AlternateMetadata = src.Metadata
}
}

// UnmarshalXML is a custom XML unmarshaler for DataCenterInfo, populating either Metadata or AlternateMetadata
// depending on the type of data center indicated by the Name.
func (i *DataCenterInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
p := preliminaryDataCenterInfo{
Metadata: make(map[string]string, 11),
}
if err := d.DecodeElement(&p, &start); err != nil {
return err
}
adaptDataCenterInfo(i, p)
return nil
}

// 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) {
type named struct {
Name string `json:"name"`
}
if i.Name == Amazon {
return json.Marshal(struct {
named
Metadata AmazonMetadataType `json:"metadata"`
}{named{i.Name}, i.Metadata})
}
return json.Marshal(struct {
named
Metadata map[string]string `json:"metadata"`
}{named{i.Name}, i.AlternateMetadata})
}

// 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),
}
if err := json.Unmarshal(b, &p); err != nil {
return err
}
adaptDataCenterInfo(i, p)
return nil
}
44 changes: 26 additions & 18 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"time"
)

// EurekaUrlSlugs is a map of resource names -> eureka URLs
// EurekaUrlSlugs is a map of resource names->Eureka URLs.
var EurekaURLSlugs = map[string]string{
"Apps": "apps",
"Instances": "instances",
}

// EurekaConnection is the settings required to make eureka requests
// EurekaConnection is the settings required to make Eureka requests.
type EurekaConnection struct {
ServiceUrls []string
ServicePort int
Expand All @@ -26,30 +26,30 @@ type EurekaConnection struct {
UseJson bool
}

// GetAppsResponseJson lets us deserialize the eureka/v2/apps response JSON--a wrapped GetAppsResponse
// GetAppsResponseJson lets us deserialize the eureka/v2/apps response JSONa wrapped GetAppsResponse.
type GetAppsResponseJson struct {
Response *GetAppsResponse `json:"applications"`
}

// GetAppsResponse lets us deserialize the eureka/v2/apps response XML
// GetAppsResponse lets us deserialize the eureka/v2/apps response XML.
type GetAppsResponse struct {
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
// Application deserializeable from Eureka JSON.
type GetAppResponseJson struct {
Application Application `json:"application"`
}

// Application deserializeable from Eureka XML
// Application deserializeable from Eureka XML.
type Application struct {
Name string `xml:"name" json:"name"`
Instances []*Instance `xml:"instance" json:"instance"`
}

// StatusType is an enum of the different statuses allowed by Eureka
// StatusType is an enum of the different statuses allowed by Eureka.
type StatusType string

// Supported statuses
Expand All @@ -67,12 +67,12 @@ const (
MyOwn = "MyOwn"
)

// RegisterInstanceJson lets us serialize the eureka/v2/apps/<ins> request JSON--a wrapped Instance
// RegisterInstanceJson lets us serialize the eureka/v2/apps/<ins> request JSONa wrapped Instance.
type RegisterInstanceJson struct {
Instance *Instance `json:"instance"`
}

// Instance [de]serializeable [to|from] Eureka XML
// Instance [de]serializeable [to|from] Eureka XML.
type Instance struct {
XMLName struct{} `xml:"instance" json:"-"`
HostName string `xml:"hostName" json:"hostName"`
Expand Down Expand Up @@ -102,21 +102,22 @@ type Instance struct {
UniqueID func(i Instance) string `xml:"-" json:"-"`
}

// Port struct used for JSON [un]marshaling only
// looks like: "port":{"@enabled":"true", "$":"7101"},
// 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.
// InstanceMetadata represents the eureka metadata, which is arbitrary XML.
// See metadata.go for more info.
type InstanceMetadata struct {
Raw []byte `xml:",innerxml" json:"-"`
parsed map[string]interface{}
}

// AmazonMetadataType is information about AZ's, AMI's, and the AWS instance
// AmazonMetadataType is information about AZ's, AMI's, and the AWS instance.
// <xsd:complexType name="amazonMetdataType">
// from http://docs.amazonwebservices.com/AWSEC2/latest/DeveloperGuide/index.html?AESDG-chapter-instancedata.html
type AmazonMetadataType struct {
Expand All @@ -133,13 +134,20 @@ type AmazonMetadataType struct {
InstanceType string `xml:"instance-type" json:"instance-type"`
}

// DataCenterInfo is only really useful when running in AWS.
// DataCenterInfo indicates which type of data center hosts this instance
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for all those comments! Great job

// and conveys details about the instance's environment.
type DataCenterInfo struct {
Name string `xml:"name" json:"name"`
Metadata AmazonMetadataType `xml:"metadata" json:"metadata"`
// Name indicates which type of data center hosts this instance.
Name string
// Metadata provides details specific to an Amazon data center,
// populated and honored when the Name field's value is "Amazon".
Metadata AmazonMetadataType
// AlternateMetadata provides details specific to a data center other than Amazon,
// populated and honored when the Name field's value is not "Amazon".
AlternateMetadata map[string]string
}

// LeaseInfo tells us about the renewal from Eureka, including how old it is
// LeaseInfo tells us about the renewal from Eureka, including how old it is.
type LeaseInfo struct {
RenewalIntervalInSecs int32 `xml:"renewalIntervalInSecs" json:"renewalIntervalInSecs"`
DurationInSecs int32 `xml:"durationInSecs" json:"durationInSecs"`
Expand Down
Loading