Просмотр исходного кода

Merge pull request #842 from kubecost/kaelan-asset-unmarshal

Add json unmarshaling for all Asset types and AssetSet
Kaelan Patel 4 лет назад
Родитель
Сommit
94218bda8d
4 измененных файлов с 1532 добавлено и 153 удалено
  1. 24 153
      pkg/kubecost/asset.go
  2. 953 0
      pkg/kubecost/asset_unmarshal.go
  3. 552 0
      pkg/kubecost/asset_unmarshal_test.go
  4. 3 0
      pkg/util/json/json.go

+ 24 - 153
pkg/kubecost/asset.go

@@ -1,7 +1,6 @@
 package kubecost
 
 import (
-	"bytes"
 	"encoding"
 	"fmt"
 	"strings"
@@ -600,21 +599,6 @@ func (a *Any) Equal(that Asset) bool {
 	return true
 }
 
-// MarshalJSON implements json.Marshaler
-func (a *Any) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncode(buffer, "properties", a.Properties(), ",")
-	jsonEncode(buffer, "labels", a.Labels(), ",")
-	jsonEncode(buffer, "window", a.Window(), ",")
-	jsonEncodeString(buffer, "start", a.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", a.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", a.Minutes(), ",")
-	jsonEncodeFloat64(buffer, "adjustment", a.Adjustment(), ",")
-	jsonEncodeFloat64(buffer, "totalCost", a.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (a *Any) String() string {
 	return toString(a)
@@ -842,23 +826,6 @@ func (ca *Cloud) Equal(a Asset) bool {
 	return true
 }
 
-// MarshalJSON implements json.Marshaler
-func (ca *Cloud) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncodeString(buffer, "type", ca.Type().String(), ",")
-	jsonEncode(buffer, "properties", ca.Properties(), ",")
-	jsonEncode(buffer, "labels", ca.Labels(), ",")
-	jsonEncode(buffer, "window", ca.Window(), ",")
-	jsonEncodeString(buffer, "start", ca.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", ca.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", ca.Minutes(), ",")
-	jsonEncodeFloat64(buffer, "adjustment", ca.Adjustment(), ",")
-	jsonEncodeFloat64(buffer, "credit", ca.Credit, ",")
-	jsonEncodeFloat64(buffer, "totalCost", ca.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (ca *Cloud) String() string {
 	return toString(ca)
@@ -1042,21 +1009,6 @@ func (cm *ClusterManagement) Equal(a Asset) bool {
 	return true
 }
 
-// MarshalJSON implements json.Marshler
-func (cm *ClusterManagement) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncodeString(buffer, "type", cm.Type().String(), ",")
-	jsonEncode(buffer, "properties", cm.Properties(), ",")
-	jsonEncode(buffer, "labels", cm.Labels(), ",")
-	jsonEncode(buffer, "window", cm.Window(), ",")
-	jsonEncodeString(buffer, "start", cm.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", cm.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", cm.Minutes(), ",")
-	jsonEncodeFloat64(buffer, "totalCost", cm.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (cm *ClusterManagement) String() string {
 	return toString(cm)
@@ -1323,25 +1275,6 @@ func (d *Disk) Equal(a Asset) bool {
 	return true
 }
 
-// MarshalJSON implements the json.Marshaler interface
-func (d *Disk) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncodeString(buffer, "type", d.Type().String(), ",")
-	jsonEncode(buffer, "properties", d.Properties(), ",")
-	jsonEncode(buffer, "labels", d.Labels(), ",")
-	jsonEncode(buffer, "window", d.Window(), ",")
-	jsonEncodeString(buffer, "start", d.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", d.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", d.Minutes(), ",")
-	jsonEncodeFloat64(buffer, "byteHours", d.ByteHours, ",")
-	jsonEncodeFloat64(buffer, "bytes", d.Bytes(), ",")
-	jsonEncode(buffer, "breakdown", d.Breakdown, ",")
-	jsonEncodeFloat64(buffer, "adjustment", d.Adjustment(), ",")
-	jsonEncodeFloat64(buffer, "totalCost", d.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (d *Disk) String() string {
 	return toString(d)
@@ -1640,22 +1573,6 @@ func (n *Network) Equal(a Asset) bool {
 	return true
 }
 
-// MarshalJSON implements json.Marshal interface
-func (n *Network) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncodeString(buffer, "type", n.Type().String(), ",")
-	jsonEncode(buffer, "properties", n.Properties(), ",")
-	jsonEncode(buffer, "labels", n.Labels(), ",")
-	jsonEncode(buffer, "window", n.Window(), ",")
-	jsonEncodeString(buffer, "start", n.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", n.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
-	jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
-	jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (n *Network) String() string {
 	return toString(n)
@@ -2000,36 +1917,6 @@ func (n *Node) Equal(a Asset) bool {
 	return true
 }
 
-// MarshalJSON implements json.Marshal interface
-func (n *Node) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncodeString(buffer, "type", n.Type().String(), ",")
-	jsonEncode(buffer, "properties", n.Properties(), ",")
-	jsonEncode(buffer, "labels", n.Labels(), ",")
-	jsonEncode(buffer, "window", n.Window(), ",")
-	jsonEncodeString(buffer, "start", n.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", n.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
-	jsonEncodeString(buffer, "nodeType", n.NodeType, ",")
-	jsonEncodeFloat64(buffer, "cpuCores", n.CPUCores(), ",")
-	jsonEncodeFloat64(buffer, "ramBytes", n.RAMBytes(), ",")
-	jsonEncodeFloat64(buffer, "cpuCoreHours", n.CPUCoreHours, ",")
-	jsonEncodeFloat64(buffer, "ramByteHours", n.RAMByteHours, ",")
-	jsonEncodeFloat64(buffer, "GPUHours", n.GPUHours, ",")
-	jsonEncode(buffer, "cpuBreakdown", n.CPUBreakdown, ",")
-	jsonEncode(buffer, "ramBreakdown", n.RAMBreakdown, ",")
-	jsonEncodeFloat64(buffer, "preemptible", n.Preemptible, ",")
-	jsonEncodeFloat64(buffer, "discount", n.Discount, ",")
-	jsonEncodeFloat64(buffer, "cpuCost", n.CPUCost, ",")
-	jsonEncodeFloat64(buffer, "gpuCost", n.GPUCost, ",")
-	jsonEncodeFloat64(buffer, "gpuCount", n.GPUs(), ",")
-	jsonEncodeFloat64(buffer, "ramCost", n.RAMCost, ",")
-	jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
-	jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (n *Node) String() string {
 	return toString(n)
@@ -2306,22 +2193,6 @@ func (lb *LoadBalancer) Equal(a Asset) bool {
 	return true
 }
 
-// MarshalJSON implements json.Marshal
-func (lb *LoadBalancer) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncodeString(buffer, "type", lb.Type().String(), ",")
-	jsonEncode(buffer, "properties", lb.Properties(), ",")
-	jsonEncode(buffer, "labels", lb.Labels(), ",")
-	jsonEncode(buffer, "window", lb.Window(), ",")
-	jsonEncodeString(buffer, "start", lb.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", lb.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", lb.Minutes(), ",")
-	jsonEncodeFloat64(buffer, "adjustment", lb.Adjustment(), ",")
-	jsonEncodeFloat64(buffer, "totalCost", lb.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (lb *LoadBalancer) String() string {
 	return toString(lb)
@@ -2515,28 +2386,18 @@ func (sa *SharedAsset) Equal(a Asset) bool {
 	return true
 }
 
-// MarshalJSON implements json.Marshaler
-func (sa *SharedAsset) MarshalJSON() ([]byte, error) {
-	buffer := bytes.NewBufferString("{")
-	jsonEncodeString(buffer, "type", sa.Type().String(), ",")
-	jsonEncode(buffer, "properties", sa.Properties(), ",")
-	jsonEncode(buffer, "labels", sa.Labels(), ",")
-	jsonEncode(buffer, "properties", sa.Properties(), ",")
-	jsonEncode(buffer, "labels", sa.Labels(), ",")
-	jsonEncode(buffer, "window", sa.Window(), ",")
-	jsonEncodeString(buffer, "start", sa.Start().Format(time.RFC3339), ",")
-	jsonEncodeString(buffer, "end", sa.End().Format(time.RFC3339), ",")
-	jsonEncodeFloat64(buffer, "minutes", sa.Minutes(), ",")
-	jsonEncodeFloat64(buffer, "totalCost", sa.TotalCost(), "")
-	buffer.WriteString("}")
-	return buffer.Bytes(), nil
-}
-
 // String implements fmt.Stringer
 func (sa *SharedAsset) String() string {
 	return toString(sa)
 }
 
+// This type exists because only the assets map of AssetSet is marshaled to
+// json, which makes it impossible to recreate an AssetSet struct. Thus,
+// the type when unmarshaling a marshaled AssetSet,is AssetSetResponse
+type AssetSetResponse struct {
+	Assets map[string]Asset
+}
+
 // AssetSet stores a set of Assets, each with a unique name, that share
 // a window. An AssetSet is mutable, so treat it like a threadsafe map.
 type AssetSet struct {
@@ -2831,13 +2692,6 @@ func (as *AssetSet) Map() map[string]Asset {
 	return as.Clone().assets
 }
 
-// MarshalJSON JSON-encodes the AssetSet
-func (as *AssetSet) MarshalJSON() ([]byte, error) {
-	as.RLock()
-	defer as.RUnlock()
-	return json.Marshal(as.assets)
-}
-
 func (as *AssetSet) Set(asset Asset, aggregateBy []string) error {
 	if as.IsEmpty() {
 		as.Lock()
@@ -3045,6 +2899,14 @@ func (asr *AssetSetRange) MarshalJSON() ([]byte, error) {
 	return json.Marshal(asr.assets)
 }
 
+// As with AssetSet, AssetSetRange does not serialize all its fields,
+// making it impossible to reconstruct the AssetSetRange from its json.
+// Therefore, the type a marshaled AssetSetRange unmarshals to is
+// AssetSetRangeResponse
+type AssetSetRangeResponse struct {
+	Assets []*AssetSetResponse
+}
+
 func (asr *AssetSetRange) UTCOffset() time.Duration {
 	if asr.Length() == 0 {
 		return 0
@@ -3135,6 +2997,15 @@ func (asr *AssetSetRange) Minutes() float64 {
 	return duration.Minutes()
 }
 
+// This is a helper type. The Asset API returns a json which cannot be natively
+// unmarshaled into any Asset struct. Therefore, this struct IN COMBINATION WITH
+// DESERIALIZATION LOGIC DEFINED IN asset_unmarshal.go can unmarshal a json directly
+// from an Assets API query
+type AssetAPIResponse struct {
+	Code int                   `json:"code"`
+	Data AssetSetRangeResponse `json:"data"`
+}
+
 // Returns true if string slices a and b contain all of the same strings, in any order.
 func sameContents(a, b []string) bool {
 	if len(a) != len(b) {

+ 953 - 0
pkg/kubecost/asset_unmarshal.go

@@ -0,0 +1,953 @@
+package kubecost
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+	"time"
+
+	// gojson is default golang json, required for RawMessage decoding
+	gojson "encoding/json"
+
+	"github.com/kubecost/cost-model/pkg/util/json"
+)
+
+// Encoding and decoding logic for Asset types
+
+// Any marshal and unmarshal
+
+// MarshalJSON implements json.Marshaler
+func (a *Any) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncode(buffer, "properties", a.Properties(), ",")
+	jsonEncode(buffer, "labels", a.Labels(), ",")
+	jsonEncode(buffer, "window", a.Window(), ",")
+	jsonEncodeString(buffer, "start", a.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", a.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", a.Minutes(), ",")
+	jsonEncodeFloat64(buffer, "adjustment", a.Adjustment(), ",")
+	jsonEncodeFloat64(buffer, "totalCost", a.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (a *Any) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = a.InterfaceToAny(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to Any, carrying over relevant fields
+func (a *Any) InterfaceToAny(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	a.properties = &properties
+	a.labels = labels
+	a.start = start
+	a.end = end
+	a.window = Window{
+		start: &start,
+		end:   &end,
+	}
+
+	if adjustment, err := getTypedVal(fmap["adjustment"]); err == nil {
+		a.adjustment = adjustment.(float64)
+	}
+	if Cost, err := getTypedVal(fmap["totalCost"]); err == nil {
+		a.Cost = Cost.(float64) - a.adjustment
+	}
+
+	return nil
+}
+
+// Cloud marshal and unmarshal
+
+// MarshalJSON implements json.Marshaler
+func (ca *Cloud) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncodeString(buffer, "type", ca.Type().String(), ",")
+	jsonEncode(buffer, "properties", ca.Properties(), ",")
+	jsonEncode(buffer, "labels", ca.Labels(), ",")
+	jsonEncode(buffer, "window", ca.Window(), ",")
+	jsonEncodeString(buffer, "start", ca.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", ca.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", ca.Minutes(), ",")
+	jsonEncodeFloat64(buffer, "adjustment", ca.Adjustment(), ",")
+	jsonEncodeFloat64(buffer, "credit", ca.Credit, ",")
+	jsonEncodeFloat64(buffer, "totalCost", ca.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (ca *Cloud) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = ca.InterfaceToCloud(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to Cloud, carrying over relevant fields
+func (ca *Cloud) InterfaceToCloud(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	ca.properties = &properties
+	ca.labels = labels
+	ca.start = start
+	ca.end = end
+	ca.window = Window{
+		start: &start,
+		end:   &end,
+	}
+
+	if adjustment, err := getTypedVal(fmap["adjustment"]); err == nil {
+		ca.adjustment = adjustment.(float64)
+	}
+	if Credit, err := getTypedVal(fmap["credit"]); err == nil {
+		ca.Credit = Credit.(float64)
+	}
+	if Cost, err := getTypedVal(fmap["totalCost"]); err == nil {
+		ca.Cost = Cost.(float64) - ca.adjustment - ca.Credit
+	}
+
+	return nil
+}
+
+// ClusterManagement marshal and unmarshal
+
+// MarshalJSON implements json.Marshler
+func (cm *ClusterManagement) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncodeString(buffer, "type", cm.Type().String(), ",")
+	jsonEncode(buffer, "properties", cm.Properties(), ",")
+	jsonEncode(buffer, "labels", cm.Labels(), ",")
+	jsonEncode(buffer, "window", cm.Window(), ",")
+	jsonEncodeString(buffer, "start", cm.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", cm.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", cm.Minutes(), ",")
+	jsonEncodeFloat64(buffer, "totalCost", cm.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (cm *ClusterManagement) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = cm.InterfaceToClusterManagement(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to ClusterManagement, carrying over relevant fields
+func (cm *ClusterManagement) InterfaceToClusterManagement(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	cm.properties = &properties
+	cm.labels = labels
+	cm.window = Window{
+		start: &start,
+		end:   &end,
+	}
+
+	if Cost, err := getTypedVal(fmap["totalCost"]); err == nil {
+		cm.Cost = Cost.(float64)
+	}
+
+	return nil
+}
+
+// Disk marshal and unmarshal
+
+// MarshalJSON implements the json.Marshaler interface
+func (d *Disk) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncodeString(buffer, "type", d.Type().String(), ",")
+	jsonEncode(buffer, "properties", d.Properties(), ",")
+	jsonEncode(buffer, "labels", d.Labels(), ",")
+	jsonEncode(buffer, "window", d.Window(), ",")
+	jsonEncodeString(buffer, "start", d.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", d.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", d.Minutes(), ",")
+	jsonEncodeFloat64(buffer, "byteHours", d.ByteHours, ",")
+	jsonEncodeFloat64(buffer, "bytes", d.Bytes(), ",")
+	jsonEncode(buffer, "breakdown", d.Breakdown, ",")
+	jsonEncodeFloat64(buffer, "adjustment", d.Adjustment(), ",")
+	jsonEncodeFloat64(buffer, "totalCost", d.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (d *Disk) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = d.InterfaceToDisk(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to Disk, carrying over relevant fields
+func (d *Disk) InterfaceToDisk(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	fbreakdown := fmap["breakdown"].(map[string]interface{})
+
+	breakdown := toBreakdown(fbreakdown)
+
+	d.properties = &properties
+	d.labels = labels
+	d.start = start
+	d.end = end
+	d.window = Window{
+		start: &start,
+		end:   &end,
+	}
+	d.Breakdown = &breakdown
+
+	if adjustment, err := getTypedVal(fmap["adjustment"]); err == nil {
+		d.adjustment = adjustment.(float64)
+	}
+	if Cost, err := getTypedVal(fmap["totalCost"]); err == nil {
+		d.Cost = Cost.(float64) - d.adjustment
+	}
+	if ByteHours, err := getTypedVal(fmap["byteHours"]); err == nil {
+		d.ByteHours = ByteHours.(float64)
+	}
+
+	// d.Local is not marhsaled, and cannot be calculated from marshaled values.
+	// Currently, it is just ignored and not set in the resulting unmarshal to Disk
+	//  be aware that this means a resulting Disk from an unmarshal is therefore NOT
+	// equal to the originally marshaled Disk.
+
+	return nil
+
+}
+
+// Network marshal and unmarshal
+
+// MarshalJSON implements json.Marshal interface
+func (n *Network) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncodeString(buffer, "type", n.Type().String(), ",")
+	jsonEncode(buffer, "properties", n.Properties(), ",")
+	jsonEncode(buffer, "labels", n.Labels(), ",")
+	jsonEncode(buffer, "window", n.Window(), ",")
+	jsonEncodeString(buffer, "start", n.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", n.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
+	jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
+	jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (n *Network) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = n.InterfaceToNetwork(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to Network, carrying over relevant fields
+func (n *Network) InterfaceToNetwork(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	n.properties = &properties
+	n.labels = labels
+	n.start = start
+	n.end = end
+	n.window = Window{
+		start: &start,
+		end:   &end,
+	}
+
+	if adjustment, err := getTypedVal(fmap["adjustment"]); err == nil {
+		n.adjustment = adjustment.(float64)
+	}
+	if Cost, err := getTypedVal(fmap["totalCost"]); err == nil {
+		n.Cost = Cost.(float64) - n.adjustment
+	}
+
+	return nil
+
+}
+
+// Node marshal and unmarshal
+
+// MarshalJSON implements json.Marshal interface
+func (n *Node) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncodeString(buffer, "type", n.Type().String(), ",")
+	jsonEncode(buffer, "properties", n.Properties(), ",")
+	jsonEncode(buffer, "labels", n.Labels(), ",")
+	jsonEncode(buffer, "window", n.Window(), ",")
+	jsonEncodeString(buffer, "start", n.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", n.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
+	jsonEncodeString(buffer, "nodeType", n.NodeType, ",")
+	jsonEncodeFloat64(buffer, "cpuCores", n.CPUCores(), ",")
+	jsonEncodeFloat64(buffer, "ramBytes", n.RAMBytes(), ",")
+	jsonEncodeFloat64(buffer, "cpuCoreHours", n.CPUCoreHours, ",")
+	jsonEncodeFloat64(buffer, "ramByteHours", n.RAMByteHours, ",")
+	jsonEncodeFloat64(buffer, "GPUHours", n.GPUHours, ",")
+	jsonEncode(buffer, "cpuBreakdown", n.CPUBreakdown, ",")
+	jsonEncode(buffer, "ramBreakdown", n.RAMBreakdown, ",")
+	jsonEncodeFloat64(buffer, "preemptible", n.Preemptible, ",")
+	jsonEncodeFloat64(buffer, "discount", n.Discount, ",")
+	jsonEncodeFloat64(buffer, "cpuCost", n.CPUCost, ",")
+	jsonEncodeFloat64(buffer, "gpuCost", n.GPUCost, ",")
+	jsonEncodeFloat64(buffer, "gpuCount", n.GPUs(), ",")
+	jsonEncodeFloat64(buffer, "ramCost", n.RAMCost, ",")
+	jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
+	jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (n *Node) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = n.InterfaceToNode(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to Node, carrying over relevant fields
+func (n *Node) InterfaceToNode(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	fcpuBreakdown := fmap["cpuBreakdown"].(map[string]interface{})
+	framBreakdown := fmap["ramBreakdown"].(map[string]interface{})
+
+	cpuBreakdown := toBreakdown(fcpuBreakdown)
+	ramBreakdown := toBreakdown(framBreakdown)
+
+	n.properties = &properties
+	n.labels = labels
+	n.start = start
+	n.end = end
+	n.window = Window{
+		start: &start,
+		end:   &end,
+	}
+	n.CPUBreakdown = &cpuBreakdown
+	n.RAMBreakdown = &ramBreakdown
+
+	if adjustment, err := getTypedVal(fmap["adjustment"]); err == nil {
+		n.adjustment = adjustment.(float64)
+	}
+	if NodeType, err := getTypedVal(fmap["nodeType"]); err == nil {
+		n.NodeType = NodeType.(string)
+	}
+	if CPUCoreHours, err := getTypedVal(fmap["cpuCoreHours"]); err == nil {
+		n.CPUCoreHours = CPUCoreHours.(float64)
+	}
+	if RAMByteHours, err := getTypedVal(fmap["ramByteHours"]); err == nil {
+		n.RAMByteHours = RAMByteHours.(float64)
+	}
+	if GPUHours, err := getTypedVal(fmap["GPUHours"]); err == nil {
+		n.GPUHours = GPUHours.(float64)
+	}
+	if CPUCost, err := getTypedVal(fmap["cpuCost"]); err == nil {
+		n.CPUCost = CPUCost.(float64)
+	}
+	if GPUCost, err := getTypedVal(fmap["gpuCost"]); err == nil {
+		n.GPUCost = GPUCost.(float64)
+	}
+	if GPUCount, err := getTypedVal(fmap["gpuCount"]); err == nil {
+		n.GPUCount = GPUCount.(float64)
+	}
+	if RAMCost, err := getTypedVal(fmap["ramCost"]); err == nil {
+		n.RAMCost = RAMCost.(float64)
+	}
+	if Discount, err := getTypedVal(fmap["discount"]); err == nil {
+		n.Discount = Discount.(float64)
+	}
+	if Preemptible, err := getTypedVal(fmap["preemptible"]); err == nil {
+		n.Preemptible = Preemptible.(float64)
+	}
+
+	return nil
+}
+
+// Loadbalancer marshal and unmarshal
+
+// MarshalJSON implements json.Marshal
+func (lb *LoadBalancer) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncodeString(buffer, "type", lb.Type().String(), ",")
+	jsonEncode(buffer, "properties", lb.Properties(), ",")
+	jsonEncode(buffer, "labels", lb.Labels(), ",")
+	jsonEncode(buffer, "window", lb.Window(), ",")
+	jsonEncodeString(buffer, "start", lb.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", lb.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", lb.Minutes(), ",")
+	jsonEncodeFloat64(buffer, "adjustment", lb.Adjustment(), ",")
+	jsonEncodeFloat64(buffer, "totalCost", lb.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (lb *LoadBalancer) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = lb.InterfaceToLoadBalancer(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to LoadBalancer, carrying over relevant fields
+func (lb *LoadBalancer) InterfaceToLoadBalancer(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	lb.properties = &properties
+	lb.labels = labels
+	lb.start = start
+	lb.end = end
+	lb.window = Window{
+		start: &start,
+		end:   &end,
+	}
+
+	if adjustment, err := getTypedVal(fmap["adjustment"]); err == nil {
+		lb.adjustment = adjustment.(float64)
+	}
+	if Cost, err := getTypedVal(fmap["totalCost"]); err == nil {
+		lb.Cost = Cost.(float64) - lb.adjustment
+	}
+
+	return nil
+
+}
+
+// SharedAsset marshal and unmarshal
+
+// MarshalJSON implements json.Marshaler
+func (sa *SharedAsset) MarshalJSON() ([]byte, error) {
+	buffer := bytes.NewBufferString("{")
+	jsonEncodeString(buffer, "type", sa.Type().String(), ",")
+	jsonEncode(buffer, "properties", sa.Properties(), ",")
+	jsonEncode(buffer, "labels", sa.Labels(), ",")
+	jsonEncode(buffer, "properties", sa.Properties(), ",")
+	jsonEncode(buffer, "labels", sa.Labels(), ",")
+	jsonEncode(buffer, "window", sa.Window(), ",")
+	jsonEncodeString(buffer, "start", sa.Start().Format(time.RFC3339), ",")
+	jsonEncodeString(buffer, "end", sa.End().Format(time.RFC3339), ",")
+	jsonEncodeFloat64(buffer, "minutes", sa.Minutes(), ",")
+	jsonEncodeFloat64(buffer, "totalCost", sa.TotalCost(), "")
+	buffer.WriteString("}")
+	return buffer.Bytes(), nil
+}
+
+func (sa *SharedAsset) UnmarshalJSON(b []byte) error {
+
+	var f interface{}
+
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	err = sa.InterfaceToSharedAsset(f)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Converts interface{} to SharedAsset, carrying over relevant fields
+func (sa *SharedAsset) InterfaceToSharedAsset(itf interface{}) error {
+
+	fmap := itf.(map[string]interface{})
+
+	// parse properties map to AssetProperties
+	fproperties := fmap["properties"].(map[string]interface{})
+	properties := toAssetProp(fproperties)
+
+	// parse labels map to AssetLabels
+	labels := make(map[string]string)
+	for k, v := range fmap["labels"].(map[string]interface{}) {
+		labels[k] = v.(string)
+	}
+
+	// parse start and end strings to time.Time
+	start, err := time.Parse(time.RFC3339, fmap["start"].(string))
+	if err != nil {
+		return err
+	}
+	end, err := time.Parse(time.RFC3339, fmap["end"].(string))
+	if err != nil {
+		return err
+	}
+
+	sa.properties = &properties
+	sa.labels = labels
+	sa.window = Window{
+		start: &start,
+		end:   &end,
+	}
+
+	if Cost, err := getTypedVal(fmap["totalCost"]); err == nil {
+		sa.Cost = Cost.(float64)
+	}
+
+	return nil
+
+}
+
+// AssetSet marshal
+
+// MarshalJSON JSON-encodes the AssetSet
+func (as *AssetSet) MarshalJSON() ([]byte, error) {
+	as.RLock()
+	defer as.RUnlock()
+	return json.Marshal(as.assets)
+}
+
+// AssetSetResponse for unmarshaling of AssetSet.assets into AssetSet
+
+// Unmarshals a marshaled AssetSet json into AssetSetResponse
+func (asr *AssetSetResponse) UnmarshalJSON(b []byte) error {
+
+	// gojson used here, as jsonitter UnmarshalJSON won't work with RawMessage
+	var assetMap map[string]*gojson.RawMessage
+
+	// Partial unmarshal to map of json RawMessage
+	err := gojson.Unmarshal(b, &assetMap)
+	if err != nil {
+		return err
+	}
+
+	err = asr.RawMessageToAssetSetResponse(assetMap)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (asr *AssetSetResponse) RawMessageToAssetSetResponse(assetMap map[string]*gojson.RawMessage) error {
+
+	newAssetMap := make(map[string]Asset)
+
+	// For each item in asset map, unmarshal to appropriate type
+	for key, rawMessage := range assetMap {
+
+		var f interface{}
+
+		err := json.Unmarshal(*rawMessage, &f)
+		if err != nil {
+			return err
+		}
+
+		fmap := f.(map[string]interface{})
+
+		switch t := fmap["type"]; t {
+		case "Cloud":
+
+			var ca Cloud
+			err := ca.InterfaceToCloud(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &ca
+
+		case "ClusterManagement":
+
+			var cm ClusterManagement
+			err := cm.InterfaceToClusterManagement(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &cm
+
+		case "Disk":
+
+			var d Disk
+			err := d.InterfaceToDisk(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &d
+
+		case "Network":
+
+			var nw Network
+			err := nw.InterfaceToNetwork(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &nw
+
+		case "Node":
+
+			var n Node
+			err := n.InterfaceToNode(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &n
+
+		case "LoadBalancer":
+
+			var lb LoadBalancer
+			err := lb.InterfaceToLoadBalancer(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &lb
+
+		case "Shared":
+
+			var sa SharedAsset
+			err := sa.InterfaceToSharedAsset(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &sa
+
+		default:
+
+			var a Any
+			err := a.InterfaceToAny(f)
+
+			if err != nil {
+				return err
+			}
+
+			newAssetMap[key] = &a
+
+		}
+	}
+
+	asr.Assets = newAssetMap
+
+	return nil
+}
+
+func (asrr *AssetSetRangeResponse) UnmarshalJSON(b []byte) error {
+
+	// gojson used here, as jsonitter UnmarshalJSON won't work with RawMessage
+	var assetMapList []map[string]*gojson.RawMessage
+
+	// Partial unmarshal to map of json RawMessage
+	err := gojson.Unmarshal(b, &assetMapList)
+	if err != nil {
+		return err
+	}
+
+	var assetSetList []*AssetSetResponse
+
+	for _, rawm := range assetMapList {
+
+		var asresp AssetSetResponse
+		err = asresp.RawMessageToAssetSetResponse(rawm)
+		if err != nil {
+			return err
+		}
+
+		assetSetList = append(assetSetList, &asresp)
+
+	}
+
+	asrr.Assets = assetSetList
+
+	return nil
+}
+
+// Extra decoding util functions, for clarity
+
+// Creates an AssetProperties directly from map[string]interface{}
+func toAssetProp(fproperties map[string]interface{}) AssetProperties {
+	var properties AssetProperties
+
+	if category, v := fproperties["category"].(string); v {
+		properties.Category = category
+	}
+	if provider, v := fproperties["provider"].(string); v {
+		properties.Provider = provider
+	}
+	if account, v := fproperties["account"].(string); v {
+		properties.Account = account
+	}
+	if project, v := fproperties["project"].(string); v {
+		properties.Project = project
+	}
+	if service, v := fproperties["service"].(string); v {
+		properties.Service = service
+	}
+	if cluster, v := fproperties["cluster"].(string); v {
+		properties.Cluster = cluster
+	}
+	if name, v := fproperties["name"].(string); v {
+		properties.Name = name
+	}
+	if providerID, v := fproperties["providerID"].(string); v {
+		properties.ProviderID = providerID
+	}
+
+	return properties
+
+}
+
+// Creates an Breakdown directly from map[string]interface{}
+func toBreakdown(fproperties map[string]interface{}) Breakdown {
+	var breakdown Breakdown
+
+	if idle, v := fproperties["idle"].(float64); v {
+		breakdown.Idle = idle
+	}
+	if other, v := fproperties["other"].(float64); v {
+		breakdown.Other = other
+	}
+	if system, v := fproperties["system"].(float64); v {
+		breakdown.System = system
+	}
+	if user, v := fproperties["user"].(float64); v {
+		breakdown.User = user
+	}
+
+	return breakdown
+
+}
+
+// Not strictly nessesary, but cleans up the code and is a secondary check
+// for correct types
+func getTypedVal(itf interface{}) (interface{}, error) {
+	switch itf := itf.(type) {
+	case float64:
+		return float64(itf), nil
+	case string:
+		return string(itf), nil
+	default:
+		unktype := reflect.ValueOf(itf)
+		return nil, fmt.Errorf("Type %v is an invalid type", unktype)
+	}
+}

+ 552 - 0
pkg/kubecost/asset_unmarshal_test.go

@@ -0,0 +1,552 @@
+package kubecost
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+)
+
+var s = time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
+var e = start1.Add(day)
+var unmarshalWindow = NewWindow(&s, &e)
+
+func TestAny_Unmarshal(t *testing.T) {
+
+	any1 := NewAsset(*unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	any1.SetProperties(&AssetProperties{
+		Name:       "any1",
+		Cluster:    "cluster1",
+		ProviderID: "any1",
+	})
+	any1.Cost = 9.0
+	any1.SetAdjustment(1.0)
+
+	bytes, _ := json.Marshal(any1)
+
+	var testany Any
+	any2 := &testany
+
+	err := json.Unmarshal(bytes, any2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("Any Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial Any equal those in Any from unmarshal
+	if !any1.properties.Equal(any2.properties) {
+		t.Fatalf("Any Unmarshal: properties mutated in unmarshal")
+	}
+	if !any1.labels.Equal(any2.labels) {
+		t.Fatalf("Any Unmarshal: labels mutated in unmarshal")
+	}
+	if !any1.window.Equal(any2.window) {
+		t.Fatalf("Any Unmarshal: window mutated in unmarshal")
+	}
+	if !any1.start.Equal(any2.start) {
+		t.Fatalf("Any Unmarshal: start mutated in unmarshal")
+	}
+	if !any1.end.Equal(any2.end) {
+		t.Fatalf("Any Unmarshal: end mutated in unmarshal")
+	}
+	if any1.adjustment != any2.adjustment {
+		t.Fatalf("Any Unmarshal: adjustment mutated in unmarshal")
+	}
+	if any1.Cost != any2.Cost {
+		t.Fatalf("Any Unmarshal: cost mutated in unmarshal")
+	}
+
+	// As a final check, make sure the above checks out
+	if !any1.Equal(any2) {
+		t.Fatalf("Any Unmarshal: Any mutated in unmarshal")
+	}
+
+}
+
+func TestCloud_Unmarshal(t *testing.T) {
+
+	cloud1 := NewCloud("Compute", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	cloud1.SetLabels(map[string]string{
+		"namespace": "namespace1",
+		"env":       "env1",
+		"product":   "product1",
+	})
+	cloud1.Cost = 10.00
+	cloud1.Credit = -1.0
+
+	bytes, _ := json.Marshal(cloud1)
+
+	var testcloud Cloud
+	cloud2 := &testcloud
+
+	err := json.Unmarshal(bytes, cloud2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("Cloud Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial Cloud equal those in Cloud from unmarshal
+	if !cloud1.properties.Equal(cloud2.properties) {
+		t.Fatalf("Cloud Unmarshal: properties mutated in unmarshal")
+	}
+	if !cloud1.labels.Equal(cloud2.labels) {
+		t.Fatalf("Cloud Unmarshal: labels mutated in unmarshal")
+	}
+	if !cloud1.window.Equal(cloud2.window) {
+		t.Fatalf("Cloud Unmarshal: window mutated in unmarshal")
+	}
+	if !cloud1.start.Equal(cloud2.start) {
+		t.Fatalf("Cloud Unmarshal: start mutated in unmarshal")
+	}
+	if !cloud1.end.Equal(cloud2.end) {
+		t.Fatalf("Cloud Unmarshal: end mutated in unmarshal")
+	}
+	if cloud1.adjustment != cloud2.adjustment {
+		t.Fatalf("Cloud Unmarshal: adjustment mutated in unmarshal")
+	}
+	if cloud1.Cost != cloud2.Cost {
+		t.Fatalf("Cloud Unmarshal: cost mutated in unmarshal")
+	}
+	if cloud1.Credit != cloud2.Credit {
+		t.Fatalf("Cloud Unmarshal: credit mutated in unmarshal")
+	}
+
+	// As a final check, make sure the above checks out
+	if !cloud1.Equal(cloud2) {
+		t.Fatalf("Cloud Unmarshal: Cloud mutated in unmarshal")
+	}
+
+}
+
+func TestClusterManagement_Unmarshal(t *testing.T) {
+
+	cm1 := NewClusterManagement("gcp", "cluster1", unmarshalWindow)
+	cm1.Cost = 9.0
+
+	bytes, _ := json.Marshal(cm1)
+
+	var testcm ClusterManagement
+	cm2 := &testcm
+
+	err := json.Unmarshal(bytes, cm2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("ClusterManagement Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial ClusterManagement equal those in ClusterManagement from unmarshal
+	if !cm1.properties.Equal(cm2.properties) {
+		t.Fatalf("ClusterManagement Unmarshal: properties mutated in unmarshal")
+	}
+	if !cm1.labels.Equal(cm2.labels) {
+		t.Fatalf("ClusterManagement Unmarshal: labels mutated in unmarshal")
+	}
+	if !cm1.window.Equal(cm2.window) {
+		t.Fatalf("ClusterManagement Unmarshal: window mutated in unmarshal")
+	}
+	if cm1.Cost != cm2.Cost {
+		t.Fatalf("ClusterManagement Unmarshal: cost mutated in unmarshal")
+	}
+
+	// As a final check, make sure the above checks out
+	if !cm1.Equal(cm2) {
+		t.Fatalf("ClusterManagement Unmarshal: ClusterManagement mutated in unmarshal")
+	}
+
+}
+
+func TestDisk_Unmarshal(t *testing.T) {
+
+	hours := unmarshalWindow.Duration().Hours()
+
+	disk1 := NewDisk("disk1", "cluster1", "disk1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	disk1.ByteHours = 60.0 * gb * hours
+	disk1.Cost = 4.0
+	disk1.Local = 1.0
+	disk1.SetAdjustment(1.0)
+	disk1.Breakdown = &Breakdown{
+		Idle:   0.1,
+		System: 0.2,
+		User:   0.3,
+		Other:  0.4,
+	}
+
+	bytes, _ := json.Marshal(disk1)
+
+	var testdisk Disk
+	disk2 := &testdisk
+
+	err := json.Unmarshal(bytes, disk2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("Disk Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial Disk equal those in Disk from unmarshal
+	if !disk1.properties.Equal(disk2.properties) {
+		t.Fatalf("Disk Unmarshal: properties mutated in unmarshal")
+	}
+	if !disk1.labels.Equal(disk2.labels) {
+		t.Fatalf("Disk Unmarshal: labels mutated in unmarshal")
+	}
+	if !disk1.window.Equal(disk2.window) {
+		t.Fatalf("Disk Unmarshal: window mutated in unmarshal")
+	}
+	if !disk1.Breakdown.Equal(disk2.Breakdown) {
+		t.Fatalf("Disk Unmarshal: Breakdown mutated in unmarshal")
+	}
+	if !disk1.start.Equal(disk2.start) {
+		t.Fatalf("Disk Unmarshal: start mutated in unmarshal")
+	}
+	if !disk1.end.Equal(disk2.end) {
+		t.Fatalf("Disk Unmarshal: end mutated in unmarshal")
+	}
+	if disk1.adjustment != disk2.adjustment {
+		t.Fatalf("Disk Unmarshal: adjustment mutated in unmarshal")
+	}
+	if disk1.ByteHours != disk2.ByteHours {
+		t.Fatalf("Disk Unmarshal: ByteHours mutated in unmarshal")
+	}
+	if disk1.Cost != disk2.Cost {
+		t.Fatalf("Disk Unmarshal: cost mutated in unmarshal")
+	}
+
+	// Local from Disk is not marhsaled, and cannot be calculated from marshaled values.
+	// Currently, it is just ignored and not set in the resulting unmarshal to Disk. Thus,
+	// it is also ignored in this test; be aware that this means a resulting Disk from an
+	// unmarshal is therefore NOT equal to the originally marshaled Disk.
+
+}
+
+func TestNetwork_Unmarshal(t *testing.T) {
+
+	network1 := NewNetwork("network1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	network1.Cost = 4.0
+	network1.SetAdjustment(1.0)
+
+	bytes, _ := json.Marshal(network1)
+
+	var testnw Network
+	network2 := &testnw
+
+	err := json.Unmarshal(bytes, network2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("Network Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial Network equal those in Network from unmarshal
+	if !network1.properties.Equal(network2.properties) {
+		t.Fatalf("Network Unmarshal: properties mutated in unmarshal")
+	}
+	if !network1.labels.Equal(network2.labels) {
+		t.Fatalf("Network Unmarshal: labels mutated in unmarshal")
+	}
+	if !network1.window.Equal(network2.window) {
+		t.Fatalf("Network Unmarshal: window mutated in unmarshal")
+	}
+	if !network1.start.Equal(network2.start) {
+		t.Fatalf("Network Unmarshal: start mutated in unmarshal")
+	}
+	if !network1.end.Equal(network2.end) {
+		t.Fatalf("Network Unmarshal: end mutated in unmarshal")
+	}
+	if network1.adjustment != network2.adjustment {
+		t.Fatalf("Network Unmarshal: adjustment mutated in unmarshal")
+	}
+	if network1.Cost != network2.Cost {
+		t.Fatalf("Network Unmarshal: cost mutated in unmarshal")
+	}
+
+	// As a final check, make sure the above checks out
+	if !network1.Equal(network2) {
+		t.Fatalf("Network Unmarshal: Network mutated in unmarshal")
+	}
+
+}
+
+func TestNode_Unmarshal(t *testing.T) {
+
+	hours := unmarshalWindow.Duration().Hours()
+
+	node1 := NewNode("node1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	node1.CPUCoreHours = 1.0 * hours
+	node1.RAMByteHours = 2.0 * gb * hours
+	node1.GPUHours = 0.0 * hours
+	node1.GPUCost = 0.0
+	node1.CPUCost = 8.0
+	node1.RAMCost = 4.0
+	node1.Discount = 0.3
+	node1.CPUBreakdown = &Breakdown{
+		Idle:   0.6,
+		System: 0.2,
+		User:   0.2,
+		Other:  0.0,
+	}
+	node1.RAMBreakdown = &Breakdown{
+		Idle:   0.6,
+		System: 0.2,
+		User:   0.2,
+		Other:  0.0,
+	}
+	node1.SetAdjustment(1.6)
+
+	bytes, _ := json.Marshal(node1)
+
+	var testnode Node
+	node2 := &testnode
+
+	err := json.Unmarshal(bytes, node2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("Node Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial Node equal those in Node from unmarshal
+	if !node1.properties.Equal(node2.properties) {
+		t.Fatalf("Node Unmarshal: properties mutated in unmarshal")
+	}
+	if !node1.labels.Equal(node2.labels) {
+		t.Fatalf("Node Unmarshal: labels mutated in unmarshal")
+	}
+	if !node1.window.Equal(node2.window) {
+		t.Fatalf("Node Unmarshal: window mutated in unmarshal")
+	}
+	if !node1.CPUBreakdown.Equal(node2.CPUBreakdown) {
+		t.Fatalf("Node Unmarshal: CPUBreakdown mutated in unmarshal")
+	}
+	if !node1.RAMBreakdown.Equal(node2.RAMBreakdown) {
+		t.Fatalf("Node Unmarshal: RAMBreakdown mutated in unmarshal")
+	}
+	if !node1.start.Equal(node2.start) {
+		t.Fatalf("Node Unmarshal: start mutated in unmarshal")
+	}
+	if !node1.end.Equal(node2.end) {
+		t.Fatalf("Node Unmarshal: end mutated in unmarshal")
+	}
+	if node1.adjustment != node2.adjustment {
+		t.Fatalf("Node Unmarshal: adjustment mutated in unmarshal")
+	}
+	if node1.NodeType != node2.NodeType {
+		t.Fatalf("Node Unmarshal: NodeType mutated in unmarshal")
+	}
+	if node1.CPUCoreHours != node2.CPUCoreHours {
+		t.Fatalf("Node Unmarshal: CPUCoreHours mutated in unmarshal")
+	}
+	if node1.RAMByteHours != node2.RAMByteHours {
+		t.Fatalf("Node Unmarshal: RAMByteHours mutated in unmarshal")
+	}
+	if node1.GPUHours != node2.GPUHours {
+		t.Fatalf("Node Unmarshal: GPUHours mutated in unmarshal")
+	}
+	if node1.CPUCost != node2.CPUCost {
+		t.Fatalf("Node Unmarshal: CPUCost mutated in unmarshal")
+	}
+	if node1.GPUCost != node2.GPUCost {
+		t.Fatalf("Node Unmarshal: GPUCost mutated in unmarshal")
+	}
+	if node1.GPUCount != node2.GPUCount {
+		t.Fatalf("Node Unmarshal: GPUCount mutated in unmarshal")
+	}
+	if node1.RAMCost != node2.RAMCost {
+		t.Fatalf("Node Unmarshal: RAMCost mutated in unmarshal")
+	}
+	if node1.Discount != node2.Discount {
+		t.Fatalf("Node Unmarshal: Discount mutated in unmarshal")
+	}
+	if node1.Preemptible != node2.Preemptible {
+		t.Fatalf("Node Unmarshal: Preemptible mutated in unmarshal")
+	}
+
+	// As a final check, make sure the above checks out
+	if !node1.Equal(node2) {
+		t.Fatalf("Node Unmarshal: Node mutated in unmarshal")
+	}
+
+}
+
+func TestLoadBalancer_Unmarshal(t *testing.T) {
+
+	lb1 := NewLoadBalancer("loadbalancer1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	lb1.Cost = 12.0
+	lb1.SetAdjustment(4.0)
+
+	bytes, _ := json.Marshal(lb1)
+
+	var testlb LoadBalancer
+	lb2 := &testlb
+
+	err := json.Unmarshal(bytes, lb2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("LoadBalancer Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial LoadBalancer equal those in LoadBalancer from unmarshal
+	if !lb1.properties.Equal(lb2.properties) {
+		t.Fatalf("LoadBalancer Unmarshal: properties mutated in unmarshal")
+	}
+	if !lb1.labels.Equal(lb2.labels) {
+		t.Fatalf("LoadBalancer Unmarshal: labels mutated in unmarshal")
+	}
+	if !lb1.window.Equal(lb2.window) {
+		t.Fatalf("LoadBalancer Unmarshal: window mutated in unmarshal")
+	}
+	if !lb1.start.Equal(lb2.start) {
+		t.Fatalf("LoadBalancer Unmarshal: start mutated in unmarshal")
+	}
+	if !lb1.end.Equal(lb2.end) {
+		t.Fatalf("LoadBalancer Unmarshal: end mutated in unmarshal")
+	}
+	if lb1.adjustment != lb2.adjustment {
+		t.Fatalf("LoadBalancer Unmarshal: adjustment mutated in unmarshal")
+	}
+	if lb1.Cost != lb2.Cost {
+		t.Fatalf("LoadBalancer Unmarshal: cost mutated in unmarshal")
+	}
+
+	// As a final check, make sure the above checks out
+	if !lb1.Equal(lb2) {
+		t.Fatalf("LoadBalancer Unmarshal: LoadBalancer mutated in unmarshal")
+	}
+
+}
+
+func TestSharedAsset_Unmarshal(t *testing.T) {
+
+	sa1 := NewSharedAsset("sharedasset1", unmarshalWindow)
+	sa1.Cost = 7.0
+
+	bytes, _ := json.Marshal(sa1)
+
+	var testsa SharedAsset
+	sa2 := &testsa
+
+	err := json.Unmarshal(bytes, sa2)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("SharedAsset Unmarshal: unexpected error: %s", err)
+	}
+
+	// Check if all fields in initial SharedAsset equal those in SharedAsset from unmarshal
+	if !sa1.properties.Equal(sa2.properties) {
+		t.Fatalf("SharedAsset Unmarshal: properties mutated in unmarshal")
+	}
+	if !sa1.labels.Equal(sa2.labels) {
+		t.Fatalf("SharedAsset Unmarshal: labels mutated in unmarshal")
+	}
+	if !sa1.window.Equal(sa2.window) {
+		t.Fatalf("SharedAsset Unmarshal: window mutated in unmarshal")
+	}
+	if sa1.Cost != sa2.Cost {
+		t.Fatalf("SharedAsset Unmarshal: cost mutated in unmarshal")
+	}
+
+	// As a final check, make sure the above checks out
+	if !sa1.Equal(sa2) {
+		t.Fatalf("SharedAsset Unmarshal: SharedAsset mutated in unmarshal")
+	}
+
+}
+
+func TestAssetset_Unmarshal(t *testing.T) {
+
+	var s time.Time
+	var e time.Time
+	unmarshalWindow := NewWindow(&s, &e)
+
+	any := NewAsset(*unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	cloud := NewCloud("Compute", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	cm := NewClusterManagement("gcp", "cluster1", unmarshalWindow)
+	disk := NewDisk("disk1", "cluster1", "disk1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	network := NewNetwork("network1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	node := NewNode("node1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	lb := NewLoadBalancer("loadbalancer1", "cluster1", "provider1", *unmarshalWindow.start, *unmarshalWindow.end, unmarshalWindow)
+	sa := NewSharedAsset("sharedasset1", unmarshalWindow)
+
+	assetList := []Asset{any, cloud, cm, disk, network, node, lb, sa}
+
+	assetset := NewAssetSet(s, e, assetList...)
+	bytes, _ := json.Marshal(assetset)
+
+	var assetSetResponse AssetSetResponse
+	assetUnmarshalResponse := &assetSetResponse
+
+	err := json.Unmarshal(bytes, assetUnmarshalResponse)
+
+	// Check if unmarshal was successful
+	if err != nil {
+		t.Fatalf("AssetSet Unmarshal: unexpected error: %s", err)
+	}
+
+	// For each asset in unmarshaled AssetSetResponse, check if it is equal to the corresponding AssetSet asset
+	for key, asset := range assetset.assets {
+
+		if unmarshaledAsset, exists := assetUnmarshalResponse.Assets[key]; exists {
+
+			// As Disk is not marshaled with all fields, the resultant Disk will be unequal. Test all fields we have instead.
+			if unmarshaledAsset.Type().String() == "Disk" {
+
+				udiskEq := func(d1 Asset, d2 Asset) bool {
+
+					asset, _ := asset.(*Disk)
+					unmarshaledAsset, _ := unmarshaledAsset.(*Disk)
+
+					if !asset.Labels().Equal(unmarshaledAsset.Labels()) {
+						return false
+					}
+					if !asset.Properties().Equal(unmarshaledAsset.Properties()) {
+						return false
+					}
+
+					if !asset.Start().Equal(unmarshaledAsset.Start()) {
+						return false
+					}
+					if !asset.End().Equal(unmarshaledAsset.End()) {
+						return false
+					}
+					if !asset.window.Equal(unmarshaledAsset.window) {
+						return false
+					}
+					if asset.adjustment != unmarshaledAsset.adjustment {
+						return false
+					}
+					if asset.Cost != unmarshaledAsset.Cost {
+						return false
+					}
+					if asset.ByteHours != unmarshaledAsset.ByteHours {
+						return false
+					}
+					if !asset.Breakdown.Equal(unmarshaledAsset.Breakdown) {
+						return false
+					}
+
+					return true
+				}
+
+				if res := udiskEq(asset, unmarshaledAsset); !res {
+					t.Fatalf("AssetSet Unmarshal: asset at key '%s' from unmarshaled AssetSetResponse does not match corresponding asset from AssetSet", key)
+				}
+
+			} else {
+
+				if !asset.Equal(unmarshaledAsset) {
+					t.Fatalf("AssetSet Unmarshal: asset at key '%s' from unmarshaled AssetSetResponse does not match corresponding asset from AssetSet", key)
+				}
+
+			}
+
+		} else {
+			t.Fatalf("AssetSet Unmarshal: key '%s' from marshaled AssetSet does not exist in AssetSetResponse", key)
+		}
+
+	}
+
+}

+ 3 - 0
pkg/util/json/json.go

@@ -10,5 +10,8 @@ var Marshal = jsoniter.ConfigCompatibleWithStandardLibrary.Marshal
 var Unmarshal = jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal
 
 type Marshaler json.Marshaler
+type Unmarshaler json.Unmarshaler
+
+type RawMessage json.RawMessage
 
 var NewDecoder = json.NewDecoder