Skip to content

Commit

Permalink
Merge pull request #44 from seh/allow-other-dc-metadata
Browse files Browse the repository at this point in the history
Accommodate non-Amazon data center info metadata
  • Loading branch information
damtur committed Sep 27, 2016
2 parents 1fc2cae + bb10d2c commit 9a362c8
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 26 deletions.
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())
}

// 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)
}

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
// 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

0 comments on commit 9a362c8

Please sign in to comment.