From 6de7bd641b93052c706882ce34fc1cb09b870693 Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Tue, 30 Aug 2016 14:00:48 -0400 Subject: [PATCH] Accommodate non-Amazon data center info metadata 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". --- marshal.go | 161 +++++++++++++++++++++++++++++++++++++++++- struct.go | 13 +++- tests/marshal_test.go | 112 +++++++++++++++++++++++++++-- 3 files changed, 275 insertions(+), 11 deletions(-) diff --git a/marshal.go b/marshal.go index 46ed617..2dbdda6 100644 --- a/marshal.go +++ b/marshal.go @@ -5,6 +5,7 @@ package fargo import ( "encoding/json" "encoding/xml" + "io" "strconv" ) @@ -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) @@ -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 +} diff --git a/struct.go b/struct.go index 343b935..df9565c 100644 --- a/struct.go +++ b/struct.go @@ -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. diff --git a/tests/marshal_test.go b/tests/marshal_test.go index 9e60d95..7b4ec3c 100644 --- a/tests/marshal_test.go +++ b/tests/marshal_test.go @@ -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) { @@ -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, + "value1value2", + "value2value1") + }) + }) + }) +} + +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, "Amazon123expected.local") + + 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, + "MyOwnexpected.local123", + "MyOwn123expected.local") + + 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) + }) + }) }) }) })