Forráskód Böngészése

Merge pull request #768 from kubecost/sean/refactor-properties

Sean/refactor properties
Sean Holcomb 5 éve
szülő
commit
0dd46e0263

+ 1 - 1
pkg/cloud/azureprovider.go

@@ -751,7 +751,7 @@ func (az *Azure) NodePricing(key Key) (*Node, error) {
 			VCPUCost: c.CPU,
 			RAMCost:  c.RAM,
 			GPUCost:  c.GPU,
-			GPU: key.(*azureKey).GetGPUCount(),
+			GPU:      key.(*azureKey).GetGPUCount(),
 		}, nil
 	}
 	return &Node{

+ 1 - 0
pkg/cloud/csvretriever.go

@@ -11,6 +11,7 @@ import (
 	"strings"
 	"time"
 )
+
 type CSVRetriever interface {
 	GetCSVReaders(start, end time.Time) ([]*csv.Reader, error)
 }

+ 1 - 1
pkg/clustermanager/clustersendpoints.go

@@ -7,8 +7,8 @@ import (
 
 	"github.com/julienschmidt/httprouter"
 
-	"k8s.io/klog"
 	"github.com/kubecost/cost-model/pkg/util/json"
+	"k8s.io/klog"
 )
 
 // DataEnvelope is a generic wrapper struct for http response data

+ 63 - 98
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,57 +2116,22 @@ 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{}
-
-	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:
-			if len(fields) != 2 {
-				return kubecost.Properties{}, fmt.Errorf("illegal aggregate by label: %s", raw)
+// is returned with empty AllocationProperties.
+func ParseAggregationProperties(qp util.QueryParams, key string) ([]string, error) {
+	aggregateBy := []string{}
+	for _, agg := range qp.GetList(key, ",") {
+		aggregate := strings.TrimSpace(agg)
+		if aggregate != "" {
+			if prop, err := kubecost.ParseProperty(aggregate); err == nil {
+				aggregateBy = append(aggregateBy, string(prop))
+			} else if strings.HasPrefix(aggregate, "label:") {
+				aggregateBy = append(aggregateBy, aggregate)
+			} else if strings.HasPrefix(aggregate, "annotation:") {
+				aggregateBy = append(aggregateBy, aggregate)
 			}
-			label := prom.SanitizeLabelName(strings.TrimSpace(fields[1]))
-			labelMap[label] = ""
-		case kubecost.AnnotationProp:
-			if len(fields) != 2 {
-				return kubecost.Properties{}, fmt.Errorf("illegal aggregate by annotation: %s", raw)
-			}
-			annotation := prom.SanitizeLabelName(strings.TrimSpace(fields[1]))
-			annotationMap[annotation] = ""
 		}
-
-	}
-
-	if len(labelMap) > 0 {
-		aggProps.SetLabels(labelMap)
 	}
-
-	if len(annotationMap) > 0 {
-		aggProps.SetAnnotations(annotationMap)
-	}
-
-	return aggProps, nil
+	return aggregateBy, nil
 }
 
 // ComputeAllocationHandler computes an AllocationSetRange from the CostModel.

+ 35 - 37
pkg/costmodel/allocation.go

@@ -347,11 +347,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)
@@ -614,7 +614,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
 	}
 }
 
@@ -654,7 +654,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
 	}
 }
 
@@ -750,7 +750,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
 	}
 }
 
@@ -790,7 +790,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
 	}
 }
 
@@ -1001,11 +1001,10 @@ 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
+			if allocLabels == nil {
+				allocLabels = make(map[string]string)
 			}
-
 			// Apply namespace labels first, then pod labels so that pod labels
 			// overwrite namespace labels.
 			nsKey := newNamespaceKey(podKey.Cluster, podKey.Namespace)
@@ -1020,7 +1019,7 @@ func applyLabels(podMap map[podKey]*Pod, namespaceLabels map[namespaceKey]map[st
 				}
 			}
 
-			alloc.Properties.SetLabels(allocLabels)
+			alloc.Properties.Labels = allocLabels
 		}
 	}
 }
@@ -1028,11 +1027,10 @@ 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
+			if allocAnnotations == nil {
+				allocAnnotations = make(map[string]string)
 			}
-
 			// Apply namespace annotations first, then pod annotations so that
 			// pod labels overwrite namespace labels.
 			if labels, ok := namespaceAnnotations[key.Namespace]; ok {
@@ -1046,7 +1044,7 @@ func applyAnnotations(podMap map[podKey]*Pod, namespaceAnnotations map[string]ma
 				}
 			}
 
-			alloc.Properties.SetAnnotations(allocAnnotations)
+			alloc.Properties.Annotations = allocAnnotations
 		}
 	}
 }
@@ -1266,7 +1264,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
 
 			}
 		}
@@ -1277,8 +1275,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
 			}
 		}
 	}
@@ -1637,11 +1635,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
 	}
@@ -1680,11 +1678,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
 	}
@@ -1911,15 +1909,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
 }

+ 20 - 20
pkg/costmodel/cluster_helpers_test.go

