Skip to content

Commit

Permalink
Accommodate non-Amazon data center info metadata
Browse files Browse the repository at this point in the history
The existing "Metadata" field in the DataCenterInfo struct conveys
details specific to instances running within one of Amazon's data
centers. For instances running in other types of data centers, introduce
a sibling "AlternateMetadata" field, sending and populating it only when
the DataCenterInfo's "Name" field is "Amazon".

Retaining the original "Metadata" field maintains compatibility with
existing callers, unless they had populated that field for data centers
with names other than "Amazon".
  • Loading branch information
seh committed Aug 30, 2016
1 parent fb00b3e commit 6de7bd6
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 11 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 @@ -128,11 +129,11 @@ func (i InstanceMetadata) MarshalXML(e *xml.Encoder, start xml.StartElement) err

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 := xml.StartElement{Name: xml.Name{Space: "", Local: 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 +145,157 @@ func (i InstanceMetadata) MarshalXML(e *xml.Encoder, start xml.StartElement) err
// flush to ensure tokens are written
return e.Flush()
}

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

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, m map[string]string, k string) bool {
if v, ok := m[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
}
13 changes: 10 additions & 3 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,17 @@ 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.
Expand Down
112 changes: 107 additions & 5 deletions tests/marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ package fargo_test

import (
"encoding/json"
"encoding/xml"
"fmt"
"github.com/hudl/fargo"
. "github.com/smartystreets/goconvey/convey"
"io/ioutil"
"testing"

"github.com/hudl/fargo"
. "github.com/smartystreets/goconvey/convey"
)

func TestJsonMarshal(t *testing.T) {
Expand Down Expand Up @@ -44,18 +46,118 @@ func TestJsonMarshal(t *testing.T) {
}

func TestMetadataMarshal(t *testing.T) {
Convey("Given an InstanceMetadata", t, func() {
Convey("Given an Instance with metadata", t, func() {
ins := &fargo.Instance{}
ins.SetMetadataString("key1", "value1")
ins.SetMetadataString("key2", "value2")

Convey("When the metadata are marshalled", func() {
Convey("When the metadata are marshalled as JSON", func() {
b, err := json.Marshal(&ins.Metadata)
fmt.Printf("(debug info b = %s)", b)

Convey("The marshalled JSON should have these values", func() {
So(err, ShouldBeNil)
So(string(b), ShouldEqual, `{"key1":"value1","key2":"value2"}`)
})
})

Convey("When the metadata are marshalled as XML", func() {
b, err := xml.Marshal(&ins.Metadata)

Convey("The marshalled XML should have this value", func() {
So(err, ShouldBeNil)
So(string(b), ShouldBeIn,
"<InstanceMetadata><key1>value1</key1><key2>value2</key2></InstanceMetadata>",
"<InstanceMetadata><key2>value2</key2><key1>value1</key1></InstanceMetadata>")
})
})
})
}

func TestDataCenterInfoMarshal(t *testing.T) {
Convey("Given an Instance situated in a data center", t, func() {
ins := &fargo.Instance{}

Convey("When the data center name is \"Amazon\"", func() {
ins.DataCenterInfo.Name = fargo.Amazon
ins.DataCenterInfo.Metadata.InstanceID = "123"
ins.DataCenterInfo.Metadata.HostName = "expected.local"

Convey("When the data center info is marshalled as JSON", func() {
b, err := json.Marshal(&ins.DataCenterInfo)

Convey("The marshalled JSON should have these values", func() {
So(err, ShouldBeNil)
So(string(b), ShouldEqual, `{"name":"Amazon","metadata":{"ami-launch-index":"","local-hostname":"","availability-zone":"","instance-id":"123","public-ipv4":"","public-hostname":"","ami-manifest-path":"","local-ipv4":"","hostname":"expected.local","ami-id":"","instance-type":""}}`)

Convey("The value unmarshalled from JSON should have the same values as the original", func() {
d := fargo.DataCenterInfo{}
err := json.Unmarshal(b, &d)

So(err, ShouldBeNil)
So(d, ShouldResemble, ins.DataCenterInfo)
})
})
})

Convey("When the data center info is marshalled as XML", func() {
b, err := xml.Marshal(&ins.DataCenterInfo)

Convey("The marshalled XML should have this value", func() {
So(err, ShouldBeNil)
So(string(b), ShouldEqual, "<DataCenterInfo><name>Amazon</name><metadata><ami-launch-index></ami-launch-index><local-hostname></local-hostname><availability-zone></availability-zone><instance-id>123</instance-id><public-ipv4></public-ipv4><public-hostname></public-hostname><ami-manifest-path></ami-manifest-path><local-ipv4></local-ipv4><hostname>expected.local</hostname><ami-id></ami-id><instance-type></instance-type></metadata></DataCenterInfo>")

Convey("The value unmarshalled from XML should have the same values as the original", func() {
d := fargo.DataCenterInfo{}
err := xml.Unmarshal(b, &d)

So(err, ShouldBeNil)
So(d, ShouldResemble, ins.DataCenterInfo)
})
})
})
})

Convey("When the data center name is not \"Amazon\"", func() {
ins.DataCenterInfo.Name = fargo.MyOwn
ins.DataCenterInfo.AlternateMetadata = map[string]string{
"instanceId": "123",
"hostName": "expected.local",
}

Convey("When the data center info is marshalled as JSON", func() {
b, err := json.Marshal(&ins.DataCenterInfo)

Convey("The marshalled JSON should have these values", func() {
So(err, ShouldBeNil)
So(string(b), ShouldEqual, `{"name":"MyOwn","metadata":{"hostName":"expected.local","instanceId":"123"}}`)

Convey("The value unmarshalled from JSON should have the same values as the original", func() {
d := fargo.DataCenterInfo{}
err := json.Unmarshal(b, &d)

So(err, ShouldBeNil)
So(d, ShouldResemble, ins.DataCenterInfo)
})
})
})

Convey("When the data center info is marshalled as XML", func() {
b, err := xml.Marshal(&ins.DataCenterInfo)

Convey("The marshalled XML should have this value", func() {
So(err, ShouldBeNil)
So(string(b), ShouldBeIn,
"<DataCenterInfo><name>MyOwn</name><metadata><hostName>expected.local</hostName><instanceId>123</instanceId></metadata></DataCenterInfo>",
"<DataCenterInfo><name>MyOwn</name><metadata><instanceId>123</instanceId><hostName>expected.local</hostName></metadata></DataCenterInfo>")

Convey("The value unmarshalled from XML should have the same values as the original", func() {
d := fargo.DataCenterInfo{}
err := xml.Unmarshal(b, &d)

So(err, ShouldBeNil)
So(d, ShouldResemble, ins.DataCenterInfo)
})
})
})
})
})
Expand Down

0 comments on commit 6de7bd6

Please sign in to comment.