Sean Holcomb 5 лет назад
Родитель
Сommit
3a6ac85e6f

+ 77 - 77
pkg/costmodel/aggregation.go

@@ -42,45 +42,45 @@ const (
 // allocation data per resource, vectors of rate data per resource, efficiency
 // data, and metadata describing the type of aggregation operation.
 type Aggregation struct {
-	Aggregator                 string               `json:"aggregation"`
-	Subfields                  []string             `json:"subfields,omitempty"`
-	Environment                string               `json:"environment"`
-	Cluster                    string               `json:"cluster,omitempty"`
-	Properties                 *kubecost.Properties `json:"-"`
-	Start                      time.Time            `json:"-"`
-	End                        time.Time            `json:"-"`
-	CPUAllocationHourlyAverage float64              `json:"cpuAllocationAverage"`
-	CPUAllocationVectors       []*util.Vector       `json:"-"`
-	CPUAllocationTotal         float64              `json:"-"`
-	CPUCost                    float64              `json:"cpuCost"`
-	CPUCostVector              []*util.Vector       `json:"cpuCostVector,omitempty"`
-	CPUEfficiency              float64              `json:"cpuEfficiency"`
-	CPURequestedVectors        []*util.Vector       `json:"-"`
-	CPUUsedVectors             []*util.Vector       `json:"-"`
-	Efficiency                 float64              `json:"efficiency"`
-	GPUAllocationHourlyAverage float64              `json:"gpuAllocationAverage"`
-	GPUAllocationVectors       []*util.Vector       `json:"-"`
-	GPUCost                    float64              `json:"gpuCost"`
-	GPUCostVector              []*util.Vector       `json:"gpuCostVector,omitempty"`
-	GPUAllocationTotal         float64              `json:"-"`
-	RAMAllocationHourlyAverage float64              `json:"ramAllocationAverage"`
-	RAMAllocationVectors       []*util.Vector       `json:"-"`
-	RAMAllocationTotal         float64              `json:"-"`
-	RAMCost                    float64              `json:"ramCost"`
-	RAMCostVector              []*util.Vector       `json:"ramCostVector,omitempty"`
-	RAMEfficiency              float64              `json:"ramEfficiency"`
-	RAMRequestedVectors        []*util.Vector       `json:"-"`
-	RAMUsedVectors             []*util.Vector       `json:"-"`
-	PVAllocationHourlyAverage  float64              `json:"pvAllocationAverage"`
-	PVAllocationVectors        []*util.Vector       `json:"-"`
-	PVAllocationTotal          float64              `json:"-"`
-	PVCost                     float64              `json:"pvCost"`
-	PVCostVector               []*util.Vector       `json:"pvCostVector,omitempty"`
-	NetworkCost                float64              `json:"networkCost"`
-	NetworkCostVector          []*util.Vector       `json:"networkCostVector,omitempty"`
-	SharedCost                 float64              `json:"sharedCost"`
-	TotalCost                  float64              `json:"totalCost"`
-	TotalCostVector            []*util.Vector       `json:"totalCostVector,omitempty"`
+	Aggregator                 string                         `json:"aggregation"`
+	Subfields                  []string                       `json:"subfields,omitempty"`
+	Environment                string                         `json:"environment"`
+	Cluster                    string                         `json:"cluster,omitempty"`
+	Properties                 *kubecost.AllocationProperties `json:"-"`
+	Start                      time.Time                      `json:"-"`
+	End                        time.Time                      `json:"-"`
+	CPUAllocationHourlyAverage float64                        `json:"cpuAllocationAverage"`
+	CPUAllocationVectors       []*util.Vector                 `json:"-"`
+	CPUAllocationTotal         float64                        `json:"-"`
+	CPUCost                    float64                        `json:"cpuCost"`
+	CPUCostVector              []*util.Vector                 `json:"cpuCostVector,omitempty"`
+	CPUEfficiency              float64                        `json:"cpuEfficiency"`
+	CPURequestedVectors        []*util.Vector                 `json:"-"`
+	CPUUsedVectors             []*util.Vector                 `json:"-"`
+	Efficiency                 float64                        `json:"efficiency"`
+	GPUAllocationHourlyAverage float64                        `json:"gpuAllocationAverage"`
+	GPUAllocationVectors       []*util.Vector                 `json:"-"`
+	GPUCost                    float64                        `json:"gpuCost"`
+	GPUCostVector              []*util.Vector                 `json:"gpuCostVector,omitempty"`
+	GPUAllocationTotal         float64                        `json:"-"`
+	RAMAllocationHourlyAverage float64                        `json:"ramAllocationAverage"`
+	RAMAllocationVectors       []*util.Vector                 `json:"-"`
+	RAMAllocationTotal         float64                        `json:"-"`
+	RAMCost                    float64                        `json:"ramCost"`
+	RAMCostVector              []*util.Vector                 `json:"ramCostVector,omitempty"`
+	RAMEfficiency              float64                        `json:"ramEfficiency"`
+	RAMRequestedVectors        []*util.Vector                 `json:"-"`
+	RAMUsedVectors             []*util.Vector                 `json:"-"`
+	PVAllocationHourlyAverage  float64                        `json:"pvAllocationAverage"`
+	PVAllocationVectors        []*util.Vector                 `json:"-"`
+	PVAllocationTotal          float64                        `json:"-"`
+	PVCost                     float64                        `json:"pvCost"`
+	PVCostVector               []*util.Vector                 `json:"pvCostVector,omitempty"`
+	NetworkCost                float64                        `json:"networkCost"`
+	NetworkCostVector          []*util.Vector                 `json:"networkCostVector,omitempty"`
+	SharedCost                 float64                        `json:"sharedCost"`
+	TotalCost                  float64                        `json:"totalCost"`
+	TotalCostVector            []*util.Vector                 `json:"totalCostVector,omitempty"`
 }
 
 // TotalHours determines the amount of hours the Aggregation covers, as a
@@ -593,19 +593,19 @@ func aggregateDatum(cp cloud.Provider, aggregations map[string]*Aggregation, cos
 			agg.Subfields = subfields
 		}
 		if includeProperties {
-			props := &kubecost.Properties{}
-			props.SetCluster(costDatum.ClusterID)
-			props.SetNode(costDatum.NodeName)
+			props := &kubecost.AllocationProperties{}
+			props.Cluster = costDatum.ClusterID
+			props.Node = costDatum.NodeName
 			if controller, kind, hasController := costDatum.GetController(); hasController {
-				props.SetController(controller)
-				props.SetControllerKind(kind)
+				props.Controller = controller
+				props.ControllerKind = kind
 			}
-			props.SetLabels(costDatum.Labels)
-			props.SetAnnotations(costDatum.Annotations)
-			props.SetNamespace(costDatum.Namespace)
-			props.SetPod(costDatum.PodName)
-			props.SetServices(costDatum.Services)
-			props.SetContainer(costDatum.Name)
+			props.Labels = costDatum.Labels
+			props.Annotations = costDatum.Annotations
+			props.Namespace = costDatum.Namespace
+			props.Pod = costDatum.PodName
+			props.Services = costDatum.Services
+			props.Container = costDatum.Name
 			agg.Properties = props
 		}
 
@@ -2116,41 +2116,41 @@ func (a *Accesses) AggregateCostModelHandler(w http.ResponseWriter, r *http.Requ
 
 // ParseAggregationProperties attempts to parse and return aggregation properties
 // encoded under the given key. If none exist, or if parsing fails, an error
-// is returned with empty Properties.
-func ParseAggregationProperties(qp util.QueryParams, key string) (kubecost.Properties, error) {
-	aggProps := kubecost.Properties{}
+// is returned with empty AllocationProperties.
+func ParseAggregationProperties(qp util.QueryParams, key string) (kubecost.AllocationProperties, error) {
+	aggProps := kubecost.AllocationProperties{}
 
 	labelMap := make(map[string]string)
 	annotationMap := make(map[string]string)
 	for _, raw := range qp.GetList(key, ",") {
 		fields := strings.Split(raw, ":")
 
-		switch kubecost.ParseProperty(fields[0]) {
-		case kubecost.ClusterProp:
-			aggProps.SetCluster("")
-		case kubecost.NodeProp:
-			aggProps.SetNode("")
-		case kubecost.NamespaceProp:
-			aggProps.SetNamespace("")
-		case kubecost.ControllerKindProp:
-			aggProps.SetControllerKind("")
-		case kubecost.ControllerProp:
-			aggProps.SetController("")
-		case kubecost.PodProp:
-			aggProps.SetPod("")
-		case kubecost.ContainerProp:
-			aggProps.SetContainer("")
-		case kubecost.ServiceProp:
-			aggProps.SetServices([]string{})
-		case kubecost.LabelProp:
+		switch prop, _ := kubecost.ParseProperty(fields[0]); prop {
+		case kubecost.AllocationClusterProp:
+			aggProps.Cluster = ""
+		case kubecost.AllocationNodeProp:
+			aggProps.Node = ""
+		case kubecost.AllocationNamespaceProp:
+			aggProps.Namespace = ""
+		case kubecost.AllocationControllerKindProp:
+			aggProps.ControllerKind = ""
+		case kubecost.AllocationControllerProp:
+			aggProps.Controller = ""
+		case kubecost.AllocationPodProp:
+			aggProps.Pod = ""
+		case kubecost.AllocationContainerProp:
+			aggProps.Container = ""
+		case kubecost.AllocationServiceProp:
+			aggProps.Services = []string{}
+		case kubecost.AllocationLabelProp:
 			if len(fields) != 2 {
-				return kubecost.Properties{}, fmt.Errorf("illegal aggregate by label: %s", raw)
+				return kubecost.AllocationProperties{}, fmt.Errorf("illegal aggregate by label: %s", raw)
 			}
 			label := prom.SanitizeLabelName(strings.TrimSpace(fields[1]))
 			labelMap[label] = ""
-		case kubecost.AnnotationProp:
+		case kubecost.AllocationAnnotationProp:
 			if len(fields) != 2 {
-				return kubecost.Properties{}, fmt.Errorf("illegal aggregate by annotation: %s", raw)
+				return kubecost.AllocationProperties{}, fmt.Errorf("illegal aggregate by annotation: %s", raw)
 			}
 			annotation := prom.SanitizeLabelName(strings.TrimSpace(fields[1]))
 			annotationMap[annotation] = ""
@@ -2159,11 +2159,11 @@ func ParseAggregationProperties(qp util.QueryParams, key string) (kubecost.Prope
 	}
 
 	if len(labelMap) > 0 {
-		aggProps.SetLabels(labelMap)
+		aggProps.Labels = labelMap
 	}
 
 	if len(annotationMap) > 0 {
-		aggProps.SetAnnotations(annotationMap)
+		aggProps.Annotations = annotationMap
 	}
 
 	return aggProps, nil
@@ -2223,7 +2223,7 @@ func (a *Accesses) ComputeAllocationHandler(w http.ResponseWriter, r *http.Reque
 	}
 
 	// Aggregate, if requested
-	if len(aggregateBy) > 0 {
+	if !aggregateBy.Equal(&kubecost.AllocationProperties{}) {
 		err = asr.AggregateBy(aggregateBy, nil)
 		if err != nil {
 			WriteError(w, InternalServerError(err.Error()))

+ 31 - 37
pkg/costmodel/allocation.go

@@ -326,11 +326,11 @@ func (cm *CostModel) ComputeAllocation(start, end time.Time, resolution time.Dur
 
 	for _, pod := range podMap {
 		for _, alloc := range pod.Allocations {
-			cluster, _ := alloc.Properties.GetCluster()
-			nodeName, _ := alloc.Properties.GetNode()
-			namespace, _ := alloc.Properties.GetNamespace()
-			pod, _ := alloc.Properties.GetPod()
-			container, _ := alloc.Properties.GetContainer()
+			cluster := alloc.Properties.Cluster
+			nodeName:= alloc.Properties.Node
+			namespace := alloc.Properties.Namespace
+			pod := alloc.Properties.Pod
+			container := alloc.Properties.Container
 
 			podKey := newPodKey(cluster, namespace, pod)
 			nodeKey := newNodeKey(cluster, nodeName)
@@ -593,7 +593,7 @@ func applyCPUCoresAllocated(podMap map[podKey]*Pod, resCPUCoresAllocated []*prom
 			log.Warningf("CostModel.ComputeAllocation: CPU allocation query result missing 'node': %s", key)
 			continue
 		}
-		pod.Allocations[container].Properties.SetNode(node)
+		pod.Allocations[container].Properties.Node = node
 	}
 }
 
@@ -633,7 +633,7 @@ func applyCPUCoresRequested(podMap map[podKey]*Pod, resCPUCoresRequested []*prom
 			log.Warningf("CostModel.ComputeAllocation: CPU request query result missing 'node': %s", key)
 			continue
 		}
-		pod.Allocations[container].Properties.SetNode(node)
+		pod.Allocations[container].Properties.Node = node
 	}
 }
 
@@ -696,7 +696,7 @@ func applyRAMBytesAllocated(podMap map[podKey]*Pod, resRAMBytesAllocated []*prom
 			log.Warningf("CostModel.ComputeAllocation: RAM allocation query result missing 'node': %s", key)
 			continue
 		}
-		pod.Allocations[container].Properties.SetNode(node)
+		pod.Allocations[container].Properties.Node = node
 	}
 }
 
@@ -736,7 +736,7 @@ func applyRAMBytesRequested(podMap map[podKey]*Pod, resRAMBytesRequested []*prom
 			log.Warningf("CostModel.ComputeAllocation: RAM request query result missing 'node': %s", key)
 			continue
 		}
-		pod.Allocations[container].Properties.SetNode(node)
+		pod.Allocations[container].Properties.Node = node
 	}
 }
 
@@ -914,10 +914,7 @@ func resToPodAnnotations(resPodAnnotations []*prom.QueryResult) map[podKey]map[s
 func applyLabels(podMap map[podKey]*Pod, namespaceLabels map[namespaceKey]map[string]string, podLabels map[podKey]map[string]string) {
 	for podKey, pod := range podMap {
 		for _, alloc := range pod.Allocations {
-			allocLabels, err := alloc.Properties.GetLabels()
-			if err != nil {
-				allocLabels = map[string]string{}
-			}
+			allocLabels := alloc.Properties.Labels
 
 			// Apply namespace labels first, then pod labels so that pod labels
 			// overwrite namespace labels.
@@ -933,7 +930,7 @@ func applyLabels(podMap map[podKey]*Pod, namespaceLabels map[namespaceKey]map[st
 				}
 			}
 
-			alloc.Properties.SetLabels(allocLabels)
+			alloc.Properties.Labels = allocLabels
 		}
 	}
 }
@@ -941,10 +938,7 @@ func applyLabels(podMap map[podKey]*Pod, namespaceLabels map[namespaceKey]map[st
 func applyAnnotations(podMap map[podKey]*Pod, namespaceAnnotations map[string]map[string]string, podAnnotations map[podKey]map[string]string) {
 	for key, pod := range podMap {
 		for _, alloc := range pod.Allocations {
-			allocAnnotations, err := alloc.Properties.GetAnnotations()
-			if err != nil {
-				allocAnnotations = map[string]string{}
-			}
+			allocAnnotations := alloc.Properties.Annotations
 
 			// Apply namespace annotations first, then pod annotations so that
 			// pod labels overwrite namespace labels.
@@ -959,7 +953,7 @@ func applyAnnotations(podMap map[podKey]*Pod, namespaceAnnotations map[string]ma
 				}
 			}
 
-			alloc.Properties.SetAnnotations(allocAnnotations)
+			alloc.Properties.Annotations = allocAnnotations
 		}
 	}
 }
@@ -1179,7 +1173,7 @@ func applyServicesToPods(podMap map[podKey]*Pod, podLabels map[podKey]map[string
 					services = append(services, sKey.Service)
 					allocsByService[sKey] = append(allocsByService[sKey], alloc)
 				}
-				alloc.Properties.SetServices(services)
+				alloc.Properties.Services = services
 
 			}
 		}
@@ -1190,8 +1184,8 @@ func applyControllersToPods(podMap map[podKey]*Pod, podControllerMap map[podKey]
 	for key, pod := range podMap {
 		for _, alloc := range pod.Allocations {
 			if controllerKey, ok := podControllerMap[key]; ok {
-				alloc.Properties.SetControllerKind(controllerKey.ControllerKind)
-				alloc.Properties.SetController(controllerKey.Controller)
+				alloc.Properties.ControllerKind = controllerKey.ControllerKind
+				alloc.Properties.Controller = controllerKey.Controller
 			}
 		}
 	}
@@ -1550,11 +1544,11 @@ func applyUnmountedPVs(window kubecost.Window, podMap map[podKey]*Pod, pvMap map
 		}
 
 		podMap[key].AppendContainer(container)
-		podMap[key].Allocations[container].Properties.SetCluster(cluster)
-		podMap[key].Allocations[container].Properties.SetNode(node)
-		podMap[key].Allocations[container].Properties.SetNamespace(namespace)
-		podMap[key].Allocations[container].Properties.SetPod(pod)
-		podMap[key].Allocations[container].Properties.SetContainer(container)
+		podMap[key].Allocations[container].Properties.Cluster = cluster
+		podMap[key].Allocations[container].Properties.Node = node
+		podMap[key].Allocations[container].Properties.Namespace = namespace
+		podMap[key].Allocations[container].Properties.Pod = pod
+		podMap[key].Allocations[container].Properties.Container = container
 		podMap[key].Allocations[container].PVByteHours = unmountedPVBytes[cluster] * window.Minutes() / 60.0
 		podMap[key].Allocations[container].PVCost = amount
 	}
@@ -1593,11 +1587,11 @@ func applyUnmountedPVCs(window kubecost.Window, podMap map[podKey]*Pod, pvcMap m
 		}
 
 		podMap[podKey].AppendContainer(container)
-		podMap[podKey].Allocations[container].Properties.SetCluster(cluster)
-		podMap[podKey].Allocations[container].Properties.SetNode(node)
-		podMap[podKey].Allocations[container].Properties.SetNamespace(namespace)
-		podMap[podKey].Allocations[container].Properties.SetPod(pod)
-		podMap[podKey].Allocations[container].Properties.SetContainer(container)
+		podMap[podKey].Allocations[container].Properties.Cluster = cluster
+		podMap[podKey].Allocations[container].Properties.Node = node
+		podMap[podKey].Allocations[container].Properties.Namespace = namespace
+		podMap[podKey].Allocations[container].Properties.Pod = pod
+		podMap[podKey].Allocations[container].Properties.Container = container
 		podMap[podKey].Allocations[container].PVByteHours = unmountedPVCBytes[key] * window.Minutes() / 60.0
 		podMap[podKey].Allocations[container].PVCost = amount
 	}
@@ -1824,15 +1818,15 @@ func (p Pod) AppendContainer(container string) {
 
 	alloc := &kubecost.Allocation{
 		Name:       name,
-		Properties: kubecost.Properties{},
+		Properties: &kubecost.AllocationProperties{},
 		Window:     p.Window.Clone(),
 		Start:      p.Start,
 		End:        p.End,
 	}
-	alloc.Properties.SetContainer(container)
-	alloc.Properties.SetPod(p.Key.Pod)
-	alloc.Properties.SetNamespace(p.Key.Namespace)
-	alloc.Properties.SetCluster(p.Key.Cluster)
+	alloc.Properties.Container = container
+	alloc.Properties.Pod = p.Key.Pod
+	alloc.Properties.Namespace = p.Key.Namespace
+	alloc.Properties.Cluster = p.Key.Cluster
 
 	p.Allocations[container] = alloc
 }

+ 142 - 140
pkg/kubecost/allocation.go

@@ -47,32 +47,34 @@ const ShareNone = "__none__"
 // Allocation is a unit of resource allocation and cost for a given window
 // of time and for a given kubernetes construct with its associated set of
 // properties.
-// TODO:CLEANUP consider dropping name in favor of just Properties and an
+// TODO:CLEANUP consider dropping name in favor of just AllocationProperties and an
 // Assets-style key() function for AllocationSet.
 type Allocation struct {
-	Name                   string     `json:"name"`
-	Properties             Properties `json:"properties,omitempty"`
-	Window                 Window     `json:"window"`
-	Start                  time.Time  `json:"start"`
-	End                    time.Time  `json:"end"`
-	CPUCoreHours           float64    `json:"cpuCoreHours"`
-	CPUCoreRequestAverage  float64    `json:"cpuCoreRequestAverage"`
-	CPUCoreUsageAverage    float64    `json:"cpuCoreUsageAverage"`
-	CPUCost                float64    `json:"cpuCost"`
-	GPUHours               float64    `json:"gpuHours"`
-	GPUCost                float64    `json:"gpuCost"`
-	NetworkCost            float64    `json:"networkCost"`
-	LoadBalancerCost       float64    `json:"loadBalancerCost"`
-	PVByteHours            float64    `json:"pvByteHours"`
-	PVCost                 float64    `json:"pvCost"`
-	RAMByteHours           float64    `json:"ramByteHours"`
-	RAMBytesRequestAverage float64    `json:"ramByteRequestAverage"`
-	RAMBytesUsageAverage   float64    `json:"ramByteUsageAverage"`
-	RAMCost                float64    `json:"ramCost"`
-	SharedCost             float64    `json:"sharedCost"`
-	ExternalCost           float64    `json:"externalCost"`
+	Name                   string                `json:"name"`
+	Properties             *AllocationProperties  `json:"properties,omitempty"`
+	Window                 Window                `json:"window"`
+	Start                  time.Time             `json:"start"`
+	End                    time.Time             `json:"end"`
+	CPUCoreHours           float64               `json:"cpuCoreHours"`
+	CPUCoreRequestAverage  float64               `json:"cpuCoreRequestAverage"`
+	CPUCoreUsageAverage    float64               `json:"cpuCoreUsageAverage"`
+	CPUCost                float64               `json:"cpuCost"`
+	GPUHours               float64               `json:"gpuHours"`
+	GPUCost                float64               `json:"gpuCost"`
+	NetworkCost            float64               `json:"networkCost"`
+	LoadBalancerCost       float64               `json:"loadBalancerCost"`
+	PVByteHours            float64               `json:"pvByteHours"`
+	PVCost                 float64               `json:"pvCost"`
+	RAMByteHours           float64               `json:"ramByteHours"`
+	RAMBytesRequestAverage float64               `json:"ramByteRequestAverage"`
+	RAMBytesUsageAverage   float64               `json:"ramByteUsageAverage"`
+	RAMCost                float64               `json:"ramCost"`
+	SharedCost             float64               `json:"sharedCost"`
+	ExternalCost           float64               `json:"externalCost"`
 }
 
+
+
 // AllocationMatchFunc is a function that can be used to match Allocations by
 // returning true for any given Allocation if a condition is met.
 type AllocationMatchFunc func(*Allocation) bool
@@ -139,7 +141,7 @@ func (a *Allocation) Equal(that *Allocation) bool {
 	if a.Name != that.Name {
 		return false
 	}
-	if !a.Properties.Equal(&that.Properties) {
+	if !a.Properties.Equal(that.Properties) {
 		return false
 	}
 	if !a.Window.Equal(that.Window) {
@@ -304,7 +306,7 @@ func (a *Allocation) Resolution() time.Duration {
 }
 
 // IsAggregated is true if the given Allocation has been aggregated, which we
-// define by a lack of Properties.
+// define by a lack of AllocationProperties.
 func (a *Allocation) IsAggregated() bool {
 	return a == nil || a.Properties == nil
 }
@@ -331,7 +333,7 @@ func (a *Allocation) Minutes() float64 {
 }
 
 // Share adds the TotalCost of the given Allocation to the SharedCost of the
-// receiving Allocation. No Start, End, Window, or Properties are considered.
+// receiving Allocation. No Start, End, Window, or AllocationProperties are considered.
 // Neither Allocation is mutated; a new Allocation is always returned.
 func (a *Allocation) Share(that *Allocation) (*Allocation, error) {
 	if that == nil {
@@ -358,28 +360,32 @@ func (a *Allocation) add(that *Allocation) {
 		log.Warningf("Allocation.AggregateBy: trying to add a nil receiver")
 		return
 	}
+	if a.Properties != nil && that.Properties != nil {
+		aCluster := a.Properties.Cluster
+		thatCluster := that.Properties.Cluster
+		aNode := a.Properties.Node
+		thatNode := that.Properties.Node
 
-	aCluster, _ := a.Properties.GetCluster()
-	thatCluster, _ := that.Properties.GetCluster()
-	aNode, _ := a.Properties.GetNode()
-	thatNode, _ := that.Properties.GetNode()
+		// reset properties
+		a.Properties = nil
 
-	// reset properties
-	a.Properties = nil
-
-	// ensure that we carry cluster ID and/or node over if they're the same
-	// required for idle/shared cost allocation
-	if aCluster == thatCluster {
-		a.Properties = Properties{ClusterProp: aCluster}
-	}
-	if aNode == thatNode {
-		if a.Properties == nil {
-			a.Properties = Properties{NodeProp: aNode}
-		} else {
-			a.Properties.SetNode(aNode)
+		// ensure that we carry cluster ID and/or node over if they're the same
+		// required for idle/shared cost allocation
+		if aCluster == thatCluster {
+			a.Properties = &AllocationProperties{Cluster: aCluster}
 		}
+		if aNode == thatNode {
+			if a.Properties == nil {
+				a.Properties = &AllocationProperties{Node: aNode}
+			} else {
+				a.Properties.Node = aNode
+			}
+		}
+	} else {
+		a.Properties = nil
 	}
 
+
 	// Expand the window to encompass both Allocations
 	a.Window = a.Window.Expand(that.Window)
 
@@ -483,9 +489,9 @@ type AllocationAggregationOptions struct {
 }
 
 // AggregateBy aggregates the Allocations in the given AllocationSet by the given
-// Property. This will only be legal if the AllocationSet is divisible by the
-// given Property; e.g. Containers can be divided by Namespace, but not vice-a-versa.
-func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationAggregationOptions) error {
+// AllocationProperty. This will only be legal if the AllocationSet is divisible by the
+// given AllocationProperty; e.g. Containers can be divided by Namespace, but not vice-a-versa.
+func (as *AllocationSet) AggregateBy(properties AllocationProperties, options *AllocationAggregationOptions) error {
 	// The order of operations for aggregating allocations is as follows:
 	//  1. Partition external, idle, and shared allocations into separate sets.
 	//     Also, create the aggSet into which the results will be aggregated.
@@ -691,7 +697,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 				Start:      as.Start(),
 				End:        as.End(),
 				SharedCost: totalSharedCost,
-				Properties: Properties{ClusterProp: SharedSuffix}, // The allocation needs to belong to a cluster,but it really doesn't matter which one, so just make it clear.
+				Properties: &AllocationProperties{Cluster: SharedSuffix}, // The allocation needs to belong to a cluster,but it really doesn't matter which one, so just make it clear.
 			})
 		}
 	}
@@ -709,10 +715,10 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 
 	// (3-5) Filter, distribute idle cost, and aggregate (in that order)
 	for _, alloc := range as.allocations {
-		cluster, err := alloc.Properties.GetCluster()
-		if err != nil {
+		cluster := alloc.Properties.Cluster
+		if cluster == "" {
 			log.Warningf("AllocationSet.AggregateBy: missing cluster for allocation: %s", alloc.Name)
-			return err
+			return fmt.Errorf("ClusterProp is not set")
 		}
 
 		skip := false
@@ -747,9 +753,9 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 			for _, idleAlloc := range idleSet.allocations {
 				// Only share idle if the cluster matches; i.e. the allocation
 				// is from the same cluster as the idle costs
-				idleCluster, err := idleAlloc.Properties.GetCluster()
-				if err != nil {
-					return err
+				idleCluster := idleAlloc.Properties.Cluster
+				if idleCluster == "" {
+					return fmt.Errorf("ClusterProp is not set")
 				}
 				if idleCluster != cluster {
 					continue
@@ -797,8 +803,8 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 	// before sharing with the aggregated allocations.
 	if idleSet.Length() > 0 && shareSet.Length() > 0 {
 		for _, alloc := range shareSet.allocations {
-			cluster, err := alloc.Properties.GetCluster()
-			if err != nil {
+			cluster := alloc.Properties.Cluster
+			if cluster == "" {
 				log.Warningf("AllocationSet.AggregateBy: missing cluster for allocation: %s", alloc.Name)
 				return err
 			}
@@ -807,9 +813,9 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 			for _, idleAlloc := range idleSet.allocations {
 				// Only share idle if the cluster matches; i.e. the allocation
 				// is from the same cluster as the idle costs
-				idleCluster, err := idleAlloc.Properties.GetCluster()
-				if err != nil {
-					return err
+				idleCluster := idleAlloc.Properties.Cluster
+				if idleCluster == "" {
+					return fmt.Errorf("ClusterProp is not set")
 				}
 				if idleCluster != cluster {
 					continue
@@ -870,8 +876,8 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 		for idleKey := range aggSet.idleKeys {
 			idleAlloc := aggSet.Get(idleKey)
 
-			cluster, err := idleAlloc.Properties.GetCluster()
-			if err != nil {
+			cluster := idleAlloc.Properties.Cluster
+			if cluster == "" {
 				log.Warningf("AllocationSet.AggregateBy: idle allocation without cluster: %s", idleAlloc)
 				continue
 			}
@@ -933,7 +939,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 	return nil
 }
 
-func computeShareCoeffs(properties Properties, options *AllocationAggregationOptions, as *AllocationSet) (map[string]float64, error) {
+func computeShareCoeffs(properties AllocationProperties, options *AllocationAggregationOptions, as *AllocationSet) (map[string]float64, error) {
 	// Compute coeffs by totalling per-allocation, then dividing by the total.
 	coeffs := map[string]float64{}
 
@@ -998,7 +1004,7 @@ func computeShareCoeffs(properties Properties, options *AllocationAggregationOpt
 	return coeffs, nil
 }
 
-func computeIdleCoeffs(properties Properties, options *AllocationAggregationOptions, as *AllocationSet, shareSet *AllocationSet) (map[string]map[string]map[string]float64, error) {
+func computeIdleCoeffs(properties AllocationProperties, options *AllocationAggregationOptions, as *AllocationSet, shareSet *AllocationSet) (map[string]map[string]map[string]float64, error) {
 	types := []string{"cpu", "gpu", "ram"}
 
 	// Compute idle coefficients, then save them in AllocationAggregationOptions
@@ -1019,9 +1025,9 @@ func computeIdleCoeffs(properties Properties, options *AllocationAggregationOpti
 		}
 
 		// We need to key the allocations by cluster id
-		clusterID, err := alloc.Properties.GetCluster()
-		if err != nil {
-			return nil, err
+		clusterID := alloc.Properties.Cluster
+		if clusterID == "" {
+			return nil, fmt.Errorf("ClusterProp is not set")
 		}
 
 		// get the name key for the allocation
@@ -1066,9 +1072,9 @@ func computeIdleCoeffs(properties Properties, options *AllocationAggregationOpti
 		}
 
 		// We need to key the allocations by cluster id
-		clusterID, err := alloc.Properties.GetCluster()
-		if err != nil {
-			return nil, err
+		clusterID := alloc.Properties.Cluster
+		if clusterID == ""{
+			return nil, fmt.Errorf("ClusterProp is not set")
 		}
 
 		// get the name key for the allocation
@@ -1119,7 +1125,7 @@ func computeIdleCoeffs(properties Properties, options *AllocationAggregationOpti
 	return coeffs, nil
 }
 
-func (a *Allocation) generateKey(properties Properties) string {
+func (a *Allocation) generateKey(properties AllocationProperties) string {
 	if a == nil {
 		return ""
 	}
@@ -1128,45 +1134,45 @@ func (a *Allocation) generateKey(properties Properties) string {
 	// identifies allocations.
 	names := []string{}
 
-	if properties.HasCluster() {
-		cluster, _ := a.Properties.GetCluster()
+	if properties.Cluster != "" {
+		cluster := a.Properties.Cluster
 		names = append(names, cluster)
 	}
 
-	if properties.HasNode() {
-		node, _ := a.Properties.GetNode()
+	if properties.Node != "" {
+		node := a.Properties.Node
 		names = append(names, node)
 	}
 
-	if properties.HasNamespace() {
-		namespace, _ := a.Properties.GetNamespace()
+	if properties.Namespace != ""  {
+		namespace := a.Properties.Namespace
 		names = append(names, namespace)
 	}
 
-	if properties.HasControllerKind() {
-		controllerKind, err := a.Properties.GetControllerKind()
-		if err != nil {
+	if properties.ControllerKind  != "" {
+		controllerKind := a.Properties.ControllerKind
+		if controllerKind == "" {
 			// Indicate that allocation has no controller
 			controllerKind = UnallocatedSuffix
 		}
-
-		if prop, _ := properties.GetControllerKind(); prop != "" && prop != controllerKind {
-			// The allocation does not have the specified controller kind
-			controllerKind = UnallocatedSuffix
-		}
+		// TODO figure out if the is functional, refactor breaks this
+		//if prop := properties.ControllerKind; prop != "" && prop != controllerKind {
+		//	// The allocation does not have the specified controller kind
+		//	controllerKind = UnallocatedSuffix
+		//}
 		names = append(names, controllerKind)
 	}
 
-	if properties.HasController() {
-		if !properties.HasControllerKind() {
-			controllerKind, err := a.Properties.GetControllerKind()
-			if err == nil {
+	if properties.Controller != "" {
+		if properties.ControllerKind == "" {
+			controllerKind := a.Properties.ControllerKind
+			if controllerKind != "" {
 				names = append(names, controllerKind)
 			}
 		}
 
-		controller, err := a.Properties.GetController()
-		if err != nil {
+		controller := a.Properties.Controller
+		if controller == "" {
 			// Indicate that allocation has no controller
 			controller = UnallocatedSuffix
 		}
@@ -1174,43 +1180,41 @@ func (a *Allocation) generateKey(properties Properties) string {
 		names = append(names, controller)
 	}
 
-	if properties.HasPod() {
-		pod, _ := a.Properties.GetPod()
+	if properties.Pod != "" {
+		pod := a.Properties.Pod
 		names = append(names, pod)
 	}
 
-	if properties.HasContainer() {
-		container, _ := a.Properties.GetContainer()
+	if properties.Container != "" {
+		container := a.Properties.Container
 		names = append(names, container)
 	}
 
-	if properties.HasService() {
-		services, err := a.Properties.GetServices()
-		if err != nil {
+	if properties.Services != nil {
+		services := a.Properties.Services
+		if services == nil {
 			// Indicate that allocation has no services
 			names = append(names, UnallocatedSuffix)
-		} else {
-			if len(services) > 0 {
-				for _, service := range services {
-					names = append(names, service)
-					break
-				}
-			} else {
-				// Indicate that allocation has no services
-				names = append(names, UnallocatedSuffix)
+		}
+		if services != nil && len(services) > 0 {
+			for _, service := range services {
+				names = append(names, service)
+				break
 			}
+		} else {
+			// Indicate that allocation has no services
+			names = append(names, UnallocatedSuffix)
 		}
 	}
 
-	if properties.HasAnnotations() {
-		annotations, err := a.Properties.GetAnnotations()
-		if err != nil {
+	if properties.Annotations != nil   {
+		annotations:= a.Properties.Annotations
+		if annotations == nil {
 			// Indicate that allocation has no annotations
 			names = append(names, UnallocatedSuffix)
-		} else {
+		}
 			annotationNames := []string{}
-
-			aggAnnotations, _ := properties.GetAnnotations()
+			aggAnnotations := properties.Annotations
 			for annotationName := range aggAnnotations {
 				if val, ok := annotations[annotationName]; ok {
 					annotationNames = append(annotationNames, fmt.Sprintf("%s=%s", annotationName, val))
@@ -1230,38 +1234,36 @@ func (a *Allocation) generateKey(properties Properties) string {
 			}
 
 			names = append(names, annotationNames...)
-		}
+
 	}
 
-	if properties.HasLabel() {
-		labels, err := a.Properties.GetLabels()
-		if err != nil {
+	if properties.Labels != nil {
+		labels := a.Properties.Labels
+		if labels == nil {
 			// Indicate that allocation has no labels
 			names = append(names, UnallocatedSuffix)
-		} else {
-			labelNames := []string{}
-
-			aggLabels, _ := properties.GetLabels()
-			for labelName := range aggLabels {
-				if val, ok := labels[labelName]; ok {
-					labelNames = append(labelNames, fmt.Sprintf("%s=%s", labelName, val))
-				} else if indexOf(UnallocatedSuffix, labelNames) == -1 { // if UnallocatedSuffix not already in names
-					labelNames = append(labelNames, UnallocatedSuffix)
-				}
-			}
-			// resolve arbitrary ordering. e.g., app=app0/env=env0 is the same agg as env=env0/app=app0
-			if len(labelNames) > 1 {
-				sort.Strings(labelNames)
-			}
-			unallocatedSuffixIndex := indexOf(UnallocatedSuffix, labelNames)
-			// suffix should be at index 0 if it exists b/c of underscores
-			if unallocatedSuffixIndex != -1 {
-				labelNames = append(labelNames[:unallocatedSuffixIndex], labelNames[unallocatedSuffixIndex+1:]...)
-				labelNames = append(labelNames, UnallocatedSuffix) // append to end
+		}
+		labelNames := []string{}
+		aggLabels:= properties.Labels
+		for labelName := range aggLabels {
+			if val, ok := labels[labelName]; ok {
+				labelNames = append(labelNames, fmt.Sprintf("%s=%s", labelName, val))
+			} else if indexOf(UnallocatedSuffix, labelNames) == -1 { // if UnallocatedSuffix not already in names
+				labelNames = append(labelNames, UnallocatedSuffix)
 			}
-
-			names = append(names, labelNames...)
 		}
+		// resolve arbitrary ordering. e.g., app=app0/env=env0 is the same agg as env=env0/app=app0
+		if len(labelNames) > 1 {
+			sort.Strings(labelNames)
+		}
+		unallocatedSuffixIndex := indexOf(UnallocatedSuffix, labelNames)
+		// suffix should be at index 0 if it exists b/c of underscores
+		if unallocatedSuffixIndex != -1 {
+			labelNames = append(labelNames[:unallocatedSuffixIndex], labelNames[unallocatedSuffixIndex+1:]...)
+			labelNames = append(labelNames, UnallocatedSuffix) // append to end
+		}
+
+		names = append(names, labelNames...)
 	}
 
 	return strings.Join(names, "/")
@@ -1379,8 +1381,8 @@ func (as *AllocationSet) ComputeIdleAllocations(assetSet *AssetSet) (map[string]
 	// Subtract allocated costs from asset costs, leaving only the remaining
 	// idle costs.
 	as.Each(func(name string, a *Allocation) {
-		cluster, err := a.Properties.GetCluster()
-		if err != nil {
+		cluster := a.Properties.Cluster
+		if cluster == "" {
 			// Failed to find allocation's cluster
 			return
 		}
@@ -1422,7 +1424,7 @@ func (as *AllocationSet) ComputeIdleAllocations(assetSet *AssetSet) (map[string]
 		idleAlloc := &Allocation{
 			Name:       fmt.Sprintf("%s/%s", cluster, IdleSuffix),
 			Window:     window.Clone(),
-			Properties: Properties{ClusterProp: cluster},
+			Properties: &AllocationProperties{Cluster: cluster},
 			Start:      start,
 			End:        end,
 			CPUCost:    resources["cpu"],
@@ -1800,7 +1802,7 @@ func (asr *AllocationSetRange) Accumulate() (*AllocationSet, error) {
 
 // AggregateBy aggregates each AllocationSet in the range by the given
 // properties and options.
-func (asr *AllocationSetRange) AggregateBy(properties Properties, options *AllocationAggregationOptions) error {
+func (asr *AllocationSetRange) AggregateBy(properties AllocationProperties, options *AllocationAggregationOptions) error {
 	aggRange := &AllocationSetRange{allocations: []*AllocationSet{}}
 
 	asr.Lock()

+ 170 - 166
pkg/kubecost/allocation_test.go

@@ -12,20 +12,20 @@ import (
 
 const day = 24 * time.Hour
 
-func NewUnitAllocation(name string, start time.Time, resolution time.Duration, props *Properties) *Allocation {
+func NewUnitAllocation(name string, start time.Time, resolution time.Duration, props *AllocationProperties) *Allocation {
 	if name == "" {
 		name = "cluster1/namespace1/pod1/container1"
 	}
 
-	properties := &Properties{}
+	properties := &AllocationProperties{}
 	if props == nil {
-		properties.SetCluster("cluster1")
-		properties.SetNode("node1")
-		properties.SetNamespace("namespace1")
-		properties.SetControllerKind("deployment")
-		properties.SetController("deployment1")
-		properties.SetPod("pod1")
-		properties.SetContainer("container1")
+		properties.Cluster = "cluster1"
+		properties.Node = "node1"
+		properties.Namespace = "namespace1"
+		properties.ControllerKind = "deployment"
+		properties.Controller = "deployment1"
+		properties.Pod = "pod1"
+		properties.Container = "container1"
 	} else {
 		properties = props
 	}
@@ -34,7 +34,7 @@ func NewUnitAllocation(name string, start time.Time, resolution time.Duration, p
 
 	alloc := &Allocation{
 		Name:                   name,
-		Properties:             *properties,
+		Properties:             properties,
 		Window:                 NewWindow(&start, &end).Clone(),
 		Start:                  start,
 		End:                    end,
@@ -103,6 +103,7 @@ func TestAllocation_Add(t *testing.T) {
 	a1 := &Allocation{
 		Start:                  s1,
 		End:                    e1,
+		Properties: &AllocationProperties{},
 		CPUCoreHours:           2.0 * hrs1,
 		CPUCoreRequestAverage:  2.0,
 		CPUCoreUsageAverage:    1.0,
@@ -126,6 +127,7 @@ func TestAllocation_Add(t *testing.T) {
 	a2 := &Allocation{
 		Start:                  s2,
 		End:                    e2,
+		Properties: &AllocationProperties{},
 		CPUCoreHours:           1.0 * hrs2,
 		CPUCoreRequestAverage:  1.0,
 		CPUCoreUsageAverage:    1.0,
@@ -252,6 +254,7 @@ func TestAllocation_Share(t *testing.T) {
 	a1 := &Allocation{
 		Start:                  s1,
 		End:                    e1,
+		Properties: &AllocationProperties{},
 		CPUCoreHours:           2.0 * hrs1,
 		CPUCoreRequestAverage:  2.0,
 		CPUCoreUsageAverage:    1.0,
@@ -275,6 +278,7 @@ func TestAllocation_Share(t *testing.T) {
 	a2 := &Allocation{
 		Start:                  s2,
 		End:                    e2,
+		Properties: &AllocationProperties{},
 		CPUCoreHours:           1.0 * hrs2,
 		CPUCoreRequestAverage:  1.0,
 		CPUCoreUsageAverage:    1.0,
@@ -397,12 +401,12 @@ func TestAllocation_MarshalJSON(t *testing.T) {
 
 	before := &Allocation{
 		Name: "cluster1/namespace1/node1/pod1/container1",
-		Properties: Properties{
-			ClusterProp:   "cluster1",
-			NodeProp:      "node1",
-			NamespaceProp: "namespace1",
-			PodProp:       "pod1",
-			ContainerProp: "container1",
+		Properties: &AllocationProperties{
+			Cluster:   "cluster1",
+			Node:      "node1",
+			Namespace: "namespace1",
+			Pod:       "pod1",
+			Container: "container1",
 		},
 		Window:                 NewWindow(&start, &end),
 		Start:                  start,
@@ -449,8 +453,8 @@ func TestAllocationSet_generateKey(t *testing.T) {
 	var alloc *Allocation
 	var key string
 
-	props := Properties{}
-	props.SetCluster("")
+	props := AllocationProperties{}
+	props.Cluster = "true"
 
 	key = alloc.generateKey(props)
 	if key != "" {
@@ -458,9 +462,9 @@ func TestAllocationSet_generateKey(t *testing.T) {
 	}
 
 	alloc = &Allocation{}
-	alloc.Properties = Properties{
-		ClusterProp: "cluster1",
-		LabelProp: map[string]string{
+	alloc.Properties = &AllocationProperties{
+		Cluster: "cluster1",
+		Labels: map[string]string{
 			"app": "app1",
 			"env": "env1",
 		},
@@ -471,18 +475,18 @@ func TestAllocationSet_generateKey(t *testing.T) {
 		t.Fatalf("generateKey: expected \"cluster1\"; actual \"%s\"", key)
 	}
 
-	props.SetNamespace("")
-	props.SetLabels(map[string]string{"app": ""})
+	props.Namespace = "true"
+	props.Labels = map[string]string{"app": ""}
 
 	key = alloc.generateKey(props)
 	if key != "cluster1//app=app1" {
 		t.Fatalf("generateKey: expected \"cluster1//app=app1\"; actual \"%s\"", key)
 	}
 
-	alloc.Properties = Properties{
-		ClusterProp:   "cluster1",
-		NamespaceProp: "namespace1",
-		LabelProp: map[string]string{
+	alloc.Properties = &AllocationProperties{
+		Cluster:   "cluster1",
+		Namespace: "namespace1",
+		Labels: map[string]string{
 			"app": "app1",
 			"env": "env1",
 		},
@@ -499,153 +503,153 @@ func TestNewAllocationSet(t *testing.T) {
 
 func generateAllocationSet(start time.Time) *AllocationSet {
 	// Idle allocations
-	a1i := NewUnitAllocation(fmt.Sprintf("cluster1/%s", IdleSuffix), start, day, &Properties{
-		ClusterProp: "cluster1",
-		NodeProp:    "node1",
+	a1i := NewUnitAllocation(fmt.Sprintf("cluster1/%s", IdleSuffix), start, day, &AllocationProperties{
+		Cluster: "cluster1",
+		Node:    "node1",
 	})
 	a1i.CPUCost = 5.0
 	a1i.RAMCost = 15.0
 	a1i.GPUCost = 0.0
 
-	a2i := NewUnitAllocation(fmt.Sprintf("cluster2/%s", IdleSuffix), start, day, &Properties{
-		ClusterProp: "cluster2",
+	a2i := NewUnitAllocation(fmt.Sprintf("cluster2/%s", IdleSuffix), start, day, &AllocationProperties{
+		Cluster: "cluster2",
 	})
 	a2i.CPUCost = 5.0
 	a2i.RAMCost = 5.0
 	a2i.GPUCost = 0.0
 
 	// Active allocations
-	a1111 := NewUnitAllocation("cluster1/namespace1/pod1/container1", start, day, &Properties{
-		ClusterProp:   "cluster1",
-		NamespaceProp: "namespace1",
-		PodProp:       "pod1",
-		ContainerProp: "container1",
+	a1111 := NewUnitAllocation("cluster1/namespace1/pod1/container1", start, day, &AllocationProperties{
+		Cluster:   "cluster1",
+		Namespace: "namespace1",
+		Pod:       "pod1",
+		Container: "container1",
 	})
 	a1111.RAMCost = 11.00
 
-	a11abc2 := NewUnitAllocation("cluster1/namespace1/pod-abc/container2", start, day, &Properties{
-		ClusterProp:   "cluster1",
-		NamespaceProp: "namespace1",
-		PodProp:       "pod-abc",
-		ContainerProp: "container2",
+	a11abc2 := NewUnitAllocation("cluster1/namespace1/pod-abc/container2", start, day, &AllocationProperties{
+		Cluster:   "cluster1",
+		Namespace: "namespace1",
+		Pod:       "pod-abc",
+		Container: "container2",
 	})
 
-	a11def3 := NewUnitAllocation("cluster1/namespace1/pod-def/container3", start, day, &Properties{
-		ClusterProp:   "cluster1",
-		NamespaceProp: "namespace1",
-		PodProp:       "pod-def",
-		ContainerProp: "container3",
+	a11def3 := NewUnitAllocation("cluster1/namespace1/pod-def/container3", start, day, &AllocationProperties{
+		Cluster:   "cluster1",
+		Namespace: "namespace1",
+		Pod:       "pod-def",
+		Container: "container3",
 	})
 
-	a12ghi4 := NewUnitAllocation("cluster1/namespace2/pod-ghi/container4", start, day, &Properties{
-		ClusterProp:   "cluster1",
-		NamespaceProp: "namespace2",
-		PodProp:       "pod-ghi",
-		ContainerProp: "container4",
+	a12ghi4 := NewUnitAllocation("cluster1/namespace2/pod-ghi/container4", start, day, &AllocationProperties{
+		Cluster:   "cluster1",
+		Namespace: "namespace2",
+		Pod:       "pod-ghi",
+		Container: "container4",
 	})
 
-	a12ghi5 := NewUnitAllocation("cluster1/namespace2/pod-ghi/container5", start, day, &Properties{
-		ClusterProp:   "cluster1",
-		NamespaceProp: "namespace2",
-		PodProp:       "pod-ghi",
-		ContainerProp: "container5",
+	a12ghi5 := NewUnitAllocation("cluster1/namespace2/pod-ghi/container5", start, day, &AllocationProperties{
+		Cluster:   "cluster1",
+		Namespace: "namespace2",
+		Pod:       "pod-ghi",
+		Container: "container5",
 	})
 
-	a12jkl6 := NewUnitAllocation("cluster1/namespace2/pod-jkl/container6", start, day, &Properties{
-		ClusterProp:   "cluster1",
-		NamespaceProp: "namespace2",
-		PodProp:       "pod-jkl",
-		ContainerProp: "container6",
+	a12jkl6 := NewUnitAllocation("cluster1/namespace2/pod-jkl/container6", start, day, &AllocationProperties{
+		Cluster:   "cluster1",
+		Namespace: "namespace2",
+		Pod:       "pod-jkl",
+		Container: "container6",
 	})
 
-	a22mno4 := NewUnitAllocation("cluster2/namespace2/pod-mno/container4", start, day, &Properties{
-		ClusterProp:   "cluster2",
-		NamespaceProp: "namespace2",
-		PodProp:       "pod-mno",
-		ContainerProp: "container4",
+	a22mno4 := NewUnitAllocation("cluster2/namespace2/pod-mno/container4", start, day, &AllocationProperties{
+		Cluster:   "cluster2",
+		Namespace: "namespace2",
+		Pod:       "pod-mno",
+		Container: "container4",
 	})
 
-	a22mno5 := NewUnitAllocation("cluster2/namespace2/pod-mno/container5", start, day, &Properties{
-		ClusterProp:   "cluster2",
-		NamespaceProp: "namespace2",
-		PodProp:       "pod-mno",
-		ContainerProp: "container5",
+	a22mno5 := NewUnitAllocation("cluster2/namespace2/pod-mno/container5", start, day, &AllocationProperties{
+		Cluster:   "cluster2",
+		Namespace: "namespace2",
+		Pod:       "pod-mno",
+		Container: "container5",
 	})
 
-	a22pqr6 := NewUnitAllocation("cluster2/namespace2/pod-pqr/container6", start, day, &Properties{
-		ClusterProp:   "cluster2",
-		NamespaceProp: "namespace2",
-		PodProp:       "pod-pqr",
-		ContainerProp: "container6",
+	a22pqr6 := NewUnitAllocation("cluster2/namespace2/pod-pqr/container6", start, day, &AllocationProperties{
+		Cluster:   "cluster2",
+		Namespace: "namespace2",
+		Pod:       "pod-pqr",
+		Container: "container6",
 	})
 
-	a23stu7 := NewUnitAllocation("cluster2/namespace3/pod-stu/container7", start, day, &Properties{
-		ClusterProp:   "cluster2",
-		NamespaceProp: "namespace3",
-		PodProp:       "pod-stu",
-		ContainerProp: "container7",
+	a23stu7 := NewUnitAllocation("cluster2/namespace3/pod-stu/container7", start, day, &AllocationProperties{
+		Cluster:   "cluster2",
+		Namespace: "namespace3",
+		Pod:       "pod-stu",
+		Container: "container7",
 	})
 
-	a23vwx8 := NewUnitAllocation("cluster2/namespace3/pod-vwx/container8", start, day, &Properties{
-		ClusterProp:   "cluster2",
-		NamespaceProp: "namespace3",
-		PodProp:       "pod-vwx",
-		ContainerProp: "container8",
+	a23vwx8 := NewUnitAllocation("cluster2/namespace3/pod-vwx/container8", start, day, &AllocationProperties{
+		Cluster:   "cluster2",
+		Namespace: "namespace3",
+		Pod:       "pod-vwx",
+		Container: "container8",
 	})
 
-	a23vwx9 := NewUnitAllocation("cluster2/namespace3/pod-vwx/container9", start, day, &Properties{
-		ClusterProp:   "cluster2",
-		NamespaceProp: "namespace3",
-		PodProp:       "pod-vwx",
-		ContainerProp: "container9",
+	a23vwx9 := NewUnitAllocation("cluster2/namespace3/pod-vwx/container9", start, day, &AllocationProperties{
+		Cluster:   "cluster2",
+		Namespace: "namespace3",
+		Pod:       "pod-vwx",
+		Container: "container9",
 	})
 
 	// Controllers
 
-	a11abc2.Properties.SetControllerKind("deployment")
-	a11abc2.Properties.SetController("deployment1")
-	a11def3.Properties.SetControllerKind("deployment")
-	a11def3.Properties.SetController("deployment1")
-
-	a12ghi4.Properties.SetControllerKind("deployment")
-	a12ghi4.Properties.SetController("deployment2")
-	a12ghi5.Properties.SetControllerKind("deployment")
-	a12ghi5.Properties.SetController("deployment2")
-	a22mno4.Properties.SetControllerKind("deployment")
-	a22mno4.Properties.SetController("deployment2")
-	a22mno5.Properties.SetControllerKind("deployment")
-	a22mno5.Properties.SetController("deployment2")
-
-	a23stu7.Properties.SetControllerKind("deployment")
-	a23stu7.Properties.SetController("deployment3")
-
-	a12jkl6.Properties.SetControllerKind("daemonset")
-	a12jkl6.Properties.SetController("daemonset1")
-	a22pqr6.Properties.SetControllerKind("daemonset")
-	a22pqr6.Properties.SetController("daemonset1")
-
-	a23vwx8.Properties.SetControllerKind("statefulset")
-	a23vwx8.Properties.SetController("statefulset1")
-	a23vwx9.Properties.SetControllerKind("statefulset")
-	a23vwx9.Properties.SetController("statefulset1")
+	a11abc2.Properties.ControllerKind = "deployment"
+	a11abc2.Properties.Controller = "deployment1"
+	a11def3.Properties.ControllerKind = "deployment"
+	a11def3.Properties.Controller = "deployment1"
+
+	a12ghi4.Properties.ControllerKind = "deployment"
+	a12ghi4.Properties.Controller = "deployment2"
+	a12ghi5.Properties.ControllerKind = "deployment"
+	a12ghi5.Properties.Controller = "deployment2"
+	a22mno4.Properties.ControllerKind = "deployment"
+	a22mno4.Properties.Controller = "deployment2"
+	a22mno5.Properties.ControllerKind = "deployment"
+	a22mno5.Properties.Controller = "deployment2"
+
+	a23stu7.Properties.ControllerKind = "deployment"
+	a23stu7.Properties.Controller = "deployment3"
+
+	a12jkl6.Properties.ControllerKind = "daemonset"
+	a12jkl6.Properties.Controller = "daemonset1"
+	a22pqr6.Properties.ControllerKind = "daemonset"
+	a22pqr6.Properties.Controller = "daemonset1"
+
+	a23vwx8.Properties.ControllerKind = "statefulset"
+	a23vwx8.Properties.Controller = "statefulset1"
+	a23vwx9.Properties.ControllerKind = "statefulset"
+	a23vwx9.Properties.Controller = "statefulset1"
 
 	// Labels
 
-	a1111.Properties.SetLabels(map[string]string{"app": "app1", "env": "env1"})
-	a12ghi4.Properties.SetLabels(map[string]string{"app": "app2", "env": "env2"})
-	a12ghi5.Properties.SetLabels(map[string]string{"app": "app2", "env": "env2"})
-	a22mno4.Properties.SetLabels(map[string]string{"app": "app2"})
-	a22mno5.Properties.SetLabels(map[string]string{"app": "app2"})
+	a1111.Properties.Labels = map[string]string{"app": "app1", "env": "env1"}
+	a12ghi4.Properties.Labels = map[string]string{"app": "app2", "env": "env2"}
+	a12ghi5.Properties.Labels = map[string]string{"app": "app2", "env": "env2"}
+	a22mno4.Properties.Labels = map[string]string{"app": "app2"}
+	a22mno5.Properties.Labels = map[string]string{"app": "app2"}
 
 	//Annotations
-	a23stu7.Properties.SetAnnotations(map[string]string{"team": "team1"})
-	a23vwx8.Properties.SetAnnotations(map[string]string{"team": "team2"})
-	a23vwx9.Properties.SetAnnotations(map[string]string{"team": "team1"})
+	a23stu7.Properties.Annotations = map[string]string{"team": "team1"}
+	a23vwx8.Properties.Annotations = map[string]string{"team": "team2"}
+	a23vwx9.Properties.Annotations = map[string]string{"team": "team1"}
 
 	// Services
 
-	a12jkl6.Properties.SetServices([]string{"service1"})
-	a22pqr6.Properties.SetServices([]string{"service1"})
+	a12jkl6.Properties.Services = []string{"service1"}
+	a22pqr6.Properties.Services = []string{"service1"}
 
 	return NewAllocationSet(start, start.Add(day),
 		// idle
@@ -824,12 +828,12 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	sharedOverheadHourlyCost := 7.0
 
 	isNamespace3 := func(a *Allocation) bool {
-		ns, err := a.Properties.GetNamespace()
-		return err == nil && ns == "namespace3"
+		ns := a.Properties.Namespace
+		return ns == "namespace3"
 	}
 
 	isApp1 := func(a *Allocation) bool {
-		ls, _ := a.Properties.GetLabels()
+		ls := a.Properties.Labels
 		if app, ok := ls["app"]; ok && app == "app1" {
 			return true
 		}
@@ -845,7 +849,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1a AggregationProperties=(Cluster)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ClusterProp: ""}, nil)
+	err = as.AggregateBy(AllocationProperties{Cluster: "true"}, nil)
 	assertAllocationSetTotals(t, as, "1a", err, numClusters+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1a", map[string]float64{
 		"cluster1": 46.00,
@@ -856,7 +860,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1b AggregationProperties=(Namespace)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, nil)
+	err = as.AggregateBy(AllocationProperties{Namespace: "true"}, nil)
 	assertAllocationSetTotals(t, as, "1b", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1b", map[string]float64{
 		"namespace1": 28.00,
@@ -868,7 +872,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1c AggregationProperties=(Pod)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{PodProp: true}, nil)
+	err = as.AggregateBy(AllocationProperties{Pod: "true"}, nil)
 	assertAllocationSetTotals(t, as, "1c", err, numPods+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1c", map[string]float64{
 		"pod-jkl":  6.00,
@@ -886,7 +890,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1d AggregationProperties=(Container)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ContainerProp: true}, nil)
+	err = as.AggregateBy(AllocationProperties{Container: "true"}, nil)
 	assertAllocationSetTotals(t, as, "1d", err, numContainers+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1d", map[string]float64{
 		"container2": 6.00,
@@ -904,7 +908,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1e AggregationProperties=(ControllerKind)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ControllerKindProp: true}, nil)
+	err = as.AggregateBy(AllocationProperties{ControllerKind: "true"}, nil)
 	assertAllocationSetTotals(t, as, "1e", err, numControllerKinds+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1e", map[string]float64{
 		"daemonset":       12.00,
@@ -917,7 +921,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1f AggregationProperties=(Controller)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ControllerProp: true}, nil)
+	err = as.AggregateBy(AllocationProperties{Controller: "true"}, nil)
 	assertAllocationSetTotals(t, as, "1f", err, numControllers+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1f", map[string]float64{
 		"deployment/deployment2":   24.00,
@@ -932,7 +936,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1g AggregationProperties=(Service)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ServiceProp: true}, nil)
+	err = as.AggregateBy(AllocationProperties{Services: []string{"true"}}, nil)
 	assertAllocationSetTotals(t, as, "1g", err, numServices+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1g", map[string]float64{
 		"service1":        12.00,
@@ -943,7 +947,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1h AggregationProperties=(Label:app)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{LabelProp: map[string]string{"app": ""}}, nil)
+	err = as.AggregateBy(AllocationProperties{Labels: map[string]string{"app": ""}}, nil)
 	assertAllocationSetTotals(t, as, "1h", err, numLabelApps+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1h", map[string]float64{
 		"app=app1":        16.00,
@@ -955,7 +959,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1i AggregationProperties=(ControllerKind:deployment)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ControllerKindProp: "deployment"}, nil)
+	err = as.AggregateBy(AllocationProperties{ControllerKind: "deployment"}, nil)
 	assertAllocationSetTotals(t, as, "1i", err, 1+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1i", map[string]float64{
 		"deployment":      42.00,
@@ -966,7 +970,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1j AggregationProperties=(Annotation:team)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{AnnotationProp: map[string]string{"team": ""}}, nil)
+	err = as.AggregateBy(AllocationProperties{Annotations: map[string]string{"team": ""}}, nil)
 	assertAllocationSetTotals(t, as, "1j", err, 2+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1j", map[string]float64{
 		"team=team1":      12.00,
@@ -984,7 +988,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 2d AggregationProperties=(Label:app, Label:environment)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{LabelProp: map[string]string{"app": "", "env": ""}}, nil)
+	err = as.AggregateBy(AllocationProperties{Labels: map[string]string{"app": "", "env": ""}}, nil)
 	// sets should be {idle, unallocated, app1/env1, app2/env2, app2/unallocated}
 	assertAllocationSetTotals(t, as, "2d", err, numIdle+numUnallocated+3, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "2d", map[string]float64{
@@ -997,7 +1001,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 2e AggregationProperties=(Cluster, Label:app, Label:environment)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ClusterProp: "", LabelProp: map[string]string{"app": "", "env": ""}}, nil)
+	err = as.AggregateBy(AllocationProperties{Cluster: "true", Labels: map[string]string{"app": "", "env": ""}}, nil)
 	assertAllocationSetTotals(t, as, "2e", err, 6, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "2e", map[string]float64{
 		"cluster1/app=app2/env=env2":             12.00,
@@ -1010,7 +1014,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 2f AggregationProperties=(annotation:team, pod)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{AnnotationProp: map[string]string{"team": ""}, PodProp: ""}, nil)
+	err = as.AggregateBy(AllocationProperties{Annotations: map[string]string{"team": ""}, Pod: "true"}, nil)
 	assertAllocationSetTotals(t, as, "2e", err, 11, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "2e", map[string]float64{
 		"pod-jkl/" + UnallocatedSuffix: 6.00,
@@ -1035,7 +1039,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// namespace2: 46.3125 = 36.00 + 5.0*(3.0/6.0) + 15.0*(3.0/16.0) + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
 	// namespace3: 23.0000 = 18.00 + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{ShareIdle: ShareWeighted})
+	err = as.AggregateBy(AllocationProperties{Namespace: "true"}, &AllocationAggregationOptions{ShareIdle: ShareWeighted})
 	assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "3a", map[string]float64{
 		"namespace1": 42.69,
@@ -1049,7 +1053,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// namespace2: 51.0000 = 36.00 + 5.0*(1.0/2.0) + 15.0*(1.0/2.0) + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
 	// namespace3: 23.0000 = 18.00 + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{ShareIdle: ShareEven})
+	err = as.AggregateBy(AllocationProperties{Namespace: "true"}, &AllocationAggregationOptions{ShareIdle: ShareEven})
 	assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "3a", map[string]float64{
 		"namespace1": 38.00,
@@ -1065,7 +1069,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// namespace2: 45.5000 = 36.00 + 18.00*(1.0/2.0)
 	// idle:       30.0000
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: "true"}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isNamespace3},
 		ShareSplit: ShareEven,
 	})
@@ -1082,7 +1086,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// namespace2: 37.5000 =
 	// idle:       30.0000
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: "true"}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isNamespace3},
 		ShareSplit: ShareWeighted,
 	})
@@ -1100,7 +1104,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// namespace3: 23.3333 = 18.00 + 16.00*(1.0/3.0)
 	// idle:       30.0000
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: "true"}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isApp1},
 		ShareSplit: ShareEven,
 	})
@@ -1119,7 +1123,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// namespace3: 54.878 = 18.00 + (7.0*24.0)*(18.00/82.00)
 	// idle:       30.0000
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: "true"}, &AllocationAggregationOptions{
 		SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
 		ShareSplit:        ShareWeighted,
 	})
@@ -1136,21 +1140,21 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	isCluster := func(matchCluster string) func(*Allocation) bool {
 		return func(a *Allocation) bool {
-			cluster, err := a.Properties.GetCluster()
-			return err == nil && cluster == matchCluster
+			cluster := a.Properties.Cluster
+			return cluster == matchCluster
 		}
 	}
 
 	isNamespace := func(matchNamespace string) func(*Allocation) bool {
 		return func(a *Allocation) bool {
-			namespace, err := a.Properties.GetNamespace()
-			return err == nil && namespace == matchNamespace
+			namespace := a.Properties.Namespace
+			return namespace == matchNamespace
 		}
 	}
 
 	// 5a Filter by cluster with separate idle
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ClusterProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Cluster: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
 		ShareIdle:   ShareNone,
 	})
@@ -1163,7 +1167,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 5b Filter by cluster with shared idle
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ClusterProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Cluster: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
 		ShareIdle:   ShareWeighted,
 	})
@@ -1175,7 +1179,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 5c Filter by cluster, agg by namespace, with separate idle
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
 		ShareIdle:   ShareNone,
 	})
@@ -1189,7 +1193,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 5d Filter by namespace, agg by cluster, with separate idle
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ClusterProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Cluster: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareIdle:   ShareNone,
 	})
@@ -1205,7 +1209,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 6a SplitIdle
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{SplitIdle: true})
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{SplitIdle: true})
 	assertAllocationSetTotals(t, as, "6a", err, numNamespaces+numSplitIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "6a", map[string]float64{
 		"namespace1":                           28.00,
@@ -1220,7 +1224,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// Should match values from unfiltered aggregation (3a)
 	// namespace2: 46.3125 = 36.00 + 5.0*(3.0/6.0) + 15.0*(3.0/16.0) + 5.0*(3.0/6.0) + 5.0*(3.0/6.0)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareIdle:   ShareWeighted,
 	})
@@ -1234,7 +1238,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// Should match values from unfiltered aggregation (3b)
 	// namespace2: 51.0000 = 36.00 + 5.0*(1.0/2.0) + 15.0*(1.0/2.0) + 5.0*(1.0/2.0) + 5.0*(1.0/2.0)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareIdle:   ShareEven,
 	})
@@ -1251,7 +1255,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// idle:       30.0000
 	// Then namespace 2 is filtered.
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
 		SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
 		ShareSplit:        ShareWeighted,
@@ -1272,7 +1276,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// namespace2: 54.667 = 36.00 + (28.00)*(36.00/54.00)
 	// idle:       30.0000
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
 		ShareSplit:  ShareWeighted,
@@ -1320,7 +1324,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	//   shared cost    14.2292 = (42.6875)*(18.0/54.0)
 	//
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isNamespace("namespace1")},
 		ShareSplit: ShareWeighted,
 		ShareIdle:  ShareWeighted,
@@ -1370,7 +1374,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	// Then, filter for namespace2: 74.7708
 	//
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
 		ShareSplit:  ShareWeighted,
@@ -1411,7 +1415,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	//
 	// Then namespace 2 is filtered.
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy(AllocationProperties{Namespace: ""}, &AllocationAggregationOptions{
 		FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareSplit:        ShareWeighted,
 		ShareIdle:         ShareWeighted,

+ 308 - 0
pkg/kubecost/allocationprops.go

@@ -0,0 +1,308 @@
+package kubecost
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+)
+
+type AllocationProperty string
+
+const (
+	AllocationNilProp            AllocationProperty = ""
+	AllocationClusterProp        AllocationProperty = "cluster"
+	AllocationNodeProp           AllocationProperty = "node"
+	AllocationContainerProp      AllocationProperty = "container"
+	AllocationControllerProp     AllocationProperty = "controller"
+	AllocationControllerKindProp AllocationProperty = "controllerKind"
+	AllocationNamespaceProp      AllocationProperty = "namespace"
+	AllocationPodProp            AllocationProperty = "pod"
+	AllocationProviderIDProp     AllocationProperty = "providerID"
+	AllocationServiceProp        AllocationProperty = "service"
+	AllocationLabelProp          AllocationProperty = "label"
+	AllocationAnnotationProp     AllocationProperty = "annotation"
+)
+
+func ParseProperty(text string) (AllocationProperty, error) {
+	switch strings.TrimSpace(strings.ToLower(text)) {
+	case "cluster":
+		return AllocationClusterProp, nil
+	case "node":
+		return AllocationNodeProp, nil
+	case "container":
+		return AllocationContainerProp, nil
+	case "controller":
+		return AllocationControllerProp, nil
+	case "controllerKind":
+		return AllocationControllerKindProp, nil
+	case "namespace":
+		return AllocationNamespaceProp, nil
+	case "pod":
+		return AllocationPodProp, nil
+	case "providerid":
+		return AllocationProviderIDProp, nil
+	case "service":
+		return AllocationServiceProp, nil
+	case "label":
+		return AllocationLabelProp, nil
+	case "annotation":
+		return AllocationAnnotationProp, nil
+	}
+	return AllocationNilProp, fmt.Errorf("invalid allocation property: %s", text)
+}
+
+func (p AllocationProperty) String() string {
+	return string(p)
+}
+
+// AllocationProperties describes a set of Kubernetes objects.
+type AllocationProperties struct {
+	Cluster        string                `json:"cluster,omitempty"`
+	Node           string                `json:"node,omitempty"`
+	Container      string                `json:"container,omitempty"`
+	Controller     string                `json:"controller,omitempty"`
+	ControllerKind string                `json:"controllerKind,omitempty"`
+	Namespace      string                `json:"namespace,omitempty"`
+	Pod            string                `json:"pod,omitempty"`
+	Services       []string              `json:"services,omitempty"`
+	ProviderID     string                `json:"providerID,omitempty"`
+	Labels         AllocationLabels      `json:"allocationLabels,omitempty"`
+	Annotations    AllocationAnnotations `json:"allocationAnnotations,omitempty"`
+}
+
+// AllocationLabels is a schema-free mapping of key/value pairs that can be
+// attributed to an Allocation
+type AllocationLabels map[string]string
+
+// AllocationAnnotations is a schema-free mapping of key/value pairs that can be
+// attributed to an Allocation
+type AllocationAnnotations map[string]string
+
+// TODO niko/etl make sure Services deep copy works correctly
+func (p *AllocationProperties) Clone() *AllocationProperties {
+	if p == nil {
+		return nil
+	}
+
+	clone := &AllocationProperties{}
+	clone.Cluster = p.Cluster
+	clone.Node = p.Node
+	clone.Container = p.Container
+	clone.Controller = p.Controller
+	clone.ControllerKind = p.ControllerKind
+	clone.Namespace = p.Namespace
+	clone.Pod = p.Pod
+	clone.ProviderID = p.ProviderID
+	clone.Services = p.Services
+	clone.Labels = p.Labels
+	clone.Annotations = p.Annotations
+
+	return clone
+}
+
+func (p *AllocationProperties) Equal(that *AllocationProperties) bool {
+	if p == nil || that == nil {
+		return false
+	}
+
+	if p.Cluster != that.Cluster {
+		return false
+	}
+
+	if p.Node != that.Node {
+		return false
+	}
+
+	if p.Container != that.Container {
+		return false
+	}
+
+	if p.Controller != that.Controller {
+		return false
+	}
+
+	if p.ControllerKind != that.ControllerKind {
+		return false
+	}
+
+	if p.Namespace != that.Namespace {
+		return false
+	}
+
+	if p.Pod != that.Pod {
+		return false
+	}
+
+	if p.ProviderID != that.ProviderID {
+		return false
+	}
+
+	pLabels := p.Labels
+	thatLabels := that.Labels
+	if len(pLabels) != len(thatLabels) {
+		for k, pv := range pLabels {
+			tv, ok := thatLabels[k]
+			if !ok || tv != pv {
+				return false
+			}
+		}
+		return false
+	}
+
+	pAnnotations := p.Annotations
+	thatAnnotations := that.Annotations
+	if len(pAnnotations) != len(thatAnnotations) {
+		for k, pv := range pAnnotations {
+			tv, ok := thatAnnotations[k]
+			if !ok || tv != pv {
+				return false
+			}
+		}
+		return false
+	}
+
+	pServices := p.Services
+	thatServices := that.Services
+	if len(pServices) != len(thatServices) {
+		sort.Strings(pServices)
+		sort.Strings(thatServices)
+		for i, pv := range pServices {
+			tv := thatServices[i]
+			if tv != pv {
+				return false
+			}
+		}
+		return false
+	}
+
+	return true
+}
+
+func (p *AllocationProperties) String() string {
+	if p == nil {
+		return "<nil>"
+	}
+
+	strs := []string{}
+
+	if p.Cluster != "" {
+		strs = append(strs, "Cluster:"+p.Cluster)
+	}
+
+	if p.Node != "" {
+		strs = append(strs, "Node:"+p.Node)
+	}
+
+	if p.Container != "" {
+		strs = append(strs, "Container:"+p.Container)
+	}
+
+	if p.Controller != "" {
+		strs = append(strs, "Controller:"+p.Controller)
+	}
+
+	if p.ControllerKind != "" {
+		strs = append(strs, "ControllerKind:"+p.ControllerKind)
+	}
+
+	if p.Namespace != "" {
+		strs = append(strs, "Namespace:"+p.Namespace)
+	}
+
+	if p.Pod != "" {
+		strs = append(strs, "Pod:"+p.Pod)
+	}
+
+	if p.ProviderID != "" {
+		strs = append(strs, "ProviderID:"+p.ProviderID)
+	}
+
+	if len(p.Services) > 0 {
+		strs = append(strs, "Services:"+strings.Join(p.Services, ";"))
+	}
+
+	var labelStrs []string
+	for k, prop := range p.Labels {
+		labelStrs = append(labelStrs, fmt.Sprintf("%s:%s", k, prop))
+	}
+	strs = append(strs, fmt.Sprintf("Labels:{%s}", strings.Join(strs, ",")))
+
+	var AnnotationStrs []string
+	for k, prop := range p.Annotations {
+		AnnotationStrs = append(AnnotationStrs, fmt.Sprintf("%s:%s", k, prop))
+	}
+	strs = append(strs, fmt.Sprintf("Annotations:{%s}", strings.Join(strs, ",")))
+
+	return strings.Join(strs, ",")
+}
+
+// AggregationStrings converts a AllocationProperties object into a slice of strings
+// representing a request to aggregate by certain properties.
+// NOTE: today, the ordering of the properties *has to match the ordering
+// of the allocaiton function generateKey*
+func (p *AllocationProperties) AggregationStrings() []string {
+	if p == nil {
+		return []string{}
+	}
+
+	aggStrs := []string{}
+	if p.Cluster != "" {
+		aggStrs = append(aggStrs, AllocationClusterProp.String())
+	}
+	if p.Node != "" {
+		aggStrs = append(aggStrs, AllocationNodeProp.String())
+	}
+	if p.Container != "" {
+		aggStrs = append(aggStrs, AllocationContainerProp.String())
+	}
+	if p.Controller != "" {
+		aggStrs = append(aggStrs, AllocationControllerProp.String())
+	}
+	if p.ControllerKind != "" {
+		aggStrs = append(aggStrs, AllocationControllerKindProp.String())
+	}
+	if p.Namespace != "" {
+		aggStrs = append(aggStrs, AllocationNamespaceProp.String())
+	}
+	if p.Pod != "" {
+		aggStrs = append(aggStrs, AllocationPodProp.String())
+	}
+	if p.ProviderID != "" {
+		aggStrs = append(aggStrs, AllocationProviderIDProp.String())
+	}
+	if len(p.Services) > 0 {
+		aggStrs = append(aggStrs, AllocationServiceProp.String())
+	}
+	if len(p.Services) > 0 {
+		aggStrs = append(aggStrs, AllocationServiceProp.String())
+	}
+	if len(p.Labels) > 0  {
+		// e.g. expect format map[string]string{
+		// 	 "env":""
+		// 	 "app":"",
+		// }
+		// for aggregating by "label:app,label:env"
+		labels := p.Labels
+		labelAggStrs := []string{}
+		for labelName := range labels {
+			labelAggStrs = append(labelAggStrs, fmt.Sprintf("label:%s", labelName))
+		}
+		if len(labelAggStrs) > 0 {
+			// Enforce alphabetical ordering, then append to aggStrs
+			sort.Strings(labelAggStrs)
+			for _, labelName := range labelAggStrs {
+				aggStrs = append(aggStrs, labelName)
+			}
+		}
+	}
+
+
+	return aggStrs
+}
+
+func (p *AllocationProperties) isEmpty() bool {
+	if p == nil {
+		return true
+	}
+	return p.Equal(&AllocationProperties{})
+}

+ 13 - 13
pkg/kubecost/properties_test.go → pkg/kubecost/allocationprops_test.go

@@ -9,39 +9,39 @@ import "testing"
 // func TestProperty_String(t *testing.T) {}
 
 func TestProperties_AggregationString(t *testing.T) {
-	var props *Properties
+	var props *AllocationProperties
 	var aggStrs []string
 
-	// nil Properties should produce and empty slice
+	// nil AllocationProperties should produce and empty slice
 	aggStrs = props.AggregationStrings()
 	if aggStrs == nil || len(aggStrs) > 0 {
 		t.Fatalf("expected empty slice; got %v", aggStrs)
 	}
 
-	// empty Properties should product an empty slice
-	props = &Properties{}
+	// empty AllocationProperties should product an empty slice
+	props = &AllocationProperties{}
 	aggStrs = props.AggregationStrings()
 	if aggStrs == nil || len(aggStrs) > 0 {
 		t.Fatalf("expected empty slice; got %v", aggStrs)
 	}
 
-	// Properties with single, simple property set
-	props = &Properties{}
-	props.SetNamespace("")
+	// AllocationProperties with single, simple property set
+	props = &AllocationProperties{}
+	props.Namespace = "true"
 	aggStrs = props.AggregationStrings()
 	if len(aggStrs) != 1 || aggStrs[0] != "namespace" {
 		t.Fatalf("expected [\"namespace\"]; got %v", aggStrs)
 	}
 
-	// Properties with mutiple properties, including labels
+	// AllocationProperties with mutiple properties, including labels
 	// Note: order matters!
-	props = &Properties{}
-	props.SetNamespace("")
-	props.SetLabels(map[string]string{
+	props = &AllocationProperties{}
+	props.Namespace = "true"
+	props.Labels = map[string]string{
 		"env": "",
 		"app": "",
-	})
-	props.SetCluster("")
+	}
+	props.Cluster = "true"
 	aggStrs = props.AggregationStrings()
 	if len(aggStrs) != 4 {
 		t.Fatalf("expected length %d; got lenfth %d", 4, len(aggStrs))

+ 22 - 21
pkg/kubecost/asset.go

@@ -137,7 +137,7 @@ func AssetToExternalAllocation(asset Asset, aggregateBy []string, externalLabels
 	match := false
 
 	// props records the relevant Properties to set on the resultant Allocation
-	props := Properties{}
+	props := AllocationProperties{}
 
 	for _, aggBy := range aggregateBy {
 		// labelName should be derived from the mapping of properties to
@@ -171,35 +171,36 @@ func AssetToExternalAllocation(asset Asset, aggregateBy []string, externalLabels
 				match = true
 
 				// Set the corresponding label in props
-				labels, err := props.GetLabels()
-				if err != nil {
+				labels := props.Labels
+				if labels == nil {
 					labels = map[string]string{}
 				}
+
 				labels[labelName] = value
-				props.SetLabels(labels)
+				props.Labels = labels
 			} else {
 				names = append(names, value)
 				match = true
 
 				// Set the corresponding property on props
 				switch aggBy {
-				case ClusterProp.String():
-					props.SetCluster(value)
-				case NodeProp.String():
-					props.SetNode(value)
-				case NamespaceProp.String():
-					props.SetNamespace(value)
-				case ControllerKindProp.String():
-					props.SetControllerKind(value)
-				case ControllerProp.String():
-					props.SetController(value)
-				case PodProp.String():
-					props.SetPod(value)
-				case ContainerProp.String():
-					props.SetContainer(value)
-				case ServiceProp.String():
+				case AllocationClusterProp.String():
+					props.Cluster = value
+				case AllocationNodeProp.String():
+					props.Node = value
+				case AllocationNamespaceProp.String():
+					props.Namespace = value
+				case AllocationControllerKindProp.String():
+					props.ControllerKind = value
+				case AllocationControllerProp.String():
+					props.Controller = value
+				case AllocationPodProp.String():
+					props.Pod = value
+				case AllocationContainerProp.String():
+					props.Container = value
+				case AllocationServiceProp.String():
 					// TODO: external allocation: how to do this? multi-service?
-					props.SetServices([]string{value})
+					props.Services = []string{value}
 				}
 			}
 		} else {
@@ -221,7 +222,7 @@ func AssetToExternalAllocation(asset Asset, aggregateBy []string, externalLabels
 	// TODO: external allocation: resource totals?
 	return &Allocation{
 		Name:         strings.Join(names, "/"),
-		Properties:   props,
+		Properties:   &props,
 		Window:       asset.Window().Clone(),
 		Start:        asset.Start(),
 		End:          asset.End(),

+ 8 - 8
pkg/kubecost/asset_test.go

@@ -1089,8 +1089,8 @@ func TestAssetToExternalAllocation(t *testing.T) {
 	if alloc.Name != "monitoring/__external__" {
 		t.Fatalf("expected external allocation with name '%s'; got '%s'", "monitoring/__external__", alloc.Name)
 	}
-	if ns, err := alloc.Properties.GetNamespace(); err != nil || ns != "monitoring" {
-		t.Fatalf("expected external allocation with Properties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
+	if ns := alloc.Properties.Namespace; ns != "monitoring" {
+		t.Fatalf("expected external allocation with AllocationProperties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
 	}
 	if alloc.ExternalCost != 10.00 {
 		t.Fatalf("expected external allocation with ExternalCost %f; got %f", 10.00, alloc.ExternalCost)
@@ -1107,11 +1107,11 @@ func TestAssetToExternalAllocation(t *testing.T) {
 	if alloc.Name != "monitoring/env=prod/__external__" {
 		t.Fatalf("expected external allocation with name '%s'; got '%s'", "monitoring/env=prod/__external__", alloc.Name)
 	}
-	if ns, err := alloc.Properties.GetNamespace(); err != nil || ns != "monitoring" {
-		t.Fatalf("expected external allocation with Properties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
+	if ns := alloc.Properties.Namespace; ns != "monitoring" {
+		t.Fatalf("expected external allocation with AllocationProperties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
 	}
-	if ls, err := alloc.Properties.GetLabels(); err != nil || ls["env"] != "prod" {
-		t.Fatalf("expected external allocation with Properties.Labels[\"env\"] '%s'; got '%s' (%s)", "prod", ls["env"], err)
+	if ls := alloc.Properties.Labels; len(ls) == 0 || ls["env"] != "prod" {
+		t.Fatalf("expected external allocation with AllocationProperties.Labels[\"env\"] '%s'; got '%s' (%s)", "prod", ls["env"], err)
 	}
 	if alloc.ExternalCost != 10.00 {
 		t.Fatalf("expected external allocation with ExternalCost %f; got %f", 10.00, alloc.ExternalCost)
@@ -1128,8 +1128,8 @@ func TestAssetToExternalAllocation(t *testing.T) {
 	if alloc.Name != "monitoring/__unallocated__/__external__" {
 		t.Fatalf("expected external allocation with name '%s'; got '%s'", "monitoring/__unallocated__/__external__", alloc.Name)
 	}
-	if ns, err := alloc.Properties.GetNamespace(); err != nil || ns != "monitoring" {
-		t.Fatalf("expected external allocation with Properties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
+	if ns := alloc.Properties.Namespace; ns != "monitoring" {
+		t.Fatalf("expected external allocation with AllocationProperties.Namespace '%s'; got '%s' (%s)", "monitoring", ns, err)
 	}
 	if alloc.ExternalCost != 10.00 {
 		t.Fatalf("expected external allocation with ExternalCost %f; got %f", 10.00, alloc.ExternalCost)

+ 5 - 1
pkg/kubecost/bingen.go

@@ -20,5 +20,9 @@ package kubecost
 // @bingen:generate:Allocation
 // @bingen:generate:AllocationSet
 // @bingen:generate:AllocationSetRange
+// @bingen:generate:AllocationProperties
+// @bingen:generate:AllocationProperty
+// @bingen:generate:AllocationLabels
+// @bingen:generate:AllocationAnnotations
 
-//go:generate bingen -package=kubecost -version=9 -buffer=github.com/kubecost/cost-model/pkg/util
+//go:generate bingen -package=kubecost -version=10 -buffer=github.com/kubecost/cost-model/pkg/util

+ 254 - 38
pkg/kubecost/kubecost_codecs.go

@@ -25,7 +25,7 @@ const (
 	GeneratorPackageName string = "kubecost"
 
 	// CodecVersion is the version passed into the generator
-	CodecVersion uint8 = 9
+	CodecVersion uint8 = 10
 )
 
 //--------------------------------------------------------------------------
@@ -35,22 +35,23 @@ const (
 // Generated type map for resolving interface implementations to
 // to concrete types
 var typeMap map[string]reflect.Type = map[string]reflect.Type{
-	"Allocation":         reflect.TypeOf((*Allocation)(nil)).Elem(),
-	"AllocationSet":      reflect.TypeOf((*AllocationSet)(nil)).Elem(),
-	"AllocationSetRange": reflect.TypeOf((*AllocationSetRange)(nil)).Elem(),
-	"Any":                reflect.TypeOf((*Any)(nil)).Elem(),
-	"AssetProperties":    reflect.TypeOf((*AssetProperties)(nil)).Elem(),
-	"AssetSet":           reflect.TypeOf((*AssetSet)(nil)).Elem(),
-	"AssetSetRange":      reflect.TypeOf((*AssetSetRange)(nil)).Elem(),
-	"Breakdown":          reflect.TypeOf((*Breakdown)(nil)).Elem(),
-	"Cloud":              reflect.TypeOf((*Cloud)(nil)).Elem(),
-	"ClusterManagement":  reflect.TypeOf((*ClusterManagement)(nil)).Elem(),
-	"Disk":               reflect.TypeOf((*Disk)(nil)).Elem(),
-	"LoadBalancer":       reflect.TypeOf((*LoadBalancer)(nil)).Elem(),
-	"Network":            reflect.TypeOf((*Network)(nil)).Elem(),
-	"Node":               reflect.TypeOf((*Node)(nil)).Elem(),
-	"SharedAsset":        reflect.TypeOf((*SharedAsset)(nil)).Elem(),
-	"Window":             reflect.TypeOf((*Window)(nil)).Elem(),
+	"Allocation":           reflect.TypeOf((*Allocation)(nil)).Elem(),
+	"AllocationProperties": reflect.TypeOf((*AllocationProperties)(nil)).Elem(),
+	"AllocationSet":        reflect.TypeOf((*AllocationSet)(nil)).Elem(),
+	"AllocationSetRange":   reflect.TypeOf((*AllocationSetRange)(nil)).Elem(),
+	"Any":                  reflect.TypeOf((*Any)(nil)).Elem(),
+	"AssetProperties":      reflect.TypeOf((*AssetProperties)(nil)).Elem(),
+	"AssetSet":             reflect.TypeOf((*AssetSet)(nil)).Elem(),
+	"AssetSetRange":        reflect.TypeOf((*AssetSetRange)(nil)).Elem(),
+	"Breakdown":            reflect.TypeOf((*Breakdown)(nil)).Elem(),
+	"Cloud":                reflect.TypeOf((*Cloud)(nil)).Elem(),
+	"ClusterManagement":    reflect.TypeOf((*ClusterManagement)(nil)).Elem(),
+	"Disk":                 reflect.TypeOf((*Disk)(nil)).Elem(),
+	"LoadBalancer":         reflect.TypeOf((*LoadBalancer)(nil)).Elem(),
+	"Network":              reflect.TypeOf((*Network)(nil)).Elem(),
+	"Node":                 reflect.TypeOf((*Node)(nil)).Elem(),
+	"SharedAsset":          reflect.TypeOf((*SharedAsset)(nil)).Elem(),
+	"Window":               reflect.TypeOf((*Window)(nil)).Elem(),
 }
 
 //--------------------------------------------------------------------------
@@ -116,15 +117,21 @@ func (target *Allocation) MarshalBinary() (data []byte, err error) {
 	buff.WriteUInt8(CodecVersion) // version
 
 	buff.WriteString(target.Name) // write string
-	// --- [begin][write][reference](Properties) ---
-	a, errA := target.Properties.MarshalBinary()
-	if errA != nil {
-		return nil, errA
-	}
-	buff.WriteInt(len(a))
-	buff.WriteBytes(a)
-	// --- [end][write][reference](Properties) ---
+	if target.Properties == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][struct](AllocationProperties) ---
+		a, errA := target.Properties.MarshalBinary()
+		if errA != nil {
+			return nil, errA
+		}
+		buff.WriteInt(len(a))
+		buff.WriteBytes(a)
+		// --- [end][write][struct](AllocationProperties) ---
 
+	}
 	// --- [begin][write][struct](Window) ---
 	b, errB := target.Window.MarshalBinary()
 	if errB != nil {
@@ -198,17 +205,21 @@ func (target *Allocation) UnmarshalBinary(data []byte) (err error) {
 	a := buff.ReadString() // read string
 	target.Name = a
 
-	// --- [begin][read][reference](Properties) ---
-	b := &Properties{}
-	c := buff.ReadInt()    // byte array length
-	d := buff.ReadBytes(c) // byte array
-	errA := b.UnmarshalBinary(d)
-	if errA != nil {
-		return errA
-	}
-	target.Properties = *b
-	// --- [end][read][reference](Properties) ---
+	if buff.ReadUInt8() == uint8(0) {
+		target.Properties = nil
+	} else {
+		// --- [begin][read][struct](AllocationProperties) ---
+		b := &AllocationProperties{}
+		c := buff.ReadInt()    // byte array length
+		d := buff.ReadBytes(c) // byte array
+		errA := b.UnmarshalBinary(d)
+		if errA != nil {
+			return errA
+		}
+		target.Properties = b
+		// --- [end][read][struct](AllocationProperties) ---
 
+	}
 	// --- [begin][read][struct](Window) ---
 	e := &Window{}
 	f := buff.ReadInt()    // byte array length
@@ -293,6 +304,207 @@ func (target *Allocation) UnmarshalBinary(data []byte) (err error) {
 	return nil
 }
 
+//--------------------------------------------------------------------------
+//  AllocationProperties
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this AllocationProperties instance
+// into a byte array
+func (target *AllocationProperties) MarshalBinary() (data []byte, err error) {
+	// panics are recovered and propagated as errors
+	defer func() {
+		if r := recover(); r != nil {
+			if e, ok := r.(error); ok {
+				err = e
+			} else if s, ok := r.(string); ok {
+				err = fmt.Errorf("Unexpected panic: %s", s)
+			} else {
+				err = fmt.Errorf("Unexpected panic: %+v", r)
+			}
+		}
+	}()
+
+	buff := util.NewBuffer()
+	buff.WriteUInt8(CodecVersion) // version
+
+	buff.WriteString(target.Cluster)        // write string
+	buff.WriteString(target.Node)           // write string
+	buff.WriteString(target.Container)      // write string
+	buff.WriteString(target.Controller)     // write string
+	buff.WriteString(target.ControllerKind) // write string
+	buff.WriteString(target.Namespace)      // write string
+	buff.WriteString(target.Pod)            // write string
+	if target.Services == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][slice]([]string) ---
+		buff.WriteInt(len(target.Services)) // array length
+		for i := 0; i < len(target.Services); i++ {
+			buff.WriteString(target.Services[i]) // write string
+		}
+		// --- [end][write][slice]([]string) ---
+
+	}
+	buff.WriteString(target.ProviderID) // write string
+	// --- [begin][write][alias](AllocationLabels) ---
+	if map[string]string(target.Labels) == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][map](map[string]string) ---
+		buff.WriteInt(len(map[string]string(target.Labels))) // map length
+		for v, z := range map[string]string(target.Labels) {
+			buff.WriteString(v) // write string
+			buff.WriteString(z) // write string
+		}
+		// --- [end][write][map](map[string]string) ---
+
+	}
+	// --- [end][write][alias](AllocationLabels) ---
+
+	// --- [begin][write][alias](AllocationAnnotations) ---
+	if map[string]string(target.Annotations) == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][map](map[string]string) ---
+		buff.WriteInt(len(map[string]string(target.Annotations))) // map length
+		for vv, zz := range map[string]string(target.Annotations) {
+			buff.WriteString(vv) // write string
+			buff.WriteString(zz) // write string
+		}
+		// --- [end][write][map](map[string]string) ---
+
+	}
+	// --- [end][write][alias](AllocationAnnotations) ---
+
+	return buff.Bytes(), nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the AllocationProperties type
+func (target *AllocationProperties) UnmarshalBinary(data []byte) (err error) {
+	// panics are recovered and propagated as errors
+	defer func() {
+		if r := recover(); r != nil {
+			if e, ok := r.(error); ok {
+				err = e
+			} else if s, ok := r.(string); ok {
+				err = fmt.Errorf("Unexpected panic: %s", s)
+			} else {
+				err = fmt.Errorf("Unexpected panic: %+v", r)
+			}
+		}
+	}()
+
+	buff := util.NewBufferFromBytes(data)
+
+	// Codec Version Check
+	version := buff.ReadUInt8()
+	if version != CodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling AllocationProperties. Expected %d, got %d", CodecVersion, version)
+	}
+
+	a := buff.ReadString() // read string
+	target.Cluster = a
+
+	b := buff.ReadString() // read string
+	target.Node = b
+
+	c := buff.ReadString() // read string
+	target.Container = c
+
+	d := buff.ReadString() // read string
+	target.Controller = d
+
+	e := buff.ReadString() // read string
+	target.ControllerKind = e
+
+	f := buff.ReadString() // read string
+	target.Namespace = f
+
+	g := buff.ReadString() // read string
+	target.Pod = g
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.Services = nil
+	} else {
+		// --- [begin][read][slice]([]string) ---
+		k := buff.ReadInt() // array len
+		h := make([]string, k)
+		for i := 0; i < k; i++ {
+			var l string
+			m := buff.ReadString() // read string
+			l = m
+
+			h[i] = l
+		}
+		target.Services = h
+		// --- [end][read][slice]([]string) ---
+
+	}
+	n := buff.ReadString() // read string
+	target.ProviderID = n
+
+	// --- [begin][read][alias](AllocationLabels) ---
+	var o map[string]string
+	if buff.ReadUInt8() == uint8(0) {
+		o = nil
+	} else {
+		// --- [begin][read][map](map[string]string) ---
+		q := buff.ReadInt() // map len
+		p := make(map[string]string, q)
+		for j := 0; j < q; j++ {
+			var v string
+			r := buff.ReadString() // read string
+			v = r
+
+			var z string
+			s := buff.ReadString() // read string
+			z = s
+
+			p[v] = z
+		}
+		o = p
+		// --- [end][read][map](map[string]string) ---
+
+	}
+	target.Labels = AllocationLabels(o)
+	// --- [end][read][alias](AllocationLabels) ---
+
+	// --- [begin][read][alias](AllocationAnnotations) ---
+	var t map[string]string
+	if buff.ReadUInt8() == uint8(0) {
+		t = nil
+	} else {
+		// --- [begin][read][map](map[string]string) ---
+		w := buff.ReadInt() // map len
+		u := make(map[string]string, w)
+		for ii := 0; ii < w; ii++ {
+			var vv string
+			x := buff.ReadString() // read string
+			vv = x
+
+			var zz string
+			y := buff.ReadString() // read string
+			zz = y
+
+			u[vv] = zz
+		}
+		t = u
+		// --- [end][read][map](map[string]string) ---
+
+	}
+	target.Annotations = AllocationAnnotations(t)
+	// --- [end][read][alias](AllocationAnnotations) ---
+
+	return nil
+}
+
 //--------------------------------------------------------------------------
 //  AllocationSet
 //--------------------------------------------------------------------------
@@ -2447,6 +2659,7 @@ func (target *Node) MarshalBinary() (data []byte, err error) {
 	}
 	buff.WriteFloat64(target.CPUCost)     // write float64
 	buff.WriteFloat64(target.GPUCost)     // write float64
+	buff.WriteFloat64(target.GPUCount)    // write float64
 	buff.WriteFloat64(target.RAMCost)     // write float64
 	buff.WriteFloat64(target.Discount)    // write float64
 	buff.WriteFloat64(target.Preemptible) // write float64
@@ -2600,13 +2813,16 @@ func (target *Node) UnmarshalBinary(data []byte) (err error) {
 	target.GPUCost = gg
 
 	hh := buff.ReadFloat64() // read float64
-	target.RAMCost = hh
+	target.GPUCount = hh
 
 	kk := buff.ReadFloat64() // read float64
-	target.Discount = kk
+	target.RAMCost = kk
 
 	ll := buff.ReadFloat64() // read float64
-	target.Preemptible = ll
+	target.Discount = ll
+
+	mm := buff.ReadFloat64() // read float64
+	target.Preemptible = mm
 
 	return nil
 }

+ 31 - 31
pkg/kubecost/kubecost_codecs_test.go

@@ -450,76 +450,76 @@ func TestNode_BinaryEncoding(t *testing.T) {
 }
 
 func TestProperties_BinaryEncoding(t *testing.T) {
-	var p0, p1 *Properties
+	var p0, p1 *AllocationProperties
 	var bs []byte
 	var err error
 
 	// empty properties
-	p0 = &Properties{}
+	p0 = &AllocationProperties{}
 	bs, err = p0.MarshalBinary()
 	if err != nil {
-		t.Fatalf("Properties.Binary: unexpected error: %s", err)
+		t.Fatalf("AllocationProperties.Binary: unexpected error: %s", err)
 	}
 
-	p1 = &Properties{}
+	p1 = &AllocationProperties{}
 	err = p1.UnmarshalBinary(bs)
 	if err != nil {
-		t.Fatalf("Properties.Binary: unexpected error: %s", err)
+		t.Fatalf("AllocationProperties.Binary: unexpected error: %s", err)
 	}
 
 	if !p0.Equal(p1) {
-		t.Fatalf("Properties.Binary: expected %s; found %s", p0, p1)
+		t.Fatalf("AllocationProperties.Binary: expected %s; found %s", p0, p1)
 	}
 
 	// complete properties
-	p0 = &Properties{}
-	p0.SetCluster("cluster1")
-	p0.SetContainer("container-abc-1")
-	p0.SetController("daemonset-abc")
-	p0.SetControllerKind("daemonset")
-	p0.SetNamespace("namespace1")
-	p0.SetNode("node1")
-	p0.SetPod("daemonset-abc-123")
-	p0.SetLabels(map[string]string{
+	p0 = &AllocationProperties{}
+	p0.Cluster = "cluster1"
+	p0.Container = "container-abc-1"
+	p0.Controller = "daemonset-abc"
+	p0.ControllerKind = "daemonset"
+	p0.Namespace = "namespace1"
+	p0.Node = "node1"
+	p0.Pod = "daemonset-abc-123"
+	p0.Labels = map[string]string{
 		"app":  "cost-analyzer",
 		"tier": "frontend",
-	})
-	p0.SetServices([]string{"kubecost-frontend"})
+	}
+	p0.Services = []string{"kubecost-frontend"}
 	bs, err = p0.MarshalBinary()
 	if err != nil {
-		t.Fatalf("Properties.Binary: unexpected error: %s", err)
+		t.Fatalf("AllocationProperties.Binary: unexpected error: %s", err)
 	}
 
-	p1 = &Properties{}
+	p1 = &AllocationProperties{}
 	err = p1.UnmarshalBinary(bs)
 	if err != nil {
-		t.Fatalf("Properties.Binary: unexpected error: %s", err)
+		t.Fatalf("AllocationProperties.Binary: unexpected error: %s", err)
 	}
 
 	if !p0.Equal(p1) {
-		t.Fatalf("Properties.Binary: expected %s; found %s", p0, p1)
+		t.Fatalf("AllocationProperties.Binary: expected %s; found %s", p0, p1)
 	}
 
 	// incomplete properties
-	p0 = &Properties{}
-	p0.SetCluster("cluster1")
-	p0.SetController("daemonset-abc")
-	p0.SetControllerKind("daemonset")
-	p0.SetNamespace("namespace1")
-	p0.SetServices([]string{})
+	p0 = &AllocationProperties{}
+	p0.Cluster = ("cluster1")
+	p0.Controller = "daemonset-abc"
+	p0.ControllerKind = "daemonset"
+	p0.Namespace = "namespace1"
+	p0.Services = []string{}
 	bs, err = p0.MarshalBinary()
 	if err != nil {
-		t.Fatalf("Properties.Binary: unexpected error: %s", err)
+		t.Fatalf("AllocationProperties.Binary: unexpected error: %s", err)
 	}
 
-	p1 = &Properties{}
+	p1 = &AllocationProperties{}
 	err = p1.UnmarshalBinary(bs)
 	if err != nil {
-		t.Fatalf("Properties.Binary: unexpected error: %s", err)
+		t.Fatalf("AllocationProperties.Binary: unexpected error: %s", err)
 	}
 
 	if !p0.Equal(p1) {
-		t.Fatalf("Properties.Binary: expected %s; found %s", p0, p1)
+		t.Fatalf("AllocationProperties.Binary: expected %s; found %s", p0, p1)
 	}
 }
 

+ 0 - 698
pkg/kubecost/properties.go

@@ -1,698 +0,0 @@
-package kubecost
-
-import (
-	"fmt"
-	"sort"
-	"strings"
-
-	"github.com/kubecost/cost-model/pkg/util"
-)
-
-type Property string
-
-const (
-	NilProp            Property = ""
-	ClusterProp        Property = "cluster"
-	NodeProp           Property = "node"
-	ContainerProp      Property = "container"
-	ControllerProp     Property = "controller"
-	ControllerKindProp Property = "controllerKind"
-	LabelProp          Property = "label"
-	AnnotationProp     Property = "annotation"
-	NamespaceProp      Property = "namespace"
-	PodProp            Property = "pod"
-	ServiceProp        Property = "service"
-)
-
-var availableProperties []Property = []Property{
-	NilProp,
-	ClusterProp,
-	NodeProp,
-	ContainerProp,
-	ControllerProp,
-	ControllerKindProp,
-	LabelProp,
-	AnnotationProp,
-	NamespaceProp,
-	PodProp,
-	ServiceProp,
-}
-
-func ParseProperty(prop string) Property {
-	for _, property := range availableProperties {
-		if strings.ToLower(string(property)) == strings.ToLower(prop) {
-			return property
-		}
-	}
-	return NilProp
-}
-
-func (p Property) String() string {
-	return string(p)
-}
-
-type PropertyValue struct {
-	Property Property
-	Value    interface{}
-}
-
-// Properties describes a set of Kubernetes objects.
-// TODO:CLEANUP make this a struct smdh
-type Properties map[Property]interface{}
-
-// TODO niko/etl make sure Services deep copy works correctly
-func (p *Properties) Clone() Properties {
-	if p == nil {
-		return nil
-	}
-
-	clone := make(Properties, len(*p))
-	for k, v := range *p {
-		clone[k] = v
-	}
-	return clone
-}
-
-func (p *Properties) Equal(that *Properties) bool {
-	if p == nil || that == nil {
-		return false
-	}
-
-	if p.Length() != that.Length() {
-		return false
-	}
-
-	pCluster, _ := p.GetCluster()
-	thatCluster, _ := that.GetCluster()
-	if pCluster != thatCluster {
-		return false
-	}
-
-	pNode, _ := p.GetNode()
-	thatNode, _ := that.GetNode()
-	if pNode != thatNode {
-		return false
-	}
-
-	pContainer, _ := p.GetContainer()
-	thatContainer, _ := that.GetContainer()
-	if pContainer != thatContainer {
-		return false
-	}
-
-	pController, _ := p.GetController()
-	thatController, _ := that.GetController()
-	if pController != thatController {
-		return false
-	}
-
-	pControllerKind, _ := p.GetControllerKind()
-	thatControllerKind, _ := that.GetControllerKind()
-	if pControllerKind != thatControllerKind {
-		return false
-	}
-
-	pNamespace, _ := p.GetNamespace()
-	thatNamespace, _ := that.GetNamespace()
-	if pNamespace != thatNamespace {
-		return false
-	}
-
-	pPod, _ := p.GetPod()
-	thatPod, _ := that.GetPod()
-	if pPod != thatPod {
-		return false
-	}
-
-	pLabels, _ := p.GetLabels()
-	thatLabels, _ := that.GetLabels()
-	if len(pLabels) != len(thatLabels) {
-		for k, pv := range pLabels {
-			tv, ok := thatLabels[k]
-			if !ok || tv != pv {
-				return false
-			}
-		}
-		return false
-	}
-
-	pAnnotations, _ := p.GetAnnotations()
-	thatAnnotations, _ := that.GetAnnotations()
-	if len(pAnnotations) != len(thatAnnotations) {
-		for k, pv := range pAnnotations {
-			tv, ok := thatAnnotations[k]
-			if !ok || tv != pv {
-				return false
-			}
-		}
-		return false
-	}
-
-	pServices, _ := p.GetServices()
-	thatServices, _ := that.GetServices()
-	if len(pServices) != len(thatServices) {
-		sort.Strings(pServices)
-		sort.Strings(thatServices)
-		for i, pv := range pServices {
-			tv := thatServices[i]
-			if tv != pv {
-				return false
-			}
-		}
-		return false
-	}
-
-	return true
-}
-
-func (p *Properties) Intersection(that Properties) Properties {
-	spec := &Properties{}
-
-	sCluster, sErr := p.GetCluster()
-	tCluster, tErr := that.GetCluster()
-	if sErr == nil && tErr == nil && sCluster == tCluster {
-		spec.SetCluster(sCluster)
-	}
-
-	sNode, sErr := p.GetNode()
-	tNode, tErr := that.GetNode()
-	if sErr == nil && tErr == nil && sNode == tNode {
-		spec.SetNode(sNode)
-	}
-
-	sContainer, sErr := p.GetContainer()
-	tContainer, tErr := that.GetContainer()
-	if sErr == nil && tErr == nil && sContainer == tContainer {
-		spec.SetContainer(sContainer)
-	}
-
-	sController, sErr := p.GetController()
-	tController, tErr := that.GetController()
-	if sErr == nil && tErr == nil && sController == tController {
-		spec.SetController(sController)
-	}
-
-	sControllerKind, sErr := p.GetControllerKind()
-	tControllerKind, tErr := that.GetControllerKind()
-	if sErr == nil && tErr == nil && sControllerKind == tControllerKind {
-		spec.SetControllerKind(sControllerKind)
-	}
-
-	sNamespace, sErr := p.GetNamespace()
-	tNamespace, tErr := that.GetNamespace()
-	if sErr == nil && tErr == nil && sNamespace == tNamespace {
-		spec.SetNamespace(sNamespace)
-	}
-
-	sPod, sErr := p.GetPod()
-	tPod, tErr := that.GetPod()
-	if sErr == nil && tErr == nil && sPod == tPod {
-		spec.SetPod(sPod)
-	}
-
-	// TODO niko/etl intersection of services and labels and annotations
-
-	return *spec
-}
-
-// Length returns the number of Properties
-func (p *Properties) Length() int {
-	if p == nil {
-		return 0
-	}
-	return len(*p)
-}
-
-func (p *Properties) String() string {
-	if p == nil {
-		return "<nil>"
-	}
-
-	strs := []string{}
-	for key, prop := range *p {
-		strs = append(strs, fmt.Sprintf("%s:%s", key, prop))
-	}
-	return fmt.Sprintf("{%s}", strings.Join(strs, "; "))
-}
-
-// AggregationStrings converts a Properties object into a slice of strings
-// representing a request to aggregate by certain properties.
-// NOTE: today, the ordering of the properties *has to match the ordering
-// of the allocaiton function generateKey*
-func (p *Properties) AggregationStrings() []string {
-	if p == nil {
-		return []string{}
-	}
-
-	aggStrs := []string{}
-	if p.HasCluster() {
-		aggStrs = append(aggStrs, ClusterProp.String())
-	}
-	if p.HasNode() {
-		aggStrs = append(aggStrs, NodeProp.String())
-	}
-	if p.HasNamespace() {
-		aggStrs = append(aggStrs, NamespaceProp.String())
-	}
-	if p.HasControllerKind() {
-		aggStrs = append(aggStrs, ControllerKindProp.String())
-	}
-	if p.HasController() {
-		aggStrs = append(aggStrs, ControllerProp.String())
-	}
-	if p.HasPod() {
-		aggStrs = append(aggStrs, PodProp.String())
-	}
-	if p.HasContainer() {
-		aggStrs = append(aggStrs, ContainerProp.String())
-	}
-	if p.HasService() {
-		aggStrs = append(aggStrs, ServiceProp.String())
-	}
-	if p.HasLabel() {
-		// e.g. expect format map[string]string{
-		// 	 "env":""
-		// 	 "app":"",
-		// }
-		// for aggregating by "label:app,label:env"
-		labels, _ := p.GetLabels()
-		labelAggStrs := []string{}
-		for labelName := range labels {
-			labelAggStrs = append(labelAggStrs, fmt.Sprintf("label:%s", labelName))
-		}
-		if len(labelAggStrs) > 0 {
-			// Enforce alphabetical ordering, then append to aggStrs
-			sort.Strings(labelAggStrs)
-			for _, labelName := range labelAggStrs {
-				aggStrs = append(aggStrs, labelName)
-			}
-		}
-	}
-	return aggStrs
-}
-
-func (p *Properties) Get(prop Property) (string, error) {
-	if raw, ok := (*p)[prop]; ok {
-		if result, ok := raw.(string); ok {
-			return result, nil
-		}
-		return "", fmt.Errorf("%s is not a string", prop)
-	}
-	return "", fmt.Errorf("%s not set", prop)
-}
-
-func (p *Properties) Has(prop Property) bool {
-	_, ok := (*p)[prop]
-	return ok
-}
-
-func (p *Properties) Set(prop Property, value string) {
-	(*p)[prop] = value
-}
-
-func (p *Properties) GetCluster() (string, error) {
-	if raw, ok := (*p)[ClusterProp]; ok {
-		if cluster, ok := raw.(string); ok {
-			return cluster, nil
-		}
-		return "", fmt.Errorf("ClusterProp is not a string")
-	}
-	return "", fmt.Errorf("ClusterProp not set")
-}
-
-func (p *Properties) HasCluster() bool {
-	_, ok := (*p)[ClusterProp]
-	return ok
-}
-
-func (p *Properties) SetCluster(cluster string) {
-	(*p)[ClusterProp] = cluster
-}
-
-func (p *Properties) GetNode() (string, error) {
-	if raw, ok := (*p)[NodeProp]; ok {
-		if node, ok := raw.(string); ok {
-			return node, nil
-		}
-		return "", fmt.Errorf("NodeProp is not a string")
-	}
-	return "", fmt.Errorf("NodeProp not set")
-}
-
-func (p *Properties) HasNode() bool {
-	_, ok := (*p)[NodeProp]
-	return ok
-}
-
-func (p *Properties) SetNode(node string) {
-	(*p)[NodeProp] = node
-}
-
-func (p *Properties) GetContainer() (string, error) {
-	if raw, ok := (*p)[ContainerProp]; ok {
-		if container, ok := raw.(string); ok {
-			return container, nil
-		}
-		return "", fmt.Errorf("ContainerProp is not a string")
-	}
-	return "", fmt.Errorf("ContainerProp not set")
-}
-
-func (p *Properties) HasContainer() bool {
-	_, ok := (*p)[ContainerProp]
-	return ok
-}
-
-func (p *Properties) SetContainer(container string) {
-	(*p)[ContainerProp] = container
-}
-
-func (p *Properties) GetController() (string, error) {
-	if raw, ok := (*p)[ControllerProp]; ok {
-		if controller, ok := raw.(string); ok {
-			return controller, nil
-		}
-		return "", fmt.Errorf("ControllerProp is not a string")
-	}
-	return "", fmt.Errorf("ControllerProp not set")
-}
-
-func (p *Properties) HasController() bool {
-	_, ok := (*p)[ControllerProp]
-	return ok
-}
-
-func (p *Properties) SetController(controller string) {
-	(*p)[ControllerProp] = controller
-}
-
-func (p *Properties) GetControllerKind() (string, error) {
-	if raw, ok := (*p)[ControllerKindProp]; ok {
-		if controllerKind, ok := raw.(string); ok {
-			return controllerKind, nil
-		}
-		return "", fmt.Errorf("ControllerKindProp is not a string")
-	}
-	return "", fmt.Errorf("ControllerKindProp not set")
-}
-
-func (p *Properties) HasControllerKind() bool {
-	_, ok := (*p)[ControllerKindProp]
-	return ok
-}
-
-func (p *Properties) SetControllerKind(controllerKind string) {
-	(*p)[ControllerKindProp] = controllerKind
-}
-
-func (p *Properties) GetLabels() (map[string]string, error) {
-	if raw, ok := (*p)[LabelProp]; ok {
-		if labels, ok := raw.(map[string]string); ok {
-			return labels, nil
-		}
-		return map[string]string{}, fmt.Errorf("LabelProp is not a map[string]string")
-	}
-	return map[string]string{}, fmt.Errorf("LabelProp not set")
-}
-
-func (p *Properties) HasLabel() bool {
-	_, ok := (*p)[LabelProp]
-	return ok
-}
-
-func (p *Properties) SetLabels(labels map[string]string) {
-	(*p)[LabelProp] = labels
-}
-
-func (p *Properties) GetAnnotations() (map[string]string, error) {
-	if raw, ok := (*p)[AnnotationProp]; ok {
-		if annotations, ok := raw.(map[string]string); ok {
-			return annotations, nil
-		}
-		return map[string]string{}, fmt.Errorf("AnnotationProp is not a map[string]string")
-	}
-	return map[string]string{}, fmt.Errorf("AnnotationProp not set")
-}
-
-func (p *Properties) HasAnnotations() bool {
-	_, ok := (*p)[AnnotationProp]
-	return ok
-}
-
-func (p *Properties) SetAnnotations(annotations map[string]string) {
-	(*p)[AnnotationProp] = annotations
-}
-
-func (p *Properties) GetNamespace() (string, error) {
-	if raw, ok := (*p)[NamespaceProp]; ok {
-		if namespace, ok := raw.(string); ok {
-			return namespace, nil
-		}
-		return "", fmt.Errorf("NamespaceProp is not a string")
-	}
-	return "", fmt.Errorf("NamespaceProp not set")
-}
-
-func (p *Properties) HasNamespace() bool {
-	_, ok := (*p)[NamespaceProp]
-	return ok
-}
-
-func (p *Properties) SetNamespace(namespace string) {
-	(*p)[NamespaceProp] = namespace
-}
-
-func (p *Properties) GetPod() (string, error) {
-	if raw, ok := (*p)[PodProp]; ok {
-		if pod, ok := raw.(string); ok {
-			return pod, nil
-		}
-		return "", fmt.Errorf("PodProp is not a string")
-	}
-	return "", fmt.Errorf("PodProp not set")
-}
-
-func (p *Properties) HasPod() bool {
-	_, ok := (*p)[PodProp]
-	return ok
-}
-
-func (p *Properties) SetPod(pod string) {
-	(*p)[PodProp] = pod
-}
-
-func (p *Properties) GetServices() ([]string, error) {
-	if raw, ok := (*p)[ServiceProp]; ok {
-		if services, ok := raw.([]string); ok {
-			return services, nil
-		}
-		return []string{}, fmt.Errorf("ServiceProp is not a string")
-	}
-	return []string{}, fmt.Errorf("ServiceProp not set")
-}
-
-func (p *Properties) HasService() bool {
-	_, ok := (*p)[ServiceProp]
-	return ok
-}
-
-func (p *Properties) SetServices(services []string) {
-	(*p)[ServiceProp] = services
-}
-
-func (p *Properties) MarshalBinary() (data []byte, err error) {
-	buff := util.NewBuffer()
-	buff.WriteUInt8(CodecVersion) // version
-
-	// ClusterProp
-	cluster, err := p.GetCluster()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1)) // write non-nil byte
-		buff.WriteString(cluster) // write string
-	}
-
-	// NodeProp
-	node, err := p.GetNode()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1)) // write non-nil byte
-		buff.WriteString(node)    // write string
-	}
-
-	// ContainerProp
-	container, err := p.GetContainer()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1))   // write non-nil byte
-		buff.WriteString(container) // write string
-	}
-
-	// ControllerProp
-	controller, err := p.GetController()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1))    // write non-nil byte
-		buff.WriteString(controller) // write string
-	}
-
-	// ControllerKindProp
-	controllerKind, err := p.GetControllerKind()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1))        // write non-nil byte
-		buff.WriteString(controllerKind) // write string
-	}
-
-	// NamespaceProp
-	namespace, err := p.GetNamespace()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1))   // write non-nil byte
-		buff.WriteString(namespace) // write string
-	}
-
-	// PodProp
-	pod, err := p.GetPod()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1)) // write non-nil byte
-		buff.WriteString(pod)     // write string
-	}
-
-	// LabelProp
-	labels, err := p.GetLabels()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1))  // write non-nil byte
-		buff.WriteInt(len(labels)) // map length
-		for k, v := range labels {
-			buff.WriteString(k) // write string
-			buff.WriteString(v) // write string
-		}
-	}
-
-	// AnnotationProp
-	annotations, err := p.GetAnnotations()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1))       // write non-nil byte
-		buff.WriteInt(len(annotations)) // map length
-		for k, v := range annotations {
-			buff.WriteString(k) // write string
-			buff.WriteString(v) // write string
-		}
-	}
-
-	// ServiceProp
-	services, err := p.GetServices()
-	if err != nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1))    // write non-nil byte
-		buff.WriteInt(len(services)) // slice length
-		for _, v := range services {
-			buff.WriteString(v) // write string
-		}
-	}
-
-	return buff.Bytes(), nil
-}
-
-func (p *Properties) UnmarshalBinary(data []byte) error {
-	buff := util.NewBufferFromBytes(data)
-	v := buff.ReadUInt8() // version
-	if v != CodecVersion {
-		return fmt.Errorf("Invalid Version. Expected %d, got %d", CodecVersion, v)
-	}
-
-	*p = Properties{}
-
-	// ClusterProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		cluster := buff.ReadString() // read string
-		p.SetCluster(cluster)
-	}
-
-	// NodeProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		node := buff.ReadString() // read string
-		p.SetNode(node)
-	}
-
-	// ContainerProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		container := buff.ReadString() // read string
-		p.SetContainer(container)
-	}
-
-	// ControllerProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		controller := buff.ReadString() // read string
-		p.SetController(controller)
-	}
-
-	// ControllerKindProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		controllerKind := buff.ReadString() // read string
-		p.SetControllerKind(controllerKind)
-	}
-
-	// NamespaceProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		namespace := buff.ReadString() // read string
-		p.SetNamespace(namespace)
-	}
-
-	// PodProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		pod := buff.ReadString() // read string
-		p.SetPod(pod)
-	}
-
-	// LabelProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		length := buff.ReadInt() // read map len
-		labels := make(map[string]string, length)
-		for idx := 0; idx < length; idx++ {
-			key := buff.ReadString()
-			val := buff.ReadString()
-			labels[key] = val
-		}
-		p.SetLabels(labels)
-	}
-
-	// AnnotationProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		length := buff.ReadInt() // read map len
-		annotations := make(map[string]string, length)
-		for idx := 0; idx < length; idx++ {
-			key := buff.ReadString()
-			val := buff.ReadString()
-			annotations[key] = val
-		}
-		p.SetAnnotations(annotations)
-	}
-
-	// ServiceProp
-	if buff.ReadUInt8() == 1 { // read nil byte
-		length := buff.ReadInt() // read map len
-		services := make([]string, length)
-		for idx := 0; idx < length; idx++ {
-			val := buff.ReadString()
-			services[idx] = val
-		}
-		p.SetServices(services)
-	}
-
-	return nil
-}