@@ -711,15 +711,15 @@ func TestBuildGPUCostMap(t *testing.T) {
 			promResult: []*prom.QueryResult{
 				{
 					Metric: map[string]interface{}{
-						"cluster_id": "cluster1",
-						"node": "node1",
-						"instance_type":"type1",
-						"provider_id": "provider1",
+						"cluster_id":    "cluster1",
+						"node":          "node1",
+						"instance_type": "type1",
+						"provider_id":   "provider1",
 					},
 					Values: []*util.Vector{
 						&util.Vector{
 							Timestamp: 0,
-							Value: 0,
+							Value:     0,
 						},
 					},
 				},
@@ -744,15 +744,15 @@ func TestBuildGPUCostMap(t *testing.T) {
 			promResult: []*prom.QueryResult{
 				{
 					Metric: map[string]interface{}{
-						"cluster_id": "cluster1",
-						"node": "node1",
-						"instance_type":"type1",
-						"provider_id": "provider1",
+						"cluster_id":    "cluster1",
+						"node":          "node1",
+						"instance_type": "type1",
+						"provider_id":   "provider1",
 					},
 					Values: []*util.Vector{
 						&util.Vector{
 							Timestamp: 0,
-							Value: 2,
+							Value:     2,
 						},
 					},
 				},
@@ -777,15 +777,15 @@ func TestBuildGPUCostMap(t *testing.T) {
 			promResult: []*prom.QueryResult{
 				{
 					Metric: map[string]interface{}{
-						"cluster_id": "cluster1",
-						"node": "node1",
-						"instance_type":"type1",
-						"provider_id": "provider1",
+						"cluster_id":    "cluster1",
+						"node":          "node1",
+						"instance_type": "type1",
+						"provider_id":   "provider1",
 					},
 					Values: []*util.Vector{
 						&util.Vector{
 							Timestamp: 0,
-							Value: 2,
+							Value:     2,
 						},
 					},
 				},
@@ -818,15 +818,15 @@ func TestBuildGPUCostMap(t *testing.T) {
 			promResult: []*prom.QueryResult{
 				{
 					Metric: map[string]interface{}{
-						"cluster_id": "cluster1",
-						"node": "node1",
-						"instance_type":"type1",
-						"provider_id": "provider1",
+						"cluster_id":    "cluster1",
+						"node":          "node1",
+						"instance_type": "type1",
+						"provider_id":   "provider1",
 					},
 					Values: []*util.Vector{
 						&util.Vector{
 							Timestamp: 0,
-							Value: 2,
+							Value:     2,
 						},
 					},
 				},

+ 11 - 11
pkg/costmodel/containerkeys.go

@@ -13,22 +13,22 @@ var (
 	NewKeyTupleErr = errors.New("NewKeyTuple() Provided key not containing exactly 3 components.")
 
 	// Static Errors for ContainerMetric creation
-	InvalidKeyErr error = errors.New("Not a valid key")
-	NoContainerErr error = errors.New("Prometheus vector does not have container name")
+	InvalidKeyErr      error = errors.New("Not a valid key")
+	NoContainerErr     error = errors.New("Prometheus vector does not have container name")
 	NoContainerNameErr error = errors.New("Prometheus vector does not have string container name")
-	NoPodErr error = errors.New("Prometheus vector does not have pod name")
-	NoPodNameErr error = errors.New("Prometheus vector does not have string pod name")
-	NoNamespaceErr error = errors.New("Prometheus vector does not have namespace")
+	NoPodErr           error = errors.New("Prometheus vector does not have pod name")
+	NoPodNameErr       error = errors.New("Prometheus vector does not have string pod name")
+	NoNamespaceErr     error = errors.New("Prometheus vector does not have namespace")
 	NoNamespaceNameErr error = errors.New("Prometheus vector does not have string namespace")
-	NoNodeNameErr error = errors.New("Prometheus vector does not have string node")
-	NoClusterIDErr error = errors.New("Prometheus vector does not have string cluster_id")
+	NoNodeNameErr      error = errors.New("Prometheus vector does not have string node")
+	NoClusterIDErr     error = errors.New("Prometheus vector does not have string cluster_id")
 )
 
 //--------------------------------------------------------------------------
 //  KeyTuple
 //--------------------------------------------------------------------------
 
-// KeyTuple contains is a utility which parses Namespace, Key, and ClusterID from a 
+// KeyTuple contains is a utility which parses Namespace, Key, and ClusterID from a
 // comma delimitted string.
 type KeyTuple struct {
 	key    string
@@ -103,8 +103,8 @@ func containerMetricKey(ns, podName, containerName, nodeName, clusterID string)
 	return ns + "," + podName + "," + containerName + "," + nodeName + "," + clusterID
 }
 
-// NewContainerMetricFromKey creates a new ContainerMetric instance using a provided comma delimitted 
-// string key. 
+// NewContainerMetricFromKey creates a new ContainerMetric instance using a provided comma delimitted
+// string key.
 func NewContainerMetricFromKey(key string) (*ContainerMetric, error) {
 	s := strings.Split(key, ",")
 	if len(s) == 5 {
@@ -132,7 +132,7 @@ func NewContainerMetricFromValues(ns, podName, containerName, nodeName, clusterI
 	}
 }
 
-// NewContainerMetricsFromPod creates a slice of ContainerMetric instances for each container in the 
+// NewContainerMetricsFromPod creates a slice of ContainerMetric instances for each container in the
 // provided Pod.
 func NewContainerMetricsFromPod(pod *v1.Pod, clusterID string) ([]*ContainerMetric, error) {
 	podName := pod.GetObjectMeta().GetName()

+ 1 - 1
pkg/env/costmodelenv.go

@@ -30,7 +30,7 @@ const (
 	SQLAddressEnvVar               = "SQL_ADDRESS"
 	UseCSVProviderEnvVar           = "USE_CSV_PROVIDER"
 	CSVRegionEnvVar                = "CSV_REGION"
-	CSVEndpointEnvVar 			   = "CSV_ENDPOINT"
+	CSVEndpointEnvVar              = "CSV_ENDPOINT"
 	CSVPathEnvVar                  = "CSV_PATH"
 	ConfigPathEnvVar               = "CONFIG_PATH"
 	CloudProviderAPIKeyEnvVar      = "CLOUD_PROVIDER_API_KEY"

+ 159 - 207
pkg/kubecost/allocation.go

@@ -47,30 +47,30 @@ 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"`
 
 	// RawAllocationOnly is a pointer so if it is not present it will be
 	// marshalled as null rather than as an object with Go default values.
@@ -184,7 +184,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) {
@@ -366,7 +366,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
 }
@@ -393,7 +393,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 {
@@ -420,27 +420,8 @@ func (a *Allocation) add(that *Allocation) {
 		log.Warningf("Allocation.AggregateBy: trying to add a nil receiver")
 		return
 	}
-
-	aCluster, _ := a.Properties.GetCluster()
-	thatCluster, _ := that.Properties.GetCluster()
-	aNode, _ := a.Properties.GetNode()
-	thatNode, _ := that.Properties.GetNode()
-
-	// 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)
-		}
-	}
+	// Preserve string properties that are matching between the two allocations
+	a.Properties = a.Properties.Intersection(that.Properties)
 
 	// Expand the window to encompass both Allocations
 	a.Window = a.Window.Expand(that.Window)
@@ -549,9 +530,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(aggregateBy []string, 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.
@@ -688,7 +669,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 	//
 	// In order to maintain stable results when multiple operations are being
 	// carried out (e.g. sharing idle, sharing resources, and filtering) these
-	// coefficients are computed for the full set of allocaitons prior to
+	// coefficients are computed for the full set of allocations prior to
 	// adding shared overhead and prior to applying filters.
 
 	var err error
@@ -698,7 +679,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 	// the shared allocations).
 	var idleCoefficients map[string]map[string]map[string]float64
 	if idleSet.Length() > 0 && options.ShareIdle != ShareNone {
-		idleCoefficients, err = computeIdleCoeffs(properties, options, as, shareSet)
+		idleCoefficients, err = computeIdleCoeffs(options, as, shareSet)
 		if err != nil {
 			log.Warningf("AllocationSet.AggregateBy: compute idle coeff: %s", err)
 			return fmt.Errorf("error computing idle coefficients: %s", err)
@@ -731,7 +712,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 	// need to track this on a per-cluster, per-allocation, per-resource basis.
 	var idleFiltrationCoefficients map[string]map[string]map[string]float64
 	if len(options.FilterFuncs) > 0 && options.ShareIdle == ShareNone {
-		idleFiltrationCoefficients, err = computeIdleCoeffs(properties, options, as, shareSet)
+		idleFiltrationCoefficients, err = computeIdleCoeffs(options, as, shareSet)
 		if err != nil {
 			return fmt.Errorf("error computing idle filtration coefficients: %s", err)
 		}
@@ -757,7 +738,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.
 			})
 		}
 	}
@@ -767,7 +748,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 	// of the main allocation set. See above for details and an example.
 	var shareCoefficients map[string]float64
 	if shareSet.Length() > 0 {
-		shareCoefficients, err = computeShareCoeffs(properties, options, as)
+		shareCoefficients, err = computeShareCoeffs(aggregateBy, options, as)
 		if err != nil {
 			return fmt.Errorf("error computing share coefficients: %s", err)
 		}
@@ -775,10 +756,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
@@ -813,9 +794,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
@@ -845,7 +826,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 		}
 
 		// (5) generate key to use for aggregation-by-key and allocation name
-		key := alloc.generateKey(properties)
+		key := alloc.generateKey(aggregateBy)
 
 		alloc.Name = key
 		if options.MergeUnallocated && alloc.IsUnallocated() {
@@ -863,8 +844,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
 			}
@@ -873,9 +854,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
@@ -936,8 +917,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
 			}
@@ -978,7 +959,7 @@ func (as *AllocationSet) AggregateBy(properties Properties, options *AllocationA
 			}
 		}
 		if !skip {
-			key := alloc.generateKey(properties)
+			key := alloc.generateKey(aggregateBy)
 
 			alloc.Name = key
 			aggSet.Insert(alloc)
@@ -999,7 +980,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(aggregateBy []string, options *AllocationAggregationOptions, as *AllocationSet) (map[string]float64, error) {
 	// Compute coeffs by totalling per-allocation, then dividing by the total.
 	coeffs := map[string]float64{}
 
@@ -1019,7 +1000,7 @@ func computeShareCoeffs(properties Properties, options *AllocationAggregationOpt
 
 		// Determine the post-aggregation key under which the allocation will
 		// be shared.
-		name := alloc.generateKey(properties)
+		name := alloc.generateKey(aggregateBy)
 
 		// If the current allocation will be filtered out in step 3, contribute
 		// its share of the shared coefficient to a "__filtered__" bin, which
@@ -1064,7 +1045,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(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
@@ -1085,9 +1066,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
@@ -1132,9 +1113,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
@@ -1185,7 +1166,7 @@ func computeIdleCoeffs(properties Properties, options *AllocationAggregationOpti
 	return coeffs, nil
 }
 
-func (a *Allocation) generateKey(properties Properties) string {
+func (a *Allocation) generateKey(aggregateBy []string) string {
 	if a == nil {
 		return ""
 	}
@@ -1194,140 +1175,111 @@ func (a *Allocation) generateKey(properties Properties) string {
 	// identifies allocations.
 	names := []string{}
 
-	if properties.HasCluster() {
-		cluster, _ := a.Properties.GetCluster()
-		names = append(names, cluster)
-	}
-
-	if properties.HasNode() {
-		node, _ := a.Properties.GetNode()
-		names = append(names, node)
-	}
-
-	if properties.HasNamespace() {
-		namespace, _ := a.Properties.GetNamespace()
-		names = append(names, namespace)
-	}
-
-	if properties.HasControllerKind() {
-		controllerKind, err := a.Properties.GetControllerKind()
-		if err != nil {
-			// 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
-		}
-		names = append(names, controllerKind)
-	}
-
-	if properties.HasController() {
-		if !properties.HasControllerKind() {
-			controllerKind, err := a.Properties.GetControllerKind()
-			if err == nil {
-				names = append(names, controllerKind)
+	for _, agg := range aggregateBy {
+		switch true {
+		case agg == AllocationClusterProp:
+			names = append(names, a.Properties.Cluster)
+		case agg == AllocationNodeProp:
+			names = append(names, a.Properties.Node)
+		case agg == AllocationNamespaceProp:
+			names = append(names, a.Properties.Namespace)
+		case agg == AllocationControllerKindProp:
+			controllerKind := a.Properties.ControllerKind
+			if controllerKind == "" {
+				// Indicate that allocation has no controller
+				controllerKind = UnallocatedSuffix
 			}
-		}
-
-		controller, err := a.Properties.GetController()
-		if err != nil {
-			// Indicate that allocation has no controller
-			controller = UnallocatedSuffix
-		}
-
-		names = append(names, controller)
-	}
-
-	if properties.HasPod() {
-		pod, _ := a.Properties.GetPod()
-		names = append(names, pod)
-	}
-
-	if properties.HasContainer() {
-		container, _ := a.Properties.GetContainer()
-		names = append(names, container)
-	}
-
-	if properties.HasService() {
-		services, err := a.Properties.GetServices()
-		if err != nil {
-			// Indicate that allocation has no services
-			names = append(names, UnallocatedSuffix)
-		} else {
-			if len(services) > 0 {
+			names = append(names, controllerKind)
+		case agg == AllocationDaemonSetProp || agg == AllocationStatefulSetProp || agg == AllocationDeploymentProp || agg == AllocationJobProp:
+			controller := a.Properties.Controller
+			if agg != a.Properties.ControllerKind || controller == "" {
+				// The allocation does not have the specified controller kind
+				controller = UnallocatedSuffix
+			}
+			names = append(names, controller)
+		case agg == AllocationControllerProp:
+			controller := a.Properties.Controller
+			if controller == "" {
+				// Indicate that allocation has no controller
+				controller = UnallocatedSuffix
+			} else if a.Properties.ControllerKind != "" {
+				controller = fmt.Sprintf("%s:%s", a.Properties.ControllerKind, controller)
+			}
+			names = append(names, controller)
+		case agg == AllocationPodProp:
+			names = append(names, a.Properties.Pod)
+		case agg == AllocationContainerProp:
+			names = append(names, a.Properties.Container)
+		case agg == AllocationServiceProp:
+			services := a.Properties.Services
+			if services == nil || len(services) == 0 {
+				// Indicate that allocation has no services
+				names = append(names, UnallocatedSuffix)
+			} else {
+				// This just uses the first service
 				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 {
-			// Indicate that allocation has no annotations
-			names = append(names, UnallocatedSuffix)
-		} else {
-			annotationNames := []string{}
-
-			aggAnnotations, _ := properties.GetAnnotations()
-			for annotationName := range aggAnnotations {
-				if val, ok := annotations[annotationName]; ok {
-					annotationNames = append(annotationNames, fmt.Sprintf("%s=%s", annotationName, val))
-				} else if indexOf(UnallocatedSuffix, annotationNames) == -1 { // if UnallocatedSuffix not already in names
-					annotationNames = append(annotationNames, UnallocatedSuffix)
+		case strings.HasPrefix(agg, "label:"):
+			labels := a.Properties.Labels
+			if labels == nil {
+				// Indicate that allocation has no labels
+				names = append(names, UnallocatedSuffix)
+			} else {
+				labelNames := []string{}
+				aggLabels := strings.Split(strings.TrimPrefix(agg, "label:"), ";")
+				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(annotationNames) > 1 {
-				sort.Strings(annotationNames)
-			}
-			unallocatedSuffixIndex := indexOf(UnallocatedSuffix, annotationNames)
-			// suffix should be at index 0 if it exists b/c of underscores
-			if unallocatedSuffixIndex != -1 {
-				annotationNames = append(annotationNames[:unallocatedSuffixIndex], annotationNames[unallocatedSuffixIndex+1:]...)
-				annotationNames = append(annotationNames, UnallocatedSuffix) // append to end
-			}
-
-			names = append(names, annotationNames...)
-		}
-	}
-
-	if properties.HasLabel() {
-		labels, err := a.Properties.GetLabels()
-		if err != 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
+				}
+
+				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
-			}
+		case strings.HasPrefix(agg, "annotation:"):
+			annotations := a.Properties.Annotations
+			if annotations == nil {
+				// Indicate that allocation has no annotations
+				names = append(names, UnallocatedSuffix)
+			} else {
+				annotationNames := []string{}
+				aggAnnotations := strings.Split(strings.TrimPrefix(agg, "annotation:"), ";")
+				for _, annotationName := range aggAnnotations {
+					if val, ok := annotations[annotationName]; ok {
+						annotationNames = append(annotationNames, fmt.Sprintf("%s=%s", annotationName, val))
+					} else if indexOf(UnallocatedSuffix, annotationNames) == -1 { // if UnallocatedSuffix not already in names
+						annotationNames = append(annotationNames, UnallocatedSuffix)
+					}
+				}
+				// resolve arbitrary ordering. e.g., app=app0/env=env0 is the same agg as env=env0/app=app0
+				if len(annotationNames) > 1 {
+					sort.Strings(annotationNames)
+				}
+				unallocatedSuffixIndex := indexOf(UnallocatedSuffix, annotationNames)
+				// suffix should be at index 0 if it exists b/c of underscores
+				if unallocatedSuffixIndex != -1 {
+					annotationNames = append(annotationNames[:unallocatedSuffixIndex], annotationNames[unallocatedSuffixIndex+1:]...)
+					annotationNames = append(annotationNames, UnallocatedSuffix) // append to end
+				}
 
-			names = append(names, labelNames...)
+				names = append(names, annotationNames...)
+			}
 		}
+
 	}
 
 	return strings.Join(names, "/")
@@ -1445,8 +1397,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
 		}
@@ -1488,7 +1440,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"],
@@ -1866,14 +1818,14 @@ 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(aggregateBy []string, options *AllocationAggregationOptions) error {
 	aggRange := &AllocationSetRange{allocations: []*AllocationSet{}}
 
 	asr.Lock()
 	defer asr.Unlock()
 
 	for _, as := range asr.allocations {
-		err := as.AggregateBy(properties, options)
+		err := as.AggregateBy(aggregateBy, options)
 		if err != nil {
 			return err
 		}

+ 209 - 177
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,
@@ -107,6 +107,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,
@@ -131,6 +132,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,
@@ -262,6 +264,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,
@@ -285,6 +288,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,
@@ -407,12 +411,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,
@@ -460,8 +464,9 @@ func TestAllocationSet_generateKey(t *testing.T) {
 	var alloc *Allocation
 	var key string
 
-	props := Properties{}
-	props.SetCluster("")
+	props := []string{
+		AllocationClusterProp,
+	}
 
 	key = alloc.generateKey(props)
 	if key != "" {
@@ -469,9 +474,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",
 		},
@@ -482,18 +487,21 @@ func TestAllocationSet_generateKey(t *testing.T) {
 		t.Fatalf("generateKey: expected \"cluster1\"; actual \"%s\"", key)
 	}
 
-	props.SetNamespace("")
-	props.SetLabels(map[string]string{"app": ""})
+	props = []string{
+		AllocationClusterProp,
+		AllocationNamespaceProp,
+		"label: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",
 		},
@@ -510,153 +518,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
@@ -835,12 +843,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
 		}
@@ -856,7 +864,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1a AggregationProperties=(Cluster)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ClusterProp: ""}, nil)
+	err = as.AggregateBy([]string{AllocationClusterProp}, nil)
 	assertAllocationSetTotals(t, as, "1a", err, numClusters+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1a", map[string]float64{
 		"cluster1": 46.00,
@@ -867,7 +875,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1b AggregationProperties=(Namespace)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: true}, nil)
+	err = as.AggregateBy([]string{AllocationNamespaceProp}, nil)
 	assertAllocationSetTotals(t, as, "1b", err, numNamespaces+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1b", map[string]float64{
 		"namespace1": 28.00,
@@ -879,7 +887,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1c AggregationProperties=(Pod)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{PodProp: true}, nil)
+	err = as.AggregateBy([]string{AllocationPodProp}, nil)
 	assertAllocationSetTotals(t, as, "1c", err, numPods+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1c", map[string]float64{
 		"pod-jkl":  6.00,
@@ -897,7 +905,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1d AggregationProperties=(Container)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ContainerProp: true}, nil)
+	err = as.AggregateBy([]string{AllocationContainerProp}, nil)
 	assertAllocationSetTotals(t, as, "1d", err, numContainers+numIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1d", map[string]float64{
 		"container2": 6.00,
@@ -915,7 +923,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1e AggregationProperties=(ControllerKind)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ControllerKindProp: true}, nil)
+	err = as.AggregateBy([]string{AllocationControllerKindProp}, nil)
 	assertAllocationSetTotals(t, as, "1e", err, numControllerKinds+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1e", map[string]float64{
 		"daemonset":       12.00,
@@ -928,14 +936,14 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1f AggregationProperties=(Controller)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ControllerProp: true}, nil)
+	err = as.AggregateBy([]string{AllocationControllerProp}, nil)
 	assertAllocationSetTotals(t, as, "1f", err, numControllers+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1f", map[string]float64{
-		"deployment/deployment2":   24.00,
-		"daemonset/daemonset1":     12.00,
-		"deployment/deployment3":   6.00,
-		"statefulset/statefulset1": 12.00,
-		"deployment/deployment1":   12.00,
+		"deployment:deployment2":   24.00,
+		"daemonset:daemonset1":     12.00,
+		"deployment:deployment3":   6.00,
+		"statefulset:statefulset1": 12.00,
+		"deployment:deployment1":   12.00,
 		IdleSuffix:                 30.00,
 		UnallocatedSuffix:          16.00,
 	})
@@ -943,7 +951,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 1g AggregationProperties=(Service)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ServiceProp: true}, nil)
+	err = as.AggregateBy([]string{AllocationServiceProp}, nil)
 	assertAllocationSetTotals(t, as, "1g", err, numServices+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1g", map[string]float64{
 		"service1":        12.00,
@@ -954,7 +962,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([]string{"label:app"}, nil)
 	assertAllocationSetTotals(t, as, "1h", err, numLabelApps+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1h", map[string]float64{
 		"app=app1":        16.00,
@@ -964,12 +972,14 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	})
 	assertAllocationWindow(t, as, "1h", startYesterday, endYesterday, 1440.0)
 
-	// 1i AggregationProperties=(ControllerKind:deployment)
+	// 1i AggregationProperties=(deployment)
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{ControllerKindProp: "deployment"}, nil)
-	assertAllocationSetTotals(t, as, "1i", err, 1+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
+	err = as.AggregateBy([]string{AllocationDeploymentProp}, nil)
+	assertAllocationSetTotals(t, as, "1i", err, 3+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1i", map[string]float64{
-		"deployment":      42.00,
+		"deployment1":     12.00,
+		"deployment2":     24.00,
+		"deployment3":     6.00,
 		IdleSuffix:        30.00,
 		UnallocatedSuffix: 40.00,
 	})
@@ -977,7 +987,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([]string{"annotation:team"}, nil)
 	assertAllocationSetTotals(t, as, "1j", err, 2+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "1j", map[string]float64{
 		"team=team1":      12.00,
@@ -985,7 +995,29 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 		IdleSuffix:        30.00,
 		UnallocatedSuffix: 64.00,
 	})
-	assertAllocationWindow(t, as, "1i", startYesterday, endYesterday, 1440.0)
+	assertAllocationWindow(t, as, "1j", startYesterday, endYesterday, 1440.0)
+
+	// 1k AggregationProperties=(daemonSet)
+	as = generateAllocationSet(start)
+	err = as.AggregateBy([]string{AllocationDaemonSetProp}, nil)
+	assertAllocationSetTotals(t, as, "1k", err, 1+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
+	assertAllocationTotals(t, as, "1k", map[string]float64{
+		"daemonset1":      12.00,
+		IdleSuffix:        30.00,
+		UnallocatedSuffix: 70.00,
+	})
+	assertAllocationWindow(t, as, "1k", startYesterday, endYesterday, 1440.0)
+
+	// 1l AggregationProperties=(statefulSet)
+	as = generateAllocationSet(start)
+	err = as.AggregateBy([]string{AllocationStatefulSetProp}, nil)
+	assertAllocationSetTotals(t, as, "1l", err, 1+numIdle+numUnallocated, activeTotalCost+idleTotalCost)
+	assertAllocationTotals(t, as, "1l", map[string]float64{
+		"statefulset1":    12.00,
+		IdleSuffix:        30.00,
+		UnallocatedSuffix: 70.00,
+	})
+	assertAllocationWindow(t, as, "1l", startYesterday, endYesterday, 1440.0)
 
 	// 2  Multi-aggregation
 
@@ -995,7 +1027,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([]string{"label: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{
@@ -1008,7 +1040,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([]string{AllocationClusterProp, "label: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,
@@ -1021,9 +1053,9 @@ 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)
-	assertAllocationSetTotals(t, as, "2e", err, 11, activeTotalCost+idleTotalCost)
-	assertAllocationTotals(t, as, "2e", map[string]float64{
+	err = as.AggregateBy([]string{AllocationPodProp, "annotation:team"}, nil)
+	assertAllocationSetTotals(t, as, "2f", err, 11, activeTotalCost+idleTotalCost)
+	assertAllocationTotals(t, as, "2f", map[string]float64{
 		"pod-jkl/" + UnallocatedSuffix: 6.00,
 		"pod-stu/team=team1":           6.00,
 		"pod-abc/" + UnallocatedSuffix: 6.00,
@@ -1046,7 +1078,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{ShareIdle: ShareWeighted})
 	assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "3a", map[string]float64{
 		"namespace1": 42.69,
@@ -1060,7 +1092,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{ShareIdle: ShareEven})
 	assertAllocationSetTotals(t, as, "3a", err, numNamespaces, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "3a", map[string]float64{
 		"namespace1": 38.00,
@@ -1076,7 +1108,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isNamespace3},
 		ShareSplit: ShareEven,
 	})
@@ -1093,7 +1125,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isNamespace3},
 		ShareSplit: ShareWeighted,
 	})
@@ -1111,7 +1143,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isApp1},
 		ShareSplit: ShareEven,
 	})
@@ -1130,7 +1162,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
 		ShareSplit:        ShareWeighted,
 	})
@@ -1147,21 +1179,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([]string{AllocationClusterProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
 		ShareIdle:   ShareNone,
 	})
@@ -1174,7 +1206,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([]string{AllocationClusterProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
 		ShareIdle:   ShareWeighted,
 	})
@@ -1186,7 +1218,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isCluster("cluster1")},
 		ShareIdle:   ShareNone,
 	})
@@ -1200,7 +1232,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([]string{AllocationClusterProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareIdle:   ShareNone,
 	})
@@ -1216,7 +1248,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 
 	// 6a SplitIdle
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{SplitIdle: true})
+	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{SplitIdle: true})
 	assertAllocationSetTotals(t, as, "6a", err, numNamespaces+numSplitIdle, activeTotalCost+idleTotalCost)
 	assertAllocationTotals(t, as, "6a", map[string]float64{
 		"namespace1":                           28.00,
@@ -1231,7 +1263,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareIdle:   ShareWeighted,
 	})
@@ -1245,7 +1277,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareIdle:   ShareEven,
 	})
@@ -1262,7 +1294,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
 		SharedHourlyCosts: map[string]float64{"total": sharedOverheadHourlyCost},
 		ShareSplit:        ShareWeighted,
@@ -1283,7 +1315,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
 		ShareSplit:  ShareWeighted,
@@ -1331,7 +1363,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		ShareFuncs: []AllocationMatchFunc{isNamespace("namespace1")},
 		ShareSplit: ShareWeighted,
 		ShareIdle:  ShareWeighted,
@@ -1381,7 +1413,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([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		FilterFuncs: []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareFuncs:  []AllocationMatchFunc{isNamespace("namespace1")},
 		ShareSplit:  ShareWeighted,
@@ -1422,7 +1454,7 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 	//
 	// Then namespace 2 is filtered.
 	as = generateAllocationSet(start)
-	err = as.AggregateBy(Properties{NamespaceProp: ""}, &AllocationAggregationOptions{
+	err = as.AggregateBy([]string{AllocationNamespaceProp}, &AllocationAggregationOptions{
 		FilterFuncs:       []AllocationMatchFunc{isNamespace("namespace2")},
 		ShareSplit:        ShareWeighted,
 		ShareIdle:         ShareWeighted,

+ 294 - 0
pkg/kubecost/allocationprops.go

@@ -0,0 +1,294 @@
+package kubecost
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+)
+
+const (
+	AllocationNilProp            string = ""
+	AllocationClusterProp        string = "cluster"
+	AllocationNodeProp           string = "node"
+	AllocationContainerProp      string = "container"
+	AllocationControllerProp     string = "controller"
+	AllocationControllerKindProp string = "controllerKind"
+	AllocationNamespaceProp      string = "namespace"
+	AllocationPodProp            string = "pod"
+	AllocationProviderIDProp     string = "providerID"
+	AllocationServiceProp        string = "service"
+	AllocationLabelProp          string = "label"
+	AllocationAnnotationProp     string = "annotation"
+	AllocationDeploymentProp     string = "deployment"
+	AllocationStatefulSetProp    string = "statefulset"
+	AllocationDaemonSetProp      string = "daemonset"
+	AllocationJobProp            string = "job"
+)
+
+func ParseProperty(text string) (string, 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
+	case "deployment":
+		return AllocationDeploymentProp, nil
+	case "daemonset":
+		return AllocationDaemonSetProp, nil
+	case "statefulset":
+		return AllocationStatefulSetProp, nil
+	case "job":
+		return AllocationJobProp, nil
+	}
+	return AllocationNilProp, fmt.Errorf("invalid allocation property: %s", text)
+}
+
+// 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
+
+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
+
+	var services []string
+	for _, s := range p.Services {
+		services = append(services, s)
+	}
+	clone.Services = services
+
+	labels := make(map[string]string)
+	for k, v := range p.Labels {
+		labels[k] = v
+	}
+	clone.Labels = labels
+
+	annotations := make(map[string]string)
+	for k, v := range p.Annotations {
+		annotations[k] = v
+	}
+	clone.Annotations = 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
+			}
+		}
+	} else {
+		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
+			}
+		}
+	} else {
+		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
+			}
+		}
+	} else {
+		return false
+	}
+
+	return true
+}
+
+// Intersection returns an *AllocationProperties which contains all matching fields between the calling and parameter AllocationProperties
+// nillable slices and maps are left as nil
+func (p *AllocationProperties) Intersection(that *AllocationProperties) *AllocationProperties {
+	if p == nil || that == nil {
+		return nil
+	}
+	intersectionProps := &AllocationProperties{}
+	if p.Cluster == that.Cluster {
+		intersectionProps.Cluster = p.Cluster
+	}
+	if p.Node == that.Node {
+		intersectionProps.Node = p.Node
+	}
+	if p.Container == that.Container {
+		intersectionProps.Container = p.Container
+	}
+	if p.Controller == that.Controller {
+		intersectionProps.Controller = p.Controller
+	}
+	if p.ControllerKind == that.ControllerKind {
+		intersectionProps.ControllerKind = p.ControllerKind
+	}
+	if p.Namespace == that.Namespace {
+		intersectionProps.Namespace = p.Namespace
+	}
+	if p.Pod == that.Pod {
+		intersectionProps.Pod = p.Pod
+	}
+	if p.ProviderID == that.ProviderID {
+		intersectionProps.ProviderID = p.ProviderID
+	}
+	return intersectionProps
+}
+
+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 fmt.Sprintf("{%s}", strings.Join(strs, "; "))
+}

+ 61 - 0
pkg/kubecost/allocationprops_test.go

@@ -0,0 +1,61 @@
+package kubecost
+
+// TODO niko/etl
+// func TestParseProperty(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperty_String(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_Clone(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_Intersection(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetCluster(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetCluster(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetContainer(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetContainer(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetController(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetController(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetControllerKind(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetControllerKind(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetLabels(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetLabels(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetNamespace(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetNamespace(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetPod(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetPod(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_GetServices(t *testing.T) {}
+
+// TODO niko/etl
+// func TestProperties_SetServices(t *testing.T) {}

+ 23 - 22
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:
+					props.Cluster = value
+				case AllocationNodeProp:
+					props.Node = value
+				case AllocationNamespaceProp:
+					props.Namespace = value
+				case AllocationControllerKindProp:
+					props.ControllerKind = value
+				case AllocationControllerProp:
+					props.Controller = value
+				case AllocationPodProp:
+					props.Pod = value
+				case AllocationContainerProp:
+					props.Container = value
+				case AllocationServiceProp:
 					// 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(),
@@ -1915,7 +1916,7 @@ func (n *Node) Clone() Asset {
 		RAMBreakdown: n.RAMBreakdown.Clone(),
 		CPUCost:      n.CPUCost,
 		GPUCost:      n.GPUCost,
-		GPUCount:      n.GPUCount,
+		GPUCount:     n.GPUCount,
 		RAMCost:      n.RAMCost,
 		Preemptible:  n.Preemptible,
 		Discount:     n.Discount,

+ 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,6 +20,10 @@ package kubecost
 // @bingen:generate:Allocation
 // @bingen:generate:AllocationSet
 // @bingen:generate:AllocationSetRange
+// @bingen:generate:AllocationProperties
+// @bingen:generate:AllocationProperty
+// @bingen:generate:AllocationLabels
+// @bingen:generate:AllocationAnnotations
 // @bingen:generate:RawAllocationOnlyData
 
-//go:generate bingen -package=kubecost -version=10 -buffer=github.com/kubecost/cost-model/pkg/util
+//go:generate bingen -package=kubecost -version=11 -buffer=github.com/kubecost/cost-model/pkg/util

+ 231 - 19
pkg/kubecost/kubecost_codecs.go

@@ -25,7 +25,7 @@ const (
 	GeneratorPackageName string = "kubecost"
 
 	// CodecVersion is the version passed into the generator
-	CodecVersion uint8 = 10
+	CodecVersion uint8 = 11
 )
 
 //--------------------------------------------------------------------------
@@ -36,6 +36,7 @@ const (
 // to concrete types
 var typeMap map[string]reflect.Type = map[string]reflect.Type{
 	"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(),
@@ -117,15 +118,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 {
@@ -214,17 +221,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
@@ -324,6 +335,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
 //--------------------------------------------------------------------------

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

+ 0 - 115
pkg/kubecost/properties_test.go

@@ -1,115 +0,0 @@
-package kubecost
-
-import "testing"
-
-// TODO niko/etl
-// func TestParseProperty(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperty_String(t *testing.T) {}
-
-func TestProperties_AggregationString(t *testing.T) {
-	var props *Properties
-	var aggStrs []string
-
-	// nil Properties 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{}
-	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("")
-	aggStrs = props.AggregationStrings()
-	if len(aggStrs) != 1 || aggStrs[0] != "namespace" {
-		t.Fatalf("expected [\"namespace\"]; got %v", aggStrs)
-	}
-
-	// Properties with mutiple properties, including labels
-	// Note: order matters!
-	props = &Properties{}
-	props.SetNamespace("")
-	props.SetLabels(map[string]string{
-		"env": "",
-		"app": "",
-	})
-	props.SetCluster("")
-	aggStrs = props.AggregationStrings()
-	if len(aggStrs) != 4 {
-		t.Fatalf("expected length %d; got lenfth %d", 4, len(aggStrs))
-	}
-	if aggStrs[0] != "cluster" {
-		t.Fatalf("expected aggStrs[0] == \"%s\"; got \"%s\"", "cluster", aggStrs[0])
-	}
-	if aggStrs[1] != "namespace" {
-		t.Fatalf("expected aggStrs[1] == \"%s\"; got \"%s\"", "namespace", aggStrs[1])
-	}
-	if aggStrs[2] != "label:app" {
-		t.Fatalf("expected aggStrs[2] == \"%s\"; got \"%s\"", "label:app", aggStrs[2])
-	}
-	if aggStrs[3] != "label:env" {
-		t.Fatalf("expected aggStrs[3] == \"%s\"; got \"%s\"", "label:env", aggStrs[3])
-	}
-}
-
-// TODO niko/etl
-// func TestProperties_Clone(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_Intersection(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetCluster(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetCluster(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetContainer(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetContainer(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetController(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetController(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetControllerKind(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetControllerKind(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetLabels(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetLabels(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetNamespace(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetNamespace(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetPod(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetPod(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_GetServices(t *testing.T) {}
-
-// TODO niko/etl
-// func TestProperties_SetServices(t *testing.T) {}