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

CloudCosts

Signed-off-by: Sean Holcomb <seanholcomb@gmail.com>
Sean Holcomb 3 éve
szülő
commit
b8a2bfdb96

+ 6 - 0
pkg/env/env.go

@@ -123,6 +123,12 @@ func GetDuration(key string, defaultValue time.Duration) time.Duration {
 	return envMapper.GetDuration(key, defaultValue)
 }
 
+// GetList parses a []string from the enviroment variable key parameter.  If the environment
+// // variable is empty or fails to parse, nil is returned.
+func GetList(key, delimiter string) []string {
+	return envMapper.GetList(key, delimiter)
+}
+
 // Set sets the environment variable for the key provided using the value provided.
 func Set(key string, value string) error {
 	return envMapper.Set(key, value)

+ 10 - 0
pkg/filter/allcut.go

@@ -0,0 +1,10 @@
+package filter
+
+// AllCut is a filter that matches nothing. This is useful
+// for applications like authorization, where a user/group/role may be disallowed
+// from viewing data entirely.
+type AllCut[T any] struct{}
+
+func (ac AllCut[T]) String() string { return "(AllCut)" }
+
+func (ac AllCut[T]) Matches(T) bool { return false }

+ 9 - 0
pkg/filter/allpass.go

@@ -0,0 +1,9 @@
+package filter
+
+// AllPass is a filter that matches everything and is the same as no filter. It is implemented here as a guard
+// against universal operations occurring in the absence of filters.
+type AllPass[T any] struct{}
+
+func (n AllPass[T]) String() string { return "(AllPass)" }
+
+func (n AllPass[T]) Matches(T) bool { return true }

+ 36 - 0
pkg/filter/and.go

@@ -0,0 +1,36 @@
+package filter
+
+import (
+	"fmt"
+)
+
+// And is a set of filters that should be evaluated as a logical
+// AND.
+type And[T any] struct {
+	Filters []Filter[T]
+}
+
+func (a And[T]) String() string {
+	s := "(and"
+	for _, f := range a.Filters {
+		s += fmt.Sprintf(" %s", f)
+	}
+
+	s += ")"
+	return s
+}
+
+func (a And[T]) Matches(that T) bool {
+	filters := a.Filters
+	if len(filters) == 0 {
+		return true
+	}
+
+	for _, filter := range filters {
+		if !filter.Matches(that) {
+			return false
+		}
+	}
+
+	return true
+}

+ 20 - 0
pkg/filter/filter.go

@@ -0,0 +1,20 @@
+package filter
+
+// Filter represents anything that can be used to filter given generic type T.
+//
+// Implement this interface with caution. While it is generic, it
+// is intended to be introspectable so query handlers can perform various
+// optimizations. These optimizations include:
+// - Routing a query to the most optimal cache
+// - Querying backing data stores efficiently (e.g. translation to SQL)
+//
+// Custom implementations of this interface outside of this package should not
+// expect to receive these benefits. Passing a custom implementation to a
+// handler may in errors.
+type Filter[T any] interface {
+	String() string
+
+	// Matches is the canonical in-Go function for determining if T
+	// matches a filter.
+	Matches(T) bool
+}

+ 1073 - 0
pkg/filter/filter_test.go

@@ -0,0 +1,1073 @@
+package filter_test
+
+import (
+	"testing"
+
+	"github.com/opencost/opencost/pkg/filter"
+	"github.com/opencost/opencost/pkg/kubecost"
+)
+
+func Test_String_Matches(t *testing.T) {
+	cases := []struct {
+		name   string
+		a      *kubecost.Allocation
+		filter filter.Filter[*kubecost.Allocation]
+
+		expected bool
+	}{
+		{
+			name: "ClusterID Equals -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationClusterProp,
+				Op:    filter.StringEquals,
+				Value: "cluster-one",
+			},
+
+			expected: true,
+		},
+		{
+			name: "ClusterID StartsWith -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationClusterProp,
+				Op:    filter.StringStartsWith,
+				Value: "cluster",
+			},
+
+			expected: true,
+		},
+		{
+			name: "ClusterID StartsWith -> false",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Cluster: "k8s-one",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationClusterProp,
+				Op:    filter.StringStartsWith,
+				Value: "cluster",
+			},
+
+			expected: false,
+		},
+		{
+			name: "ClusterID empty StartsWith '' -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Cluster: "",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationClusterProp,
+				Op:    filter.StringStartsWith,
+				Value: "",
+			},
+
+			expected: true,
+		},
+		{
+			name: "ClusterID nonempty StartsWith '' -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Cluster: "abc",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationClusterProp,
+				Op:    filter.StringStartsWith,
+				Value: "",
+			},
+
+			expected: true,
+		},
+		{
+			name: "Node Equals -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Node: "node123",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationNodeProp,
+				Op:    filter.StringEquals,
+				Value: "node123",
+			},
+
+			expected: true,
+		},
+		{
+			name: "Namespace Equals Unallocated -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationNamespaceProp,
+				Op:    filter.StringEquals,
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: true,
+		},
+		{
+			name: "ControllerKind Equals -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					ControllerKind: "deployment", // We generally store controller kinds as all lowercase
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationControllerKindProp,
+				Op:    filter.StringEquals,
+				Value: "deployment",
+			},
+
+			expected: true,
+		},
+		{
+			name: "ControllerName Equals -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Controller: "kc-cost-analyzer",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationControllerProp,
+				Op:    filter.StringEquals,
+				Value: "kc-cost-analyzer",
+			},
+
+			expected: true,
+		},
+		{
+			name: "Pod (with UID) Equals -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Pod: "pod-123 UID-ABC",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationPodProp,
+				Op:    filter.StringEquals,
+				Value: "pod-123 UID-ABC",
+			},
+
+			expected: true,
+		},
+		{
+			name: "Container Equals -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Container: "cost-model",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationContainerProp,
+				Op:    filter.StringEquals,
+				Value: "cost-model",
+			},
+
+			expected: true,
+		},
+		{
+			name: `namespace unallocated -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationNamespaceProp,
+				Op:    filter.StringEquals,
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: true,
+		},
+	}
+
+	for _, c := range cases {
+		result := c.filter.Matches(c.a)
+
+		if result != c.expected {
+			t.Errorf("%s: expected %t, got %t", c.name, c.expected, result)
+		}
+	}
+}
+
+func Test_StringSlice_Matches(t *testing.T) {
+	cases := []struct {
+		name   string
+		a      *kubecost.Allocation
+		filter filter.Filter[*kubecost.Allocation]
+
+		expected bool
+	}{
+		{
+			name: `services contains -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: "serv2",
+			},
+
+			expected: true,
+		},
+		{
+			name: `services contains -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: "serv3",
+			},
+
+			expected: false,
+		},
+		{
+			name: `services contains unallocated -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: false,
+		},
+		{
+			name: `services contains unallocated -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: true,
+		},
+	}
+
+	for _, c := range cases {
+		result := c.filter.Matches(c.a)
+
+		if result != c.expected {
+			t.Errorf("%s: expected %t, got %t", c.name, c.expected, result)
+		}
+	}
+}
+
+func Test_StringMap_Matches(t *testing.T) {
+	cases := []struct {
+		name   string
+		a      *kubecost.Allocation
+		filter filter.Filter[*kubecost.Allocation]
+
+		expected bool
+	}{
+		{
+			name: `label[app]="foo" -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "foo",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationLabelProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: "foo",
+			},
+
+			expected: true,
+		},
+		{
+			name: `label[app]="foo" -> different value -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "bar",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationLabelProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: "foo",
+			},
+
+			expected: false,
+		},
+		{
+			name: `label[app]="foo" -> label missing -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"someotherlabel": "someothervalue",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationLabelProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: "foo",
+			},
+
+			expected: false,
+		},
+		{
+			name: `label[app]=Unallocated -> label missing -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"someotherlabel": "someothervalue",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationLabelProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: true,
+		},
+		{
+			name: `label[app]=Unallocated -> label present -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "test",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationLabelProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: false,
+		},
+		{
+			name: `annotation[prom_modified_name]="testing123" -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"prom_modified_name": "testing123",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationAnnotationProp,
+				Op:    filter.StringMapEquals,
+				Key:   "prom_modified_name",
+				Value: "testing123",
+			},
+
+			expected: true,
+		},
+		{
+			name: `annotation[app]="foo" -> different value -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"app": "bar",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationAnnotationProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: "foo",
+			},
+
+			expected: false,
+		},
+		{
+			name: `annotation[app]="foo" -> annotation missing -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"someotherannotation": "someothervalue",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationAnnotationProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: "foo",
+			},
+
+			expected: false,
+		},
+	}
+
+	for _, c := range cases {
+		result := c.filter.Matches(c.a)
+
+		if result != c.expected {
+			t.Errorf("%s: expected %t, got %t", c.name, c.expected, result)
+		}
+	}
+}
+
+func Test_Not_Matches(t *testing.T) {
+	cases := []struct {
+		name   string
+		a      *kubecost.Allocation
+		filter filter.Filter[*kubecost.Allocation]
+
+		expected bool
+	}{
+		{
+			name: "Namespace NotEquals -> false",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kube-system",
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationNamespaceProp,
+					Op:    filter.StringEquals,
+					Value: "kube-system",
+				},
+			},
+
+			expected: false,
+		},
+		{
+			name: "Namespace NotEquals Unallocated -> true",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kube-system",
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationNamespaceProp,
+					Op:    filter.StringEquals,
+					Value: kubecost.UnallocatedSuffix,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "Namespace NotEquals Unallocated -> false",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "",
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationNamespaceProp,
+					Op:    filter.StringEquals,
+					Value: kubecost.UnallocatedSuffix,
+				},
+			},
+
+			expected: false,
+		},
+
+		{
+			name: `label[app]!=Unallocated -> label missing -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"someotherlabel": "someothervalue",
+					},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationLabelProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: kubecost.UnallocatedSuffix,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: `label[app]!=Unallocated -> label present -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "test",
+					},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationLabelProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: kubecost.UnallocatedSuffix,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: `label[app]!="foo" -> label missing -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"someotherlabel": "someothervalue",
+					},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationLabelProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: "foo",
+				},
+			},
+
+			expected: true,
+		},
+		{
+			name: `annotation[prom_modified_name]="testing123" -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"prom_modified_name": "testing123",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationAnnotationProp,
+				Op:    filter.StringMapEquals,
+				Key:   "prom_modified_name",
+				Value: "testing123",
+			},
+
+			expected: true,
+		},
+		{
+			name: `annotation[app]="foo" -> different value -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"app": "bar",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationAnnotationProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: "foo",
+			},
+
+			expected: false,
+		},
+		{
+			name: `annotation[app]="foo" -> annotation missing -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"someotherannotation": "someothervalue",
+					},
+				},
+			},
+			filter: filter.StringMapProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationAnnotationProp,
+				Op:    filter.StringMapEquals,
+				Key:   "app",
+				Value: "foo",
+			},
+
+			expected: false,
+		},
+		{
+			name: `annotation[app]!="foo" -> annotation missing -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"someotherannotation": "someothervalue",
+					},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationAnnotationProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: "foo",
+				},
+			},
+
+			expected: true,
+		},
+		{
+			name: `namespace unallocated -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "",
+				},
+			},
+			filter: filter.StringProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationNamespaceProp,
+				Op:    filter.StringEquals,
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: true,
+		},
+		{
+			name: `services contains -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: "serv2",
+			},
+
+			expected: true,
+		},
+		{
+			name: `services contains -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: "serv3",
+			},
+
+			expected: false,
+		},
+		{
+			name: `services notcontains -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringSliceProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationServiceProp,
+					Op:    filter.StringSliceContains,
+					Value: "serv3",
+				},
+			},
+			expected: true,
+		},
+		{
+			name: `services notcontains -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringSliceProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationServiceProp,
+					Op:    filter.StringSliceContains,
+					Value: "serv2",
+				},
+			},
+
+			expected: false,
+		},
+		{
+			name: `services notcontains unallocated -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringSliceProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationServiceProp,
+					Op:    filter.StringSliceContains,
+					Value: kubecost.UnallocatedSuffix,
+				},
+			},
+
+			expected: true,
+		},
+		{
+			name: `services notcontains unallocated -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{},
+				},
+			},
+			filter: filter.Not[*kubecost.Allocation]{
+				Filter: filter.StringSliceProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationServiceProp,
+					Op:    filter.StringSliceContains,
+					Value: kubecost.UnallocatedSuffix,
+				},
+			},
+
+			expected: false,
+		},
+		{
+			name: `services containsprefix -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContainsPrefix,
+				Value: "serv",
+			},
+
+			expected: true,
+		},
+		{
+			name: `services containsprefix -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"foo", "bar"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContainsPrefix,
+				Value: "serv",
+			},
+
+			expected: false,
+		},
+		{
+			name: `services contains unallocated -> false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: false,
+		},
+		{
+			name: `services contains unallocated -> true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{},
+				},
+			},
+			filter: filter.StringSliceProperty[*kubecost.Allocation]{
+				Field: kubecost.AllocationServiceProp,
+				Op:    filter.StringSliceContains,
+				Value: kubecost.UnallocatedSuffix,
+			},
+
+			expected: true,
+		},
+	}
+
+	for _, c := range cases {
+		result := c.filter.Matches(c.a)
+
+		if result != c.expected {
+			t.Errorf("%s: expected %t, got %t", c.name, c.expected, result)
+		}
+	}
+}
+
+func Test_None_Matches(t *testing.T) {
+	cases := []struct {
+		name string
+		a    *kubecost.Allocation
+	}{
+		{
+			name: "nil",
+			a:    nil,
+		},
+		{
+			name: "nil properties",
+			a: &kubecost.Allocation{
+				Properties: nil,
+			},
+		},
+		{
+			name: "empty properties",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{},
+			},
+		},
+		{
+			name: "ClusterID",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Cluster: "cluster-one",
+				},
+			},
+		},
+		{
+			name: "Node",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Node: "node123",
+				},
+			},
+		},
+		{
+			name: "Namespace",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kube-system",
+				},
+			},
+		},
+		{
+			name: "ControllerKind",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					ControllerKind: "deployment", // We generally store controller kinds as all lowercase
+				},
+			},
+		},
+		{
+			name: "ControllerName",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Controller: "kc-cost-analyzer",
+				},
+			},
+		},
+		{
+			name: "Pod",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Pod: "pod-123 UID-ABC",
+				},
+			},
+		},
+		{
+			name: "Container",
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Container: "cost-model",
+				},
+			},
+		},
+		{
+			name: `label`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Labels: map[string]string{
+						"app": "foo",
+					},
+				},
+			},
+		},
+		{
+			name: `annotation`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Annotations: map[string]string{
+						"prom_modified_name": "testing123",
+					},
+				},
+			},
+		},
+		{
+			name: `services`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Services: []string{"serv1", "serv2"},
+				},
+			},
+		},
+	}
+
+	for _, c := range cases {
+		result := filter.AllCut[*kubecost.Allocation]{}.Matches(c.a)
+
+		if result {
+			t.Errorf("%s: should have been rejected", c.name)
+		}
+	}
+}
+
+func Test_And_Matches(t *testing.T) {
+	cases := []struct {
+		name   string
+		a      *kubecost.Allocation
+		filter filter.Filter[*kubecost.Allocation]
+
+		expected bool
+	}{
+		{
+			name: `label[app]="foo" and namespace="kubecost" -> both true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kubecost",
+					Labels: map[string]string{
+						"app": "foo",
+					},
+				},
+			},
+			filter: filter.And[*kubecost.Allocation]{[]filter.Filter[*kubecost.Allocation]{
+				filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationLabelProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: "foo",
+				},
+				filter.StringProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationNamespaceProp,
+					Op:    filter.StringEquals,
+					Value: "kubecost",
+				},
+			}},
+			expected: true,
+		},
+		{
+			name: `label[app]="foo" and namespace="kubecost" -> first true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kube-system",
+					Labels: map[string]string{
+						"app": "foo",
+					},
+				},
+			},
+			filter: filter.And[*kubecost.Allocation]{[]filter.Filter[*kubecost.Allocation]{
+				filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationLabelProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: "foo",
+				},
+				filter.StringProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationNamespaceProp,
+					Op:    filter.StringEquals,
+					Value: "kubecost",
+				},
+			}},
+			expected: false,
+		},
+		{
+			name: `label[app]="foo" and namespace="kubecost" -> second true`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kubecost",
+					Labels: map[string]string{
+						"app": "bar",
+					},
+				},
+			},
+			filter: filter.And[*kubecost.Allocation]{[]filter.Filter[*kubecost.Allocation]{
+				filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationLabelProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: "foo",
+				},
+				filter.StringProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationNamespaceProp,
+					Op:    filter.StringEquals,
+					Value: "kubecost",
+				},
+			}},
+			expected: false,
+		},
+		{
+			name: `label[app]="foo" and namespace="kubecost" -> both false`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kube-system",
+					Labels: map[string]string{
+						"app": "bar",
+					},
+				},
+			},
+			filter: filter.And[*kubecost.Allocation]{[]filter.Filter[*kubecost.Allocation]{
+				filter.StringMapProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationLabelProp,
+					Op:    filter.StringMapEquals,
+					Key:   "app",
+					Value: "foo",
+				},
+				filter.StringProperty[*kubecost.Allocation]{
+					Field: kubecost.AllocationNamespaceProp,
+					Op:    filter.StringEquals,
+					Value: "kubecost",
+				},
+			}},
+			expected: false,
+		},
+		{
+			name: `(and none) matches nothing`,
+			a: &kubecost.Allocation{
+				Properties: &kubecost.AllocationProperties{
+					Namespace: "kube-system",
+					Labels: map[string]string{
+						"app": "bar",
+					},
+				},
+			},
+			filter: filter.And[*kubecost.Allocation]{[]filter.Filter[*kubecost.Allocation]{
+				filter.AllCut[*kubecost.Allocation]{},
+			}},
+			expected: false,
+		},
+	}
+
+	for _, c := range cases {
+		result := c.filter.Matches(c.a)
+
+		if result != c.expected {
+			t.Errorf("%s: expected %t, got %t", c.name, c.expected, result)
+		}
+	}
+}

+ 17 - 0
pkg/filter/not.go

@@ -0,0 +1,17 @@
+package filter
+
+import "fmt"
+
+// Not negates any filter contained within it
+type Not[T any] struct {
+	Filter Filter[T]
+}
+
+func (n Not[T]) String() string {
+	return fmt.Sprintf("(not %s)", n.Filter.String())
+}
+
+// Matches inverts the result of the child filter
+func (n Not[T]) Matches(that T) bool {
+	return !n.Filter.Matches(that)
+}

+ 36 - 0
pkg/filter/or.go

@@ -0,0 +1,36 @@
+package filter
+
+import (
+	"fmt"
+)
+
+// Or is a set of filters that should be evaluated as a logical
+// OR.
+type Or[T any] struct {
+	Filters []Filter[T]
+}
+
+func (o Or[T]) String() string {
+	s := "(or"
+	for _, f := range o.Filters {
+		s += fmt.Sprintf(" %s", f)
+	}
+
+	s += ")"
+	return s
+}
+
+func (o Or[T]) Matches(that T) bool {
+	filters := o.Filters
+	if len(filters) == 0 {
+		return true
+	}
+
+	for _, filter := range filters {
+		if filter.Matches(that) {
+			return true
+		}
+	}
+
+	return false
+}

+ 83 - 0
pkg/filter/stringmapproperty.go

@@ -0,0 +1,83 @@
+package filter
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/opencost/opencost/pkg/log"
+)
+
+const unallocatedSuffix = "__unallocated__"
+
+type StringMapPropertied interface {
+	StringMapProperty(string) (map[string]string, error)
+}
+
+// StringMapOperation is an enum that represents operations that can be performed
+// when filtering (equality, inequality, etc.)
+type StringMapOperation string
+
+const (
+	// StringMapHasKey passes if the map has the provided key
+	StringMapHasKey StringMapOperation = "stringmapcontains"
+
+	StringMapStartsWith = "stringmapstartswith"
+
+	// StringMapEquals when the given key and value match
+	StringMapEquals = "stringmapequals"
+)
+
+// StringMapProperty is the lowest-level type of filter. It represents
+// a filter operation (equality, inequality, etc.) on a property that contains a string map
+type StringMapProperty[T StringMapPropertied] struct {
+	Field string
+	Op    StringMapOperation
+	Key   string
+	Value string
+}
+
+func (smp StringMapProperty[T]) String() string {
+	return fmt.Sprintf(`(%s %s[%s] "%s")`, smp.Op, smp.Field, smp.Key, smp.Value)
+}
+
+func (smp StringMapProperty[T]) Matches(that T) bool {
+
+	thatMap, err := that.StringMapProperty(smp.Field)
+	if err != nil {
+		log.Errorf("Filter: StringMapProperty: could not retrieve field %s: %s", smp.Field, err.Error())
+		return false
+	}
+
+	valueToCompare, keyIsPresent := thatMap[smp.Key]
+
+	switch smp.Op {
+	case StringMapHasKey:
+		return keyIsPresent
+	case StringMapEquals:
+		// namespace:"__unallocated__" should match a.Properties.Namespace = ""
+		// label[app]:"__unallocated__" should match _, ok := Labels[app]; !ok
+		if !keyIsPresent || valueToCompare == "" {
+			return smp.Value == unallocatedSuffix
+		}
+
+		if valueToCompare == smp.Value {
+			return true
+		}
+
+	case StringMapStartsWith:
+		if !keyIsPresent {
+			return false
+		}
+
+		// We don't need special __unallocated__ logic here because a query
+		// asking for "__unallocated__" won't have a wildcard and unallocated
+		// properties are the empty string.
+
+		return strings.HasPrefix(valueToCompare, smp.Value)
+	default:
+		log.Errorf("Filter: StringMapProperty: Unhandled filter op. This is a filter implementation error and requires immediate patching. Op: %s", smp.Op)
+		return false
+	}
+
+	return false
+}

+ 83 - 0
pkg/filter/stringproperty.go

@@ -0,0 +1,83 @@
+package filter
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/opencost/opencost/pkg/log"
+)
+
+// StringPropertied is used to validate the name of a property field and return its value
+type StringPropertied interface {
+	// StringProperty acts as a validator and getter for a structs string properties
+	StringProperty(string) (string, error)
+}
+
+// StringOperation is an enum that represents operations that can be performed
+// when filtering (equality, inequality, etc.)
+type StringOperation string
+
+// If you add a FilterOp, MAKE SURE TO UPDATE ALL FILTER IMPLEMENTATIONS! Go
+// does not enforce exhaustive pattern matching on "enum" types.
+const (
+	// StringEquals is the equality operator
+	// "kube-system" FilterEquals "kube-system" = true
+	// "kube-syste" FilterEquals "kube-system" = false
+	StringEquals StringOperation = "stringequals"
+
+	// StringStartsWith matches strings with the given prefix.
+	// "kube-system" StartsWith "kube" = true
+	//
+	// When comparing with a field represented by an array/slice, this is like
+	// applying FilterContains to every element of the slice.
+	StringStartsWith = "stringstartswith"
+)
+
+// StringProperty is the lowest-level type of filter. It represents
+// a filter operation (equality, inequality, etc.) on a field with a string value (namespace,
+// node, pod, etc.).
+type StringProperty[T StringPropertied] struct {
+	Field string
+	Op    StringOperation
+
+	// Value is for _all_ filters. A filter of 'namespace:"kubecost"' has
+	// Value="kubecost"
+	Value string
+}
+
+func (sp StringProperty[T]) String() string {
+	return fmt.Sprintf(`(%s %s "%s")`, sp.Op, sp.Field, sp.Value)
+}
+
+func (sp StringProperty[T]) Matches(that T) bool {
+
+	thatString, err := that.StringProperty(sp.Field)
+	if err != nil {
+		log.Errorf("Filter: StringProperty: could not retrieve field %s: %s", sp.Field, err.Error())
+		return false
+	}
+
+	switch sp.Op {
+	case StringEquals:
+		// namespace:"__unallocated__" should match a.Properties.Namespace = ""
+		if thatString == "" {
+			return sp.Value == unallocatedSuffix
+		}
+
+		if thatString == sp.Value {
+			return true
+		}
+	case StringStartsWith:
+
+		// We don't need special __unallocated__ logic here because a query
+		// asking for "__unallocated__" won't have a wildcard and unallocated
+		// properties are the empty string.
+
+		return strings.HasPrefix(thatString, sp.Value)
+	default:
+		log.Errorf("Filter: StringProperty: Unhandled filter op. This is a filter implementation error and requires immediate patching. Op: %s", sp.Op)
+		return false
+	}
+
+	return false
+}

+ 80 - 0
pkg/filter/stringsliceproperty.go

@@ -0,0 +1,80 @@
+package filter
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/opencost/opencost/pkg/log"
+)
+
+type StringSlicePropertied interface {
+	StringSliceProperty(string) ([]string, error)
+}
+
+// StringSliceOperation is an enum that represents operations that can be performed
+// when filtering (equality, inequality, etc.)
+type StringSliceOperation string
+
+const (
+	// StringSliceContains is an array/slice membership operator
+	// ["a", "b", "c"] FilterContains "a" = true
+	StringSliceContains StringSliceOperation = "stringslicecontains"
+
+	// StringSliceContainsPrefix is like FilterContains, but using StartsWith instead
+	// of Equals.
+	// ["kube-system", "abc123"] ContainsPrefix ["kube"] = true
+	StringSliceContainsPrefix = "stringslicecontainsprefix"
+)
+
+// StringSliceProperty is the lowest-level type of filter. It represents
+// a filter operation (equality, inequality, etc.) on a property that contains a string slice
+type StringSliceProperty[T StringSlicePropertied] struct {
+	Field string
+	Op    StringSliceOperation
+
+	Value string
+}
+
+func (ssp StringSliceProperty[T]) String() string {
+	return fmt.Sprintf(`(%s %s "%s")`, ssp.Op, ssp.Field, ssp.Value)
+}
+
+func (ssp StringSliceProperty[T]) Matches(that T) bool {
+
+	thatSlice, err := that.StringSliceProperty(ssp.Field)
+	if err != nil {
+		log.Errorf("Filter: StringSliceProperty: could not retrieve field %s: %s", ssp.Field, err.Error())
+		return false
+	}
+
+	switch ssp.Op {
+
+	case StringSliceContains:
+		if len(thatSlice) == 0 {
+			return ssp.Value == unallocatedSuffix
+		}
+
+		for _, s := range thatSlice {
+			if s == ssp.Value {
+				return true
+			}
+		}
+	case StringSliceContainsPrefix:
+		// We don't need special __unallocated__ logic here because a query
+		// asking for "__unallocated__" won't have a wildcard and unallocated
+		// properties are the empty string.
+
+		for _, s := range thatSlice {
+			if strings.HasPrefix(s, ssp.Value) {
+				return true
+			}
+		}
+
+		return false
+	default:
+		log.Errorf("Filter: StringSliceProperty: Unhandled filter op. This is a filter implementation error and requires immediate patching. Op: %s", ssp.Op)
+		return false
+	}
+
+	return false
+}

+ 70 - 0
pkg/filter/util/cloudcostaggregate.go

@@ -0,0 +1,70 @@
+package util
+
+import (
+	"strings"
+
+	"github.com/opencost/opencost/pkg/filter"
+	"github.com/opencost/opencost/pkg/kubecost"
+	"github.com/opencost/opencost/pkg/util/mapper"
+)
+
+func parseWildcardEnd(rawFilterValue string) (string, bool) {
+	return strings.TrimSuffix(rawFilterValue, "*"), strings.HasSuffix(rawFilterValue, "*")
+}
+
+func CloudCostAggregateFilterFromParams(pmr mapper.PrimitiveMapReader) filter.Filter[*kubecost.CloudCostAggregate] {
+	filter := filter.And[*kubecost.CloudCostAggregate]{
+		Filters: []filter.Filter[*kubecost.CloudCostAggregate]{},
+	}
+
+	if raw := pmr.GetList("filterAccounts", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.CloudCostAccountProp))
+	}
+
+	if raw := pmr.GetList("filterProjects", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.CloudCostProjectProp))
+	}
+
+	if raw := pmr.GetList("filterProviders", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.CloudCostProviderProp))
+	}
+
+	if raw := pmr.GetList("filterServices", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.CloudCostServiceProp))
+	}
+
+	if raw := pmr.GetList("filterLabelValues", ","); len(raw) > 0 {
+		filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.CloudCostLabelProp))
+	}
+
+	if len(filter.Filters) == 0 {
+		return nil
+	}
+
+	return filter
+}
+
+func filterV1SingleValueFromList(rawFilterValues []string, field string) filter.Filter[*kubecost.CloudCostAggregate] {
+	result := filter.Or[*kubecost.CloudCostAggregate]{
+		Filters: []filter.Filter[*kubecost.CloudCostAggregate]{},
+	}
+
+	for _, filterValue := range rawFilterValues {
+		filterValue = strings.TrimSpace(filterValue)
+		filterValue, wildcard := parseWildcardEnd(filterValue)
+
+		subFilter := filter.StringProperty[*kubecost.CloudCostAggregate]{
+			Field: field,
+			Op:    filter.StringEquals,
+			Value: filterValue,
+		}
+
+		if wildcard {
+			subFilter.Op = kubecost.FilterStartsWith
+		}
+
+		result.Filters = append(result.Filters, subFilter)
+	}
+
+	return result
+}

+ 40 - 0
pkg/filter/window.go

@@ -0,0 +1,40 @@
+package filter
+
+//
+//import (
+//	"fmt"
+//	"github.com/opencost/opencost/pkg/kubecost"
+//	"github.com/opencost/opencost/pkg/log"
+//)
+//
+//type Windowed interface {
+//	GetWindow() kubecost.Window
+//}
+//
+//// WindowOperation are operations that can be performed on types that have windows
+//type WindowOperation string
+//
+//const (
+//	WindowContains WindowOperation = "windowcontains"
+//)
+//
+//// WindowCondition is a filter can be used on any type that has a window and implements GetWindow()
+//type WindowCondition[T Windowed] struct {
+//	Window kubecost.Window
+//	Op     WindowOperation
+//}
+//
+//func (wc WindowCondition[T]) String() string {
+//	return fmt.Sprintf(`(%s "%s")`, wc.Op, wc.Window.String())
+//}
+//
+//func (wc WindowCondition[T]) Matches(that T) bool {
+//	thatWindow := that.GetWindow()
+//	switch wc.Op {
+//	case WindowContains:
+//		return wc.Window.ContainsWindow(thatWindow)
+//	default:
+//		log.Errorf("Filter: Window: Unhandled filter operation. This is a filter implementation error and requires immediate patching. Op: %s", wc.Op)
+//		return false
+//	}
+//}

+ 112 - 0
pkg/filter/window_test.go

@@ -0,0 +1,112 @@
+package filter_test
+
+// import (
+// 	"github.com/opencost/opencost/pkg/kubecost"
+// 	"testing"
+// 	"time"
+// )
+
+// type windowedImpl struct {
+// 	kubecost.Window
+// }
+
+// func (w *windowedImpl) GetWindow() kubecost.Window {
+// 	return w.Window
+// }
+
+// func newWindowedImpl(start, end *time.Time) *windowedImpl {
+// 	return &windowedImpl{kubecost.NewWindow(start, end)}
+// }
+
+// func Test_WindowContains_Matches(t *testing.T) {
+// 	noon := time.Date(2022, 9, 29, 12, 0, 0, 0, time.UTC)
+// 	one := noon.Add(time.Hour)
+// 	two := one.Add(time.Hour)
+// 	three := two.Add(time.Hour)
+// 	cases := map[string]struct {
+// 		windowed *windowedImpl
+// 		filter   Filter[*windowedImpl]
+// 		expected bool
+// 	}{
+// 		"fully contains": {
+// 			windowed: newWindowedImpl(&one, &two),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&noon, &three),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: true,
+// 		},
+// 		"window matches": {
+// 			windowed: newWindowedImpl(&one, &two),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&one, &two),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: true,
+// 		},
+// 		"contains start": {
+// 			windowed: newWindowedImpl(&one, &three),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&noon, &two),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: false,
+// 		},
+// 		"contains end": {
+// 			windowed: newWindowedImpl(&noon, &two),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&one, &three),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: false,
+// 		},
+// 		"window start = filter end": {
+// 			windowed: newWindowedImpl(&one, &two),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&noon, &one),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: false,
+// 		},
+// 		"window end = filter start": {
+// 			windowed: newWindowedImpl(&noon, &one),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&one, &two),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: false,
+// 		},
+// 		"window before": {
+// 			windowed: newWindowedImpl(&noon, &one),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&two, &three),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: false,
+// 		},
+// 		"window after": {
+// 			windowed: newWindowedImpl(&two, &three),
+// 			filter: WindowCondition[*windowedImpl]{
+// 				Window: kubecost.NewWindow(&noon, &one),
+// 				Op:     WindowContains,
+// 			},
+
+// 			expected: false,
+// 		},
+// 	}
+
+// 	for name, c := range cases {
+// 		result := c.filter.Matches(c.windowed)
+
+// 		if result != c.expected {
+// 			t.Errorf("%s: expected %t, got %t", name, c.expected, result)
+// 		}
+// 	}
+// }

+ 82 - 1
pkg/kubecost/allocation.go

@@ -237,6 +237,11 @@ func (pva *PVAllocation) Equal(that *PVAllocation) bool {
 		util.IsApproximately(pva.Cost, that.Cost)
 }
 
+// GetWindow returns the window of the struct
+func (a *Allocation) GetWindow() Window {
+	return a.Window
+}
+
 // AllocationMatchFunc is a function that can be used to match Allocations by
 // returning true for any given Allocation if a condition is met.
 type AllocationMatchFunc func(*Allocation) bool
@@ -1628,6 +1633,82 @@ func (a *Allocation) generateKey(aggregateBy []string, labelConfig *LabelConfig)
 	return a.Properties.GenerateKey(aggregateBy, labelConfig)
 }
 
+func (a *Allocation) StringProperty(property string) (string, error) {
+	switch property {
+	case AllocationClusterProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.Cluster, nil
+	case AllocationNodeProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.Node, nil
+	case AllocationContainerProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.Container, nil
+	case AllocationControllerProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.Controller, nil
+	case AllocationControllerKindProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.ControllerKind, nil
+	case AllocationNamespaceProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.Namespace, nil
+	case AllocationPodProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.Pod, nil
+	case AllocationProviderIDProp:
+		if a.Properties == nil {
+			return "", nil
+		}
+		return a.Properties.ProviderID, nil
+	default:
+		return "", fmt.Errorf("Allocation: StringProperty: invalid property name: %s", property)
+	}
+}
+
+func (a *Allocation) StringSliceProperty(property string) ([]string, error) {
+	switch property {
+	case AllocationServiceProp:
+		if a.Properties == nil {
+			return nil, nil
+		}
+		return a.Properties.Services, nil
+	default:
+		return nil, fmt.Errorf("Allocation: StringSliceProperty: invalid property name: %s", property)
+	}
+}
+
+func (a *Allocation) StringMapProperty(property string) (map[string]string, error) {
+	switch property {
+	case AllocationLabelProp:
+		if a.Properties == nil {
+			return nil, nil
+		}
+		return a.Properties.Labels, nil
+	case AllocationAnnotationProp:
+		if a.Properties == nil {
+			return nil, nil
+		}
+		return a.Properties.Annotations, nil
+	default:
+		return nil, fmt.Errorf("Allocation: StringMapProperty: invalid property name: %s", property)
+	}
+}
+
 // Clone returns a new AllocationSet with a deep copy of the given
 // AllocationSet's allocations.
 func (as *AllocationSet) Clone() *AllocationSet {
@@ -1812,7 +1893,7 @@ func (as *AllocationSet) IsEmpty() bool {
 		return true
 	}
 
-	return as.Allocations == nil || len(as.Allocations) == 0
+	return false
 }
 
 // Length returns the number of Allocations in the set

+ 19 - 1
pkg/kubecost/bingen.go

@@ -22,6 +22,8 @@ package kubecost
 
 // Default Version Set (uses -version flag passed) includes shared resources
 // @bingen:generate:Window
+// @bingen:generate:Coverage
+// @bingen:generate:CoverageSet
 
 // Asset Version Set: Includes Asset pipeline specific resources
 // @bingen:set[name=Assets,version=18]
@@ -71,4 +73,20 @@ package kubecost
 // @bingen:generate:AuditSetRange
 // @bingen:end
 
-//go:generate bingen -package=kubecost -version=15 -buffer=github.com/opencost/opencost/pkg/util
+// @bingen:set[name=CloudCostAggregate,version=1]
+// @bingen:generate:CloudCostAggregate
+// @bingen:generate[stringtable]:CloudCostAggregateSet
+// @bingen:generate:CloudCostAggregateSetRange
+// @bingen:generate:CloudCostAggregateProperties
+// @bingen:generate:CloudCostAggregateLabels
+// @bingen:end
+
+// @bingen:set[name=CloudCostItem,version=1]
+// @bingen:generate:CloudCostItem
+// @bingen:generate[stringtable]:CloudCostItemSet
+// @bingen:generate:CloudCostItemSetRange
+// @bingen:generate:CloudCostItemProperties
+// @bingen:generate:CloudCostItemLabels
+// @bingen:end
+
+//go:generate bingen -package=kubecost -version=17 -buffer=github.com/opencost/opencost/pkg/util

+ 422 - 0
pkg/kubecost/cloudcostaggregate.go

@@ -0,0 +1,422 @@
+package kubecost
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/opencost/opencost/pkg/filter"
+	"github.com/opencost/opencost/pkg/log"
+)
+
+const (
+	CloudCostAccountProp  string = "account"
+	CloudCostProjectProp  string = "project"
+	CloudCostProviderProp string = "provider"
+	CloudCostServiceProp  string = "service"
+	CloudCostLabelProp    string = "label"
+)
+
+// CloudCostAggregateProperties unique property set for CloudCostAggregate within a window
+type CloudCostAggregateProperties struct {
+	Provider   string `json:"provider"`
+	Account    string `json:"account"`
+	Project    string `json:"project"`
+	Service    string `json:"service"`
+	LabelValue string `json:"label"`
+}
+
+func (ccap CloudCostAggregateProperties) Equal(that CloudCostAggregateProperties) bool {
+	return ccap.Provider == that.Provider &&
+		ccap.Account == that.Account &&
+		ccap.Project == that.Project &&
+		ccap.Service == that.Service &&
+		ccap.LabelValue == that.LabelValue
+}
+
+func (ccap CloudCostAggregateProperties) Key(props []string) string {
+	if len(props) == 0 {
+		return fmt.Sprintf("%s/%s/%s/%s/%s", ccap.Provider, ccap.Account, ccap.Project, ccap.Service, ccap.LabelValue)
+	}
+
+	keys := make([]string, len(props))
+	for i, prop := range props {
+		key := UnallocatedSuffix
+
+		switch prop {
+		case CloudCostProviderProp:
+			if ccap.Provider != "" {
+				key = ccap.Provider
+			}
+		case CloudCostAccountProp:
+			if ccap.Account != "" {
+				key = ccap.Account
+			}
+		case CloudCostProjectProp:
+			if ccap.Project != "" {
+				key = ccap.Project
+			}
+		case CloudCostServiceProp:
+			if ccap.Service != "" {
+				key = ccap.Service
+			}
+		case CloudCostLabelProp:
+			if ccap.LabelValue != "" {
+				key = ccap.LabelValue
+			}
+		}
+
+		keys[i] = key
+	}
+
+	return strings.Join(keys, "/")
+}
+
+// CloudCostAggregate represents an aggregation of Billing Integration data on the properties listed
+// - KubernetesPercent is the percent of the CloudCostAggregates cost which was from an item which could be identified
+//   as coming from a kubernetes resources.
+// - Cost is the sum of the cost of each item in the CloudCostAggregate
+// - Credit is the sum of credits applied to each item in the CloudCostAggregate
+
+type CloudCostAggregate struct {
+	Properties        CloudCostAggregateProperties `json:"properties"`
+	KubernetesPercent float64                      `json:"kubernetesPercent"`
+	Cost              float64                      `json:"cost"`
+	Credit            float64                      `json:"credit"`
+}
+
+func (cca *CloudCostAggregate) Clone() *CloudCostAggregate {
+	return &CloudCostAggregate{
+		Properties:        cca.Properties,
+		KubernetesPercent: cca.KubernetesPercent,
+		Cost:              cca.Cost,
+		Credit:            cca.Credit,
+	}
+}
+
+func (cca *CloudCostAggregate) Equal(that *CloudCostAggregate) bool {
+	if that == nil {
+		return false
+	}
+
+	return cca.Cost == that.Cost &&
+		cca.Credit == that.Credit &&
+		cca.Properties.Equal(that.Properties)
+}
+
+func (cca *CloudCostAggregate) Key(props []string) string {
+	return cca.Properties.Key(props)
+}
+
+func (cca *CloudCostAggregate) StringProperty(prop string) (string, error) {
+	if cca == nil {
+		return "", nil
+	}
+
+	switch prop {
+	case CloudCostAccountProp:
+		return cca.Properties.Account, nil
+	case CloudCostProjectProp:
+		return cca.Properties.Project, nil
+	case CloudCostProviderProp:
+		return cca.Properties.Provider, nil
+	case CloudCostServiceProp:
+		return cca.Properties.Service, nil
+	case CloudCostLabelProp:
+		return cca.Properties.LabelValue, nil
+	default:
+		return "", fmt.Errorf("invalid property name: %s", prop)
+	}
+}
+
+func (cca *CloudCostAggregate) add(that *CloudCostAggregate) {
+	if cca == nil {
+		log.Warnf("cannot add to nil CloudCostAggregate")
+		return
+	}
+
+	// Compute KubernetesPercent for sum
+	k8sPct := 0.0
+	sumCost := cca.Cost + that.Cost
+	if sumCost > 0.0 {
+		thisK8sCost := cca.Cost * cca.KubernetesPercent
+		thatK8sCost := that.Cost * that.KubernetesPercent
+		k8sPct = (thisK8sCost + thatK8sCost) / sumCost
+	}
+
+	cca.Cost = sumCost
+	cca.Credit += that.Credit
+	cca.KubernetesPercent = k8sPct
+}
+
+type CloudCostAggregateSet struct {
+	CloudCostAggregates   map[string]*CloudCostAggregate `json:"items"`
+	AggregationProperties []string                       `json:"-"`
+	Integration           string                         `json:"-"`
+	LabelName             string                         `json:"labelName,omitempty"`
+	Window                Window                         `json:"window"`
+}
+
+func NewCloudCostAggregateSet(start, end time.Time, cloudCostAggregates ...*CloudCostAggregate) *CloudCostAggregateSet {
+	ccas := &CloudCostAggregateSet{
+		CloudCostAggregates: map[string]*CloudCostAggregate{},
+		Window:              NewWindow(&start, &end),
+	}
+
+	for _, cca := range cloudCostAggregates {
+		ccas.insertByProperty(cca, nil)
+	}
+
+	return ccas
+}
+
+func (ccas *CloudCostAggregateSet) Aggregate(props []string) (*CloudCostAggregateSet, error) {
+	if ccas == nil {
+		return nil, errors.New("cannot aggregate a nil CloudCostAggregateSet")
+	}
+
+	if ccas.Window.IsOpen() {
+		return nil, fmt.Errorf("cannot aggregate a CloudCostAggregateSet with an open window: %s", ccas.Window)
+	}
+
+	// Create a new result set, with the given aggregation property
+	result := NewCloudCostAggregateSet(*ccas.Window.Start(), *ccas.Window.End())
+	result.AggregationProperties = props
+	result.LabelName = ccas.LabelName
+	result.Integration = ccas.Integration
+
+	// Insert clones of each item in the set, keyed by the given property.
+	// The underlying insert logic will add binned items together.
+	for name, cca := range ccas.CloudCostAggregates {
+		ccaClone := cca.Clone()
+		err := result.insertByProperty(ccaClone, props)
+		if err != nil {
+			return nil, fmt.Errorf("error aggregating %s by %v: %s", name, props, err)
+		}
+	}
+
+	return result, nil
+}
+
+func (ccas *CloudCostAggregateSet) Filter(filters filter.Filter[*CloudCostAggregate]) *CloudCostAggregateSet {
+	if ccas == nil {
+		return nil
+	}
+
+	result := ccas.Clone()
+	result.filter(filters)
+
+	return result
+}
+
+func (ccas *CloudCostAggregateSet) filter(filters filter.Filter[*CloudCostAggregate]) {
+	if ccas == nil {
+		return
+	}
+
+	if filters == nil {
+		return
+	}
+
+	for name, cca := range ccas.CloudCostAggregates {
+		if !filters.Matches(cca) {
+			delete(ccas.CloudCostAggregates, name)
+		}
+	}
+}
+
+func (ccas *CloudCostAggregateSet) Insert(that *CloudCostAggregate) error {
+	// Publicly, only allow Inserting as a basic operation (i.e. without causing
+	// an aggregation on a property).
+	return ccas.insertByProperty(that, nil)
+}
+
+func (ccas *CloudCostAggregateSet) insertByProperty(that *CloudCostAggregate, props []string) error {
+	if ccas == nil {
+		return fmt.Errorf("cannot insert into nil CloudCostAggregateSet")
+	}
+
+	if ccas.CloudCostAggregates == nil {
+		ccas.CloudCostAggregates = map[string]*CloudCostAggregate{}
+	}
+
+	// Add the given CloudCostAggregate to the existing entry, if there is one;
+	// otherwise just set directly into allocations
+	if _, ok := ccas.CloudCostAggregates[that.Key(props)]; !ok {
+		ccas.CloudCostAggregates[that.Key(props)] = that
+	} else {
+		ccas.CloudCostAggregates[that.Key(props)].add(that)
+	}
+
+	return nil
+}
+
+func (ccas *CloudCostAggregateSet) Clone() *CloudCostAggregateSet {
+	aggs := make(map[string]*CloudCostAggregate, len(ccas.CloudCostAggregates))
+	for k, v := range ccas.CloudCostAggregates {
+		aggs[k] = v.Clone()
+	}
+
+	return &CloudCostAggregateSet{
+		CloudCostAggregates: aggs,
+		Integration:         ccas.Integration,
+		LabelName:           ccas.LabelName,
+		Window:              ccas.Window.Clone(),
+	}
+}
+
+func (ccas *CloudCostAggregateSet) Equal(that *CloudCostAggregateSet) bool {
+	if ccas.Integration != that.Integration {
+		return false
+	}
+
+	if ccas.LabelName != that.LabelName {
+		return false
+	}
+
+	if !ccas.Window.Equal(that.Window) {
+		return false
+	}
+
+	if len(ccas.CloudCostAggregates) != len(that.CloudCostAggregates) {
+		return false
+	}
+
+	for k, cca := range ccas.CloudCostAggregates {
+		tcca, ok := that.CloudCostAggregates[k]
+		if !ok {
+			return false
+		}
+		if !cca.Equal(tcca) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (ccas *CloudCostAggregateSet) IsEmpty() bool {
+	if ccas == nil {
+		return true
+	}
+
+	if len(ccas.CloudCostAggregates) == 0 {
+		return true
+	}
+
+	return false
+}
+
+func (ccas *CloudCostAggregateSet) Length() int {
+	if ccas == nil {
+		return 0
+	}
+	return len(ccas.CloudCostAggregates)
+}
+
+func (ccas *CloudCostAggregateSet) GetWindow() Window {
+	return ccas.Window
+}
+
+func (ccas *CloudCostAggregateSet) Merge(that *CloudCostAggregateSet) (*CloudCostAggregateSet, error) {
+	if ccas == nil || that == nil {
+		return nil, fmt.Errorf("cannot merge nil CloudCostAggregateSets")
+	}
+
+	if that.IsEmpty() {
+		return ccas.Clone(), nil
+	}
+
+	if !ccas.Window.Equal(that.Window) {
+		return nil, fmt.Errorf("cannot merge CloudCostAggregateSets with different windows")
+	}
+
+	if ccas.LabelName != that.LabelName {
+		return nil, fmt.Errorf("cannot merge CloudCostAggregateSets with different label names: '%s' != '%s'", ccas.LabelName, that.LabelName)
+	}
+
+	start, end := *ccas.Window.Start(), *ccas.Window.End()
+	result := NewCloudCostAggregateSet(start, end)
+	result.LabelName = ccas.LabelName
+
+	for _, cca := range ccas.CloudCostAggregates {
+		result.insertByProperty(cca, nil)
+	}
+
+	for _, cca := range that.CloudCostAggregates {
+		result.insertByProperty(cca, nil)
+	}
+
+	return result, nil
+}
+
+func GetCloudCostAggregateSets(start, end time.Time, windowDuration time.Duration, integration string, labelName string) ([]*CloudCostAggregateSet, error) {
+	windows, err := GetWindows(start, end, windowDuration)
+	if err != nil {
+		return nil, err
+	}
+
+	// Build slice of CloudCostAggregateSet to cover the range
+	CloudCostAggregateSets := []*CloudCostAggregateSet{}
+	for _, w := range windows {
+		ccas := NewCloudCostAggregateSet(*w.Start(), *w.End())
+		ccas.Integration = integration
+		ccas.LabelName = labelName
+		CloudCostAggregateSets = append(CloudCostAggregateSets, ccas)
+	}
+	return CloudCostAggregateSets, nil
+}
+
+// LoadCloudCostAggregateSets creates and loads CloudCostAggregates into provided CloudCostAggregateSets. This method makes it so
+// that the input windows do not have to match the one day frame of the Athena queries. CloudCostAggregates being generated from a
+// CUR which may be the identical except for the pricing model used (default, RI or savings plan)
+// are accumulated here so that the resulting CloudCostAggregate with the 1d window has the correct price for the entire day.
+func LoadCloudCostAggregateSets(itemStart time.Time, itemEnd time.Time, properties CloudCostAggregateProperties, K8sPercent, cost, credit float64, CloudCostAggregateSets []*CloudCostAggregateSet) {
+	// Disperse cost of the current item across one or more CloudCostAggregates in
+	// across each relevant CloudCostAggregateSet. Stop when the end of the current
+	// block reaches the item's end time or the end of the range.
+	for _, ccas := range CloudCostAggregateSets {
+		pct := ccas.GetWindow().GetPercentInWindow(itemStart, itemEnd)
+
+		// Insert an CloudCostAggregate with that cost into the CloudCostAggregateSet at the given index
+		cca := &CloudCostAggregate{
+			Properties:        properties,
+			KubernetesPercent: K8sPercent * pct,
+			Cost:              cost * pct,
+			Credit:            credit * pct,
+		}
+		err := ccas.insertByProperty(cca, nil)
+		if err != nil {
+			log.Errorf("LoadCloudCostAggregateSets: failed to load CloudCostAggregate with key %s and window %s", cca.Key(nil), ccas.GetWindow().String())
+		}
+	}
+}
+
+type CloudCostAggregateSetRange struct {
+	CloudCostAggregateSets []*CloudCostAggregateSet `json:"sets"`
+	Window                 Window                   `json:"window"`
+}
+
+func (ccasr *CloudCostAggregateSetRange) Accumulate() (*CloudCostAggregateSet, error) {
+	if ccasr == nil {
+		return nil, errors.New("cannot accumulate a nil CloudCostAggregateSetRange")
+	}
+
+	if ccasr.Window.IsOpen() {
+		return nil, fmt.Errorf("cannot accumulate a CloudCostAggregateSetRange with an open window: %s", ccasr.Window)
+	}
+
+	result := NewCloudCostAggregateSet(*ccasr.Window.Start(), *ccasr.Window.End())
+
+	for _, ccas := range ccasr.CloudCostAggregateSets {
+		for name, cca := range ccas.CloudCostAggregates {
+			err := result.insertByProperty(cca.Clone(), ccas.AggregationProperties)
+			if err != nil {
+				return nil, fmt.Errorf("error accumulating CloudCostAggregateSetRange[%s][%s]: %s", ccas.Window.String(), name, err)
+			}
+		}
+	}
+
+	return result, nil
+}

+ 321 - 0
pkg/kubecost/cloudcostitem.go

@@ -0,0 +1,321 @@
+package kubecost
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/opencost/opencost/pkg/filter"
+	"github.com/opencost/opencost/pkg/log"
+)
+
+type CloudCostItemLabels map[string]string
+
+func (ccil CloudCostItemLabels) Clone() CloudCostItemLabels {
+	result := make(map[string]string, len(ccil))
+	for k, v := range ccil {
+		result[k] = v
+	}
+	return result
+}
+
+func (ccil CloudCostItemLabels) Equal(that CloudCostItemLabels) bool {
+	if len(ccil) != len(that) {
+		return false
+	}
+
+	// Maps are of equal length, so if all keys are in both maps, we don't
+	// have to check the keys of the other map.
+	for k, v := range ccil {
+		if tv, ok := that[k]; !ok || v != tv {
+			return false
+		}
+	}
+
+	return true
+}
+
+type CloudCostItemProperties struct {
+	ProviderID string              `json:"providerID,omitempty"`
+	Provider   string              `json:"provider,omitempty"`
+	Account    string              `json:"account,omitempty"`
+	Project    string              `json:"project,omitempty"`
+	Service    string              `json:"service,omitempty"`
+	Category   string              `json:"category,omitempty"`
+	Labels     CloudCostItemLabels `json:"labels,omitempty"`
+}
+
+func (ccip CloudCostItemProperties) Equal(that CloudCostItemProperties) bool {
+	return ccip.ProviderID == that.ProviderID &&
+		ccip.Provider == that.Provider &&
+		ccip.Account == that.Account &&
+		ccip.Project == that.Project &&
+		ccip.Service == that.Service &&
+		ccip.Category == that.Category &&
+		ccip.Labels.Equal(that.Labels)
+}
+
+func (ccip CloudCostItemProperties) Clone() CloudCostItemProperties {
+	return CloudCostItemProperties{
+		ProviderID: ccip.ProviderID,
+		Provider:   ccip.Provider,
+		Account:    ccip.Account,
+		Project:    ccip.Project,
+		Service:    ccip.Service,
+		Category:   ccip.Category,
+		Labels:     ccip.Labels.Clone(),
+	}
+}
+
+func (ccip CloudCostItemProperties) Key() string {
+	return fmt.Sprintf("%s/%s/%s/%s/%s/%s", ccip.Provider, ccip.Account, ccip.Project, ccip.Category, ccip.Service, ccip.ProviderID)
+}
+
+// CloudCostItem represents a CUR line item, identifying a cloud resource and
+// its cost over some period of time.
+type CloudCostItem struct {
+	Properties   CloudCostItemProperties
+	IsKubernetes bool
+	Window       Window
+	Cost         float64
+	Credit       float64
+}
+
+func (cci *CloudCostItem) Clone() *CloudCostItem {
+	return &CloudCostItem{
+		Properties:   cci.Properties.Clone(),
+		IsKubernetes: cci.IsKubernetes,
+		Window:       cci.Window.Clone(),
+		Cost:         cci.Cost,
+		Credit:       cci.Credit,
+	}
+}
+
+func (cci *CloudCostItem) Equal(that *CloudCostItem) bool {
+	if that == nil {
+		return false
+	}
+
+	return cci.Properties.Equal(that.Properties) &&
+		cci.IsKubernetes == that.IsKubernetes &&
+		cci.Window.Equal(that.Window) &&
+		cci.Cost == that.Cost &&
+		cci.Credit == that.Credit
+}
+
+func (cci *CloudCostItem) Key() string {
+	return cci.Properties.Key()
+}
+
+func (cci *CloudCostItem) add(that *CloudCostItem) {
+	if cci == nil {
+		log.Warnf("cannot add to nil CloudCostItem")
+		return
+	}
+
+	cci.Cost += that.Cost
+	cci.Credit += that.Credit
+	cci.Window = cci.Window.Expand(that.Window)
+}
+
+type CloudCostItemSet struct {
+	CloudCostItems map[string]*CloudCostItem
+	Window         Window
+	Integration    string
+}
+
+// NewAssetSet instantiates a new AssetSet and, optionally, inserts
+// the given list of Assets
+func NewCloudCostItemSet(start, end time.Time, cloudCostItems ...*CloudCostItem) *CloudCostItemSet {
+	ccis := &CloudCostItemSet{
+		CloudCostItems: map[string]*CloudCostItem{},
+		Window:         NewWindow(&start, &end),
+	}
+
+	for _, cci := range cloudCostItems {
+		ccis.Insert(cci)
+	}
+
+	return ccis
+}
+
+func (ccis *CloudCostItemSet) Equal(that *CloudCostItemSet) bool {
+	if ccis.Integration != that.Integration {
+		return false
+	}
+
+	if !ccis.Window.Equal(that.Window) {
+		return false
+	}
+
+	if len(ccis.CloudCostItems) != len(that.CloudCostItems) {
+		return false
+	}
+
+	for k, cci := range ccis.CloudCostItems {
+		tcci, ok := that.CloudCostItems[k]
+		if !ok {
+			return false
+		}
+		if !cci.Equal(tcci) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (ccis *CloudCostItemSet) Filter(filters filter.Filter[*CloudCostItem]) *CloudCostItemSet {
+	if ccis == nil {
+		return nil
+	}
+
+	if filters == nil {
+		return ccis.Clone()
+	}
+
+	result := NewCloudCostItemSet(*ccis.Window.start, *ccis.Window.end)
+
+	for _, cci := range ccis.CloudCostItems {
+		if filters.Matches(cci) {
+			result.Insert(cci.Clone())
+		}
+	}
+
+	return result
+}
+
+func (ccis *CloudCostItemSet) Insert(that *CloudCostItem) error {
+	if ccis == nil {
+		return fmt.Errorf("cannot insert into nil CloudCostItemSet")
+	}
+
+	if that == nil {
+		return fmt.Errorf("cannot insert nil CloudCostItem into CloudCostItemSet")
+	}
+
+	if ccis.CloudCostItems == nil {
+		ccis.CloudCostItems = map[string]*CloudCostItem{}
+	}
+
+	// Add the given CloudCostItem to the existing entry, if there is one;
+	// otherwise just set directly into allocations
+	if _, ok := ccis.CloudCostItems[that.Key()]; !ok {
+		ccis.CloudCostItems[that.Key()] = that.Clone()
+	} else {
+		ccis.CloudCostItems[that.Key()].add(that)
+	}
+
+	return nil
+}
+
+func (ccis *CloudCostItemSet) Clone() *CloudCostItemSet {
+	items := make(map[string]*CloudCostItem, len(ccis.CloudCostItems))
+	for k, v := range ccis.CloudCostItems {
+		items[k] = v.Clone()
+	}
+
+	return &CloudCostItemSet{
+		CloudCostItems: items,
+		Integration:    ccis.Integration,
+		Window:         ccis.Window.Clone(),
+	}
+}
+
+func (ccis *CloudCostItemSet) IsEmpty() bool {
+	if ccis == nil {
+		return true
+	}
+
+	if len(ccis.CloudCostItems) == 0 {
+		return true
+	}
+
+	return false
+}
+
+func (ccis *CloudCostItemSet) Length() int {
+	if ccis == nil {
+		return 0
+	}
+	return len(ccis.CloudCostItems)
+}
+
+func (ccis *CloudCostItemSet) GetWindow() Window {
+	return ccis.Window
+}
+
+func (ccis *CloudCostItemSet) Merge(that *CloudCostItemSet) (*CloudCostItemSet, error) {
+	if ccis == nil {
+		return nil, fmt.Errorf("cannot merge nil CloudCostItemSets")
+	}
+
+	if that.IsEmpty() {
+		return ccis.Clone(), nil
+	}
+
+	if !ccis.Window.Equal(that.Window) {
+		return nil, fmt.Errorf("cannot merge CloudCostItemSets with different windows")
+	}
+
+	start, end := *ccis.Window.Start(), *ccis.Window.End()
+	result := NewCloudCostItemSet(start, end)
+
+	for _, cci := range ccis.CloudCostItems {
+		result.Insert(cci)
+	}
+
+	for _, cci := range that.CloudCostItems {
+		result.Insert(cci)
+	}
+
+	return result, nil
+}
+
+// GetCloudCostItemSets
+func GetCloudCostItemSets(start time.Time, end time.Time, window time.Duration, integration string) ([]*CloudCostItemSet, error) {
+	windows, err := GetWindows(start, end, window)
+	if err != nil {
+		return nil, err
+	}
+
+	// Build slice of CloudCostItemSet to cover the range
+	CloudCostItemSets := []*CloudCostItemSet{}
+	for _, w := range windows {
+		ccis := NewCloudCostItemSet(*w.Start(), *w.End())
+		ccis.Integration = integration
+		CloudCostItemSets = append(CloudCostItemSets, ccis)
+	}
+	return CloudCostItemSets, nil
+}
+
+// LoadCloudCostItemSets creates and loads CloudCostItems into provided CloudCostItemSets. This method makes it so
+// that the input windows do not have to match the one day frame of the Athena queries. CloudCostItems being generated from a
+// CUR which may be the identical except for the pricing model used (default, RI or savings plan)
+// are accumulated here so that the resulting CloudCostItem with the 1d window has the correct price for the entire day.
+func LoadCloudCostItemSets(itemStart time.Time, itemEnd time.Time, properties CloudCostItemProperties, isK8s bool, cost, credit float64, CloudCostItemSets []*CloudCostItemSet) {
+
+	// Disperse cost of the current item across one or more CloudCostItems in
+	// across each relevant CloudCostItemSet. Stop when the end of the current
+	// block reaches the item's end time or the end of the range.
+	for _, ccis := range CloudCostItemSets {
+		pct := ccis.GetWindow().GetPercentInWindow(itemStart, itemEnd)
+
+		// Insert an CloudCostItem with that cost into the CloudCostItemSet at the given index
+		cci := &CloudCostItem{
+			Properties:   properties,
+			IsKubernetes: isK8s,
+			Window:       ccis.GetWindow(),
+			Cost:         cost * pct,
+			Credit:       credit * pct,
+		}
+		err := ccis.Insert(cci)
+		if err != nil {
+			log.Errorf("LoadCloudCostItemSets: failed to load CloudCostItem with key %s and window %s: %s", cci.Key(), ccis.GetWindow().String(), err.Error())
+		}
+	}
+}
+
+type CloudCostItemSetRange struct {
+	CloudCostItemSets []*CloudCostItemSet `json:"sets"`
+	Window            Window              `json:"window"`
+}

+ 118 - 0
pkg/kubecost/coverage.go

@@ -0,0 +1,118 @@
+package kubecost
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/pkg/filter"
+)
+
+// Coverage This is a placeholder struct which can be replaced by a more specific implementation later
+type Coverage struct {
+	Window   Window    `json:"window"`
+	Type     string    `json:"type"`
+	Count    int       `json:"count"`
+	Updated  time.Time `json:"updated"`
+	Errors   []string  `json:"errors"`
+	Warnings []string  `json:"warnings"`
+}
+
+func (c *Coverage) GetWindow() Window {
+	return c.Window
+}
+
+func (c *Coverage) Key() string {
+	return c.Type
+}
+
+func (c *Coverage) IsEmpty() bool {
+	return c.Type == "" && c.Count == 0 && len(c.Errors) == 0 && len(c.Warnings) == 0 && c.Updated == time.Time{}
+}
+
+func (c *Coverage) Clone() *Coverage {
+	var errors []string
+	if len(c.Errors) > 0 {
+		errors = make([]string, len(c.Errors))
+		copy(errors, c.Errors)
+	}
+	var warnings []string
+	if len(c.Warnings) > 0 {
+		warnings = make([]string, len(c.Warnings))
+		copy(warnings, c.Warnings)
+	}
+	return &Coverage{
+		Window:   c.Window.Clone(),
+		Type:     c.Type,
+		Count:    c.Count,
+		Updated:  c.Updated,
+		Errors:   errors,
+		Warnings: warnings,
+	}
+}
+
+// Coverage This is a placeholder struct which can be replaced by a more specific implementation later
+type CoverageSet struct {
+	Window Window               `json:"window"`
+	Items  map[string]*Coverage `json:"items"`
+}
+
+func NewCoverageSet(start, end time.Time) *CoverageSet {
+	return &CoverageSet{
+		Window: NewWindow(&start, &end),
+		Items:  map[string]*Coverage{},
+	}
+}
+
+func (cs *CoverageSet) GetWindow() Window {
+	return cs.Window
+}
+
+func (cs *CoverageSet) IsEmpty() bool {
+	for _, item := range cs.Items {
+		if !item.IsEmpty() {
+			return false
+		}
+	}
+	return true
+}
+
+func (cs *CoverageSet) Clone() *CoverageSet {
+	var items map[string]*Coverage
+	if cs.Items != nil {
+		items = make(map[string]*Coverage, len(cs.Items))
+		for k, item := range cs.Items {
+			items[k] = item.Clone()
+		}
+
+	}
+	return &CoverageSet{
+		Window: cs.Window.Clone(),
+		Items:  items,
+	}
+}
+
+func (cs *CoverageSet) Insert(coverage *Coverage) {
+	if cs.Items == nil {
+		cs.Items = map[string]*Coverage{}
+	}
+	cs.Items[coverage.Key()] = coverage
+}
+
+func (cs *CoverageSet) Filter(filters filter.Filter[*Coverage]) *CoverageSet {
+	if cs == nil {
+		return nil
+	}
+
+	if filters == nil {
+		return cs.Clone()
+	}
+
+	result := NewCoverageSet(*cs.Window.start, *cs.Window.end)
+
+	for _, c := range cs.Items {
+		if filters.Matches(c) {
+			result.Insert(c.Clone())
+		}
+	}
+
+	return result
+}

+ 2108 - 125
pkg/kubecost/kubecost_codecs.go

@@ -13,12 +13,11 @@ package kubecost
 
 import (
 	"fmt"
+	util "github.com/opencost/opencost/pkg/util"
 	"reflect"
 	"strings"
 	"sync"
 	"time"
-
-	util "github.com/opencost/opencost/pkg/util"
 )
 
 const (
@@ -34,17 +33,23 @@ const (
 )
 
 const (
+	// DefaultCodecVersion is used for any resources listed in the Default version set
+	DefaultCodecVersion uint8 = 17
+
+	// AssetsCodecVersion is used for any resources listed in the Assets version set
+	AssetsCodecVersion uint8 = 18
+
 	// AllocationCodecVersion is used for any resources listed in the Allocation version set
 	AllocationCodecVersion uint8 = 15
 
 	// AuditCodecVersion is used for any resources listed in the Audit version set
 	AuditCodecVersion uint8 = 1
 
-	// DefaultCodecVersion is used for any resources listed in the Default version set
-	DefaultCodecVersion uint8 = 15
+	// CloudCostAggregateCodecVersion is used for any resources listed in the CloudCostAggregate version set
+	CloudCostAggregateCodecVersion uint8 = 1
 
-	// AssetsCodecVersion is used for any resources listed in the Assets version set
-	AssetsCodecVersion uint8 = 18
+	// CloudCostItemCodecVersion is used for any resources listed in the CloudCostItem version set
+	CloudCostItemCodecVersion uint8 = 1
 )
 
 //--------------------------------------------------------------------------
@@ -71,7 +76,17 @@ var typeMap map[string]reflect.Type = map[string]reflect.Type{
 	"AuditSetRange":                 reflect.TypeOf((*AuditSetRange)(nil)).Elem(),
 	"Breakdown":                     reflect.TypeOf((*Breakdown)(nil)).Elem(),
 	"Cloud":                         reflect.TypeOf((*Cloud)(nil)).Elem(),
+	"CloudCostAggregate":            reflect.TypeOf((*CloudCostAggregate)(nil)).Elem(),
+	"CloudCostAggregateProperties":  reflect.TypeOf((*CloudCostAggregateProperties)(nil)).Elem(),
+	"CloudCostAggregateSet":         reflect.TypeOf((*CloudCostAggregateSet)(nil)).Elem(),
+	"CloudCostAggregateSetRange":    reflect.TypeOf((*CloudCostAggregateSetRange)(nil)).Elem(),
+	"CloudCostItem":                 reflect.TypeOf((*CloudCostItem)(nil)).Elem(),
+	"CloudCostItemProperties":       reflect.TypeOf((*CloudCostItemProperties)(nil)).Elem(),
+	"CloudCostItemSet":              reflect.TypeOf((*CloudCostItemSet)(nil)).Elem(),
+	"CloudCostItemSetRange":         reflect.TypeOf((*CloudCostItemSetRange)(nil)).Elem(),
 	"ClusterManagement":             reflect.TypeOf((*ClusterManagement)(nil)).Elem(),
+	"Coverage":                      reflect.TypeOf((*Coverage)(nil)).Elem(),
+	"CoverageSet":                   reflect.TypeOf((*CoverageSet)(nil)).Elem(),
 	"Disk":                          reflect.TypeOf((*Disk)(nil)).Elem(),
 	"EqualityAudit":                 reflect.TypeOf((*EqualityAudit)(nil)).Elem(),
 	"LoadBalancer":                  reflect.TypeOf((*LoadBalancer)(nil)).Elem(),
@@ -4621,12 +4636,12 @@ func (target *Cloud) UnmarshalBinaryWithContext(ctx *DecodingContext) (err error
 }
 
 //--------------------------------------------------------------------------
-//  ClusterManagement
+//  CloudCostAggregate
 //--------------------------------------------------------------------------
 
-// MarshalBinary serializes the internal properties of this ClusterManagement instance
+// MarshalBinary serializes the internal properties of this CloudCostAggregate instance
 // into a byte array
-func (target *ClusterManagement) MarshalBinary() (data []byte, err error) {
+func (target *CloudCostAggregate) MarshalBinary() (data []byte, err error) {
 	ctx := &EncodingContext{
 		Buffer: util.NewBuffer(),
 		Table:  nil,
@@ -4641,9 +4656,9 @@ func (target *ClusterManagement) MarshalBinary() (data []byte, err error) {
 	return encBytes, nil
 }
 
-// MarshalBinaryWithContext serializes the internal properties of this ClusterManagement instance
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostAggregate instance
 // into a byte array leveraging a predefined context.
-func (target *ClusterManagement) MarshalBinaryWithContext(ctx *EncodingContext) (err error) {
+func (target *CloudCostAggregate) MarshalBinaryWithContext(ctx *EncodingContext) (err error) {
 	// panics are recovered and propagated as errors
 	defer func() {
 		if r := recover(); r != nil {
@@ -4658,65 +4673,25 @@ func (target *ClusterManagement) MarshalBinaryWithContext(ctx *EncodingContext)
 	}()
 
 	buff := ctx.Buffer
-	buff.WriteUInt8(AssetsCodecVersion) // version
-
-	// --- [begin][write][alias](AssetLabels) ---
-	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) {
-			if ctx.IsStringTable() {
-				a := ctx.Table.AddOrGet(v)
-				buff.WriteInt(a) // write table index
-			} else {
-				buff.WriteString(v) // write string
-			}
-			if ctx.IsStringTable() {
-				b := ctx.Table.AddOrGet(z)
-				buff.WriteInt(b) // write table index
-			} else {
-				buff.WriteString(z) // write string
-			}
-		}
-		// --- [end][write][map](map[string]string) ---
-
-	}
-	// --- [end][write][alias](AssetLabels) ---
-
-	if target.Properties == nil {
-		buff.WriteUInt8(uint8(0)) // write nil byte
-	} else {
-		buff.WriteUInt8(uint8(1)) // write non-nil byte
-
-		// --- [begin][write][struct](AssetProperties) ---
-		buff.WriteInt(0) // [compatibility, unused]
-		errA := target.Properties.MarshalBinaryWithContext(ctx)
-		if errA != nil {
-			return errA
-		}
-		// --- [end][write][struct](AssetProperties) ---
+	buff.WriteUInt8(CloudCostAggregateCodecVersion) // version
 
-	}
-	// --- [begin][write][struct](Window) ---
+	// --- [begin][write][struct](CloudCostAggregateProperties) ---
 	buff.WriteInt(0) // [compatibility, unused]
-	errB := target.Window.MarshalBinaryWithContext(ctx)
-	if errB != nil {
-		return errB
+	errA := target.Properties.MarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
 	}
-	// --- [end][write][struct](Window) ---
+	// --- [end][write][struct](CloudCostAggregateProperties) ---
 
-	buff.WriteFloat64(target.Cost)       // write float64
-	buff.WriteFloat64(target.Adjustment) // write float64
+	buff.WriteFloat64(target.KubernetesPercent) // write float64
+	buff.WriteFloat64(target.Cost)              // write float64
+	buff.WriteFloat64(target.Credit)            // write float64
 	return nil
 }
 
 // UnmarshalBinary uses the data passed byte array to set all the internal properties of
-// the ClusterManagement type
-func (target *ClusterManagement) UnmarshalBinary(data []byte) error {
+// the CloudCostAggregate type
+func (target *CloudCostAggregate) UnmarshalBinary(data []byte) error {
 	var table []string
 	buff := util.NewBufferFromBytes(data)
 
@@ -4746,8 +4721,8 @@ func (target *ClusterManagement) UnmarshalBinary(data []byte) error {
 }
 
 // UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
-// the ClusterManagement type
-func (target *ClusterManagement) UnmarshalBinaryWithContext(ctx *DecodingContext) (err error) {
+// the CloudCostAggregate type
+func (target *CloudCostAggregate) UnmarshalBinaryWithContext(ctx *DecodingContext) (err error) {
 	// panics are recovered and propagated as errors
 	defer func() {
 		if r := recover(); r != nil {
@@ -4764,86 +4739,2094 @@ func (target *ClusterManagement) UnmarshalBinaryWithContext(ctx *DecodingContext
 	buff := ctx.Buffer
 	version := buff.ReadUInt8()
 
-	if version > AssetsCodecVersion {
-		return fmt.Errorf("Invalid Version Unmarshaling ClusterManagement. Expected %d or less, got %d", AssetsCodecVersion, version)
+	if version > CloudCostAggregateCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostAggregate. Expected %d or less, got %d", CloudCostAggregateCodecVersion, version)
 	}
 
-	// --- [begin][read][alias](AssetLabels) ---
-	var a map[string]string
-	if buff.ReadUInt8() == uint8(0) {
-		a = nil
-	} else {
-		// --- [begin][read][map](map[string]string) ---
-		c := buff.ReadInt() // map len
-		b := make(map[string]string, c)
-		for i := 0; i < c; i++ {
-			var v string
-			var e string
-			if ctx.IsStringTable() {
-				f := buff.ReadInt() // read string index
-				e = ctx.Table[f]
-			} else {
-				e = buff.ReadString() // read string
-			}
-			d := e
-			v = d
+	// --- [begin][read][struct](CloudCostAggregateProperties) ---
+	a := &CloudCostAggregateProperties{}
+	buff.ReadInt() // [compatibility, unused]
+	errA := a.UnmarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
+	}
+	target.Properties = *a
+	// --- [end][read][struct](CloudCostAggregateProperties) ---
 
-			var z string
-			var h string
-			if ctx.IsStringTable() {
-				k := buff.ReadInt() // read string index
-				h = ctx.Table[k]
+	b := buff.ReadFloat64() // read float64
+	target.KubernetesPercent = b
+
+	c := buff.ReadFloat64() // read float64
+	target.Cost = c
+
+	d := buff.ReadFloat64() // read float64
+	target.Credit = d
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CloudCostAggregateProperties
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CloudCostAggregateProperties instance
+// into a byte array
+func (target *CloudCostAggregateProperties) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostAggregateProperties instance
+// into a byte array leveraging a predefined context.
+func (target *CloudCostAggregateProperties) MarshalBinaryWithContext(ctx *EncodingContext) (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 {
-				h = buff.ReadString() // read string
+				err = fmt.Errorf("Unexpected panic: %+v", r)
 			}
-			g := h
-			z = g
-
-			b[v] = z
 		}
-		a = b
-		// --- [end][read][map](map[string]string) ---
+	}()
 
-	}
-	target.Labels = AssetLabels(a)
-	// --- [end][read][alias](AssetLabels) ---
+	buff := ctx.Buffer
+	buff.WriteUInt8(CloudCostAggregateCodecVersion) // version
 
-	if buff.ReadUInt8() == uint8(0) {
-		target.Properties = nil
+	if ctx.IsStringTable() {
+		a := ctx.Table.AddOrGet(target.Provider)
+		buff.WriteInt(a) // write table index
 	} else {
-		// --- [begin][read][struct](AssetProperties) ---
-		l := &AssetProperties{}
-		buff.ReadInt() // [compatibility, unused]
-		errA := l.UnmarshalBinaryWithContext(ctx)
-		if errA != nil {
-			return errA
+		buff.WriteString(target.Provider) // write string
+	}
+	if ctx.IsStringTable() {
+		b := ctx.Table.AddOrGet(target.Account)
+		buff.WriteInt(b) // write table index
+	} else {
+		buff.WriteString(target.Account) // write string
+	}
+	if ctx.IsStringTable() {
+		c := ctx.Table.AddOrGet(target.Project)
+		buff.WriteInt(c) // write table index
+	} else {
+		buff.WriteString(target.Project) // write string
+	}
+	if ctx.IsStringTable() {
+		d := ctx.Table.AddOrGet(target.Service)
+		buff.WriteInt(d) // write table index
+	} else {
+		buff.WriteString(target.Service) // write string
+	}
+	if ctx.IsStringTable() {
+		e := ctx.Table.AddOrGet(target.LabelValue)
+		buff.WriteInt(e) // write table index
+	} else {
+		buff.WriteString(target.LabelValue) // write string
+	}
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CloudCostAggregateProperties type
+func (target *CloudCostAggregateProperties) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
 		}
-		target.Properties = l
-		// --- [end][read][struct](AssetProperties) ---
+	}
 
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
 	}
-	// --- [begin][read][struct](Window) ---
-	m := &Window{}
-	buff.ReadInt() // [compatibility, unused]
-	errB := m.UnmarshalBinaryWithContext(ctx)
-	if errB != nil {
-		return errB
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
 	}
-	target.Window = *m
-	// --- [end][read][struct](Window) ---
 
-	n := buff.ReadFloat64() // read float64
-	target.Cost = n
+	return nil
+}
 
-	// field version check
-	if uint8(16) <= version {
-		o := buff.ReadFloat64() // read float64
-		target.Adjustment = o
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CloudCostAggregateProperties type
+func (target *CloudCostAggregateProperties) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > CloudCostAggregateCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostAggregateProperties. Expected %d or less, got %d", CloudCostAggregateCodecVersion, version)
+	}
 
+	var b string
+	if ctx.IsStringTable() {
+		c := buff.ReadInt() // read string index
+		b = ctx.Table[c]
 	} else {
-		target.Adjustment = float64(0) // default
+		b = buff.ReadString() // read string
+	}
+	a := b
+	target.Provider = a
+
+	var e string
+	if ctx.IsStringTable() {
+		f := buff.ReadInt() // read string index
+		e = ctx.Table[f]
+	} else {
+		e = buff.ReadString() // read string
 	}
+	d := e
+	target.Account = d
 
+	var h string
+	if ctx.IsStringTable() {
+		k := buff.ReadInt() // read string index
+		h = ctx.Table[k]
+	} else {
+		h = buff.ReadString() // read string
+	}
+	g := h
+	target.Project = g
+
+	var m string
+	if ctx.IsStringTable() {
+		n := buff.ReadInt() // read string index
+		m = ctx.Table[n]
+	} else {
+		m = buff.ReadString() // read string
+	}
+	l := m
+	target.Service = l
+
+	var p string
+	if ctx.IsStringTable() {
+		q := buff.ReadInt() // read string index
+		p = ctx.Table[q]
+	} else {
+		p = buff.ReadString() // read string
+	}
+	o := p
+	target.LabelValue = o
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CloudCostAggregateSet
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CloudCostAggregateSet instance
+// into a byte array
+func (target *CloudCostAggregateSet) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  NewStringTable(),
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	sTableBytes := ctx.Table.ToBytes()
+	merged := appendBytes(sTableBytes, encBytes)
+	return merged, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostAggregateSet instance
+// into a byte array leveraging a predefined context.
+func (target *CloudCostAggregateSet) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(CloudCostAggregateCodecVersion) // version
+
+	if target.CloudCostAggregates == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][map](map[string]*CloudCostAggregate) ---
+		buff.WriteInt(len(target.CloudCostAggregates)) // map length
+		for v, z := range target.CloudCostAggregates {
+			if ctx.IsStringTable() {
+				a := ctx.Table.AddOrGet(v)
+				buff.WriteInt(a) // write table index
+			} else {
+				buff.WriteString(v) // write string
+			}
+			if z == nil {
+				buff.WriteUInt8(uint8(0)) // write nil byte
+			} else {
+				buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+				// --- [begin][write][struct](CloudCostAggregate) ---
+				buff.WriteInt(0) // [compatibility, unused]
+				errA := z.MarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				// --- [end][write][struct](CloudCostAggregate) ---
+
+			}
+		}
+		// --- [end][write][map](map[string]*CloudCostAggregate) ---
+
+	}
+	if target.AggregationProperties == 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.AggregationProperties)) // array length
+		for i := 0; i < len(target.AggregationProperties); i++ {
+			if ctx.IsStringTable() {
+				b := ctx.Table.AddOrGet(target.AggregationProperties[i])
+				buff.WriteInt(b) // write table index
+			} else {
+				buff.WriteString(target.AggregationProperties[i]) // write string
+			}
+		}
+		// --- [end][write][slice]([]string) ---
+
+	}
+	if ctx.IsStringTable() {
+		c := ctx.Table.AddOrGet(target.Integration)
+		buff.WriteInt(c) // write table index
+	} else {
+		buff.WriteString(target.Integration) // write string
+	}
+	if ctx.IsStringTable() {
+		d := ctx.Table.AddOrGet(target.LabelName)
+		buff.WriteInt(d) // write table index
+	} else {
+		buff.WriteString(target.LabelName) // write string
+	}
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errB := target.Window.MarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	// --- [end][write][struct](Window) ---
+
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CloudCostAggregateSet type
+func (target *CloudCostAggregateSet) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CloudCostAggregateSet type
+func (target *CloudCostAggregateSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > CloudCostAggregateCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostAggregateSet. Expected %d or less, got %d", CloudCostAggregateCodecVersion, version)
+	}
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.CloudCostAggregates = nil
+	} else {
+		// --- [begin][read][map](map[string]*CloudCostAggregate) ---
+		b := buff.ReadInt() // map len
+		a := make(map[string]*CloudCostAggregate, b)
+		for i := 0; i < b; i++ {
+			var v string
+			var d string
+			if ctx.IsStringTable() {
+				e := buff.ReadInt() // read string index
+				d = ctx.Table[e]
+			} else {
+				d = buff.ReadString() // read string
+			}
+			c := d
+			v = c
+
+			var z *CloudCostAggregate
+			if buff.ReadUInt8() == uint8(0) {
+				z = nil
+			} else {
+				// --- [begin][read][struct](CloudCostAggregate) ---
+				f := &CloudCostAggregate{}
+				buff.ReadInt() // [compatibility, unused]
+				errA := f.UnmarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				z = f
+				// --- [end][read][struct](CloudCostAggregate) ---
+
+			}
+			a[v] = z
+		}
+		target.CloudCostAggregates = a
+		// --- [end][read][map](map[string]*CloudCostAggregate) ---
+
+	}
+	if buff.ReadUInt8() == uint8(0) {
+		target.AggregationProperties = nil
+	} else {
+		// --- [begin][read][slice]([]string) ---
+		h := buff.ReadInt() // array len
+		g := make([]string, h)
+		for j := 0; j < h; j++ {
+			var k string
+			var m string
+			if ctx.IsStringTable() {
+				n := buff.ReadInt() // read string index
+				m = ctx.Table[n]
+			} else {
+				m = buff.ReadString() // read string
+			}
+			l := m
+			k = l
+
+			g[j] = k
+		}
+		target.AggregationProperties = g
+		// --- [end][read][slice]([]string) ---
+
+	}
+	var p string
+	if ctx.IsStringTable() {
+		q := buff.ReadInt() // read string index
+		p = ctx.Table[q]
+	} else {
+		p = buff.ReadString() // read string
+	}
+	o := p
+	target.Integration = o
+
+	var s string
+	if ctx.IsStringTable() {
+		t := buff.ReadInt() // read string index
+		s = ctx.Table[t]
+	} else {
+		s = buff.ReadString() // read string
+	}
+	r := s
+	target.LabelName = r
+
+	// --- [begin][read][struct](Window) ---
+	u := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errB := u.UnmarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	target.Window = *u
+	// --- [end][read][struct](Window) ---
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CloudCostAggregateSetRange
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CloudCostAggregateSetRange instance
+// into a byte array
+func (target *CloudCostAggregateSetRange) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostAggregateSetRange instance
+// into a byte array leveraging a predefined context.
+func (target *CloudCostAggregateSetRange) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(CloudCostAggregateCodecVersion) // version
+
+	if target.CloudCostAggregateSets == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][slice]([]*CloudCostAggregateSet) ---
+		buff.WriteInt(len(target.CloudCostAggregateSets)) // array length
+		for i := 0; i < len(target.CloudCostAggregateSets); i++ {
+			if target.CloudCostAggregateSets[i] == nil {
+				buff.WriteUInt8(uint8(0)) // write nil byte
+			} else {
+				buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+				// --- [begin][write][struct](CloudCostAggregateSet) ---
+				buff.WriteInt(0) // [compatibility, unused]
+				errA := target.CloudCostAggregateSets[i].MarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				// --- [end][write][struct](CloudCostAggregateSet) ---
+
+			}
+		}
+		// --- [end][write][slice]([]*CloudCostAggregateSet) ---
+
+	}
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errB := target.Window.MarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	// --- [end][write][struct](Window) ---
+
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CloudCostAggregateSetRange type
+func (target *CloudCostAggregateSetRange) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CloudCostAggregateSetRange type
+func (target *CloudCostAggregateSetRange) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > CloudCostAggregateCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostAggregateSetRange. Expected %d or less, got %d", CloudCostAggregateCodecVersion, version)
+	}
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.CloudCostAggregateSets = nil
+	} else {
+		// --- [begin][read][slice]([]*CloudCostAggregateSet) ---
+		b := buff.ReadInt() // array len
+		a := make([]*CloudCostAggregateSet, b)
+		for i := 0; i < b; i++ {
+			var c *CloudCostAggregateSet
+			if buff.ReadUInt8() == uint8(0) {
+				c = nil
+			} else {
+				// --- [begin][read][struct](CloudCostAggregateSet) ---
+				d := &CloudCostAggregateSet{}
+				buff.ReadInt() // [compatibility, unused]
+				errA := d.UnmarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				c = d
+				// --- [end][read][struct](CloudCostAggregateSet) ---
+
+			}
+			a[i] = c
+		}
+		target.CloudCostAggregateSets = a
+		// --- [end][read][slice]([]*CloudCostAggregateSet) ---
+
+	}
+	// --- [begin][read][struct](Window) ---
+	e := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errB := e.UnmarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	target.Window = *e
+	// --- [end][read][struct](Window) ---
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CloudCostItem
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CloudCostItem instance
+// into a byte array
+func (target *CloudCostItem) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostItem instance
+// into a byte array leveraging a predefined context.
+func (target *CloudCostItem) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(CloudCostItemCodecVersion) // version
+
+	// --- [begin][write][struct](CloudCostItemProperties) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errA := target.Properties.MarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
+	}
+	// --- [end][write][struct](CloudCostItemProperties) ---
+
+	buff.WriteBool(target.IsKubernetes) // write bool
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errB := target.Window.MarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	// --- [end][write][struct](Window) ---
+
+	buff.WriteFloat64(target.Cost)   // write float64
+	buff.WriteFloat64(target.Credit) // write float64
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CloudCostItem type
+func (target *CloudCostItem) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CloudCostItem type
+func (target *CloudCostItem) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > CloudCostItemCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostItem. Expected %d or less, got %d", CloudCostItemCodecVersion, version)
+	}
+
+	// --- [begin][read][struct](CloudCostItemProperties) ---
+	a := &CloudCostItemProperties{}
+	buff.ReadInt() // [compatibility, unused]
+	errA := a.UnmarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
+	}
+	target.Properties = *a
+	// --- [end][read][struct](CloudCostItemProperties) ---
+
+	b := buff.ReadBool() // read bool
+	target.IsKubernetes = b
+
+	// --- [begin][read][struct](Window) ---
+	c := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errB := c.UnmarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	target.Window = *c
+	// --- [end][read][struct](Window) ---
+
+	d := buff.ReadFloat64() // read float64
+	target.Cost = d
+
+	e := buff.ReadFloat64() // read float64
+	target.Credit = e
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CloudCostItemProperties
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CloudCostItemProperties instance
+// into a byte array
+func (target *CloudCostItemProperties) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostItemProperties instance
+// into a byte array leveraging a predefined context.
+func (target *CloudCostItemProperties) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(CloudCostItemCodecVersion) // version
+
+	if ctx.IsStringTable() {
+		a := ctx.Table.AddOrGet(target.ProviderID)
+		buff.WriteInt(a) // write table index
+	} else {
+		buff.WriteString(target.ProviderID) // write string
+	}
+	if ctx.IsStringTable() {
+		b := ctx.Table.AddOrGet(target.Provider)
+		buff.WriteInt(b) // write table index
+	} else {
+		buff.WriteString(target.Provider) // write string
+	}
+	if ctx.IsStringTable() {
+		c := ctx.Table.AddOrGet(target.Account)
+		buff.WriteInt(c) // write table index
+	} else {
+		buff.WriteString(target.Account) // write string
+	}
+	if ctx.IsStringTable() {
+		d := ctx.Table.AddOrGet(target.Project)
+		buff.WriteInt(d) // write table index
+	} else {
+		buff.WriteString(target.Project) // write string
+	}
+	if ctx.IsStringTable() {
+		e := ctx.Table.AddOrGet(target.Service)
+		buff.WriteInt(e) // write table index
+	} else {
+		buff.WriteString(target.Service) // write string
+	}
+	if ctx.IsStringTable() {
+		f := ctx.Table.AddOrGet(target.Category)
+		buff.WriteInt(f) // write table index
+	} else {
+		buff.WriteString(target.Category) // write string
+	}
+	// --- [begin][write][alias](CloudCostItemLabels) ---
+	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) {
+			if ctx.IsStringTable() {
+				g := ctx.Table.AddOrGet(v)
+				buff.WriteInt(g) // write table index
+			} else {
+				buff.WriteString(v) // write string
+			}
+			if ctx.IsStringTable() {
+				h := ctx.Table.AddOrGet(z)
+				buff.WriteInt(h) // write table index
+			} else {
+				buff.WriteString(z) // write string
+			}
+		}
+		// --- [end][write][map](map[string]string) ---
+
+	}
+	// --- [end][write][alias](CloudCostItemLabels) ---
+
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CloudCostItemProperties type
+func (target *CloudCostItemProperties) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CloudCostItemProperties type
+func (target *CloudCostItemProperties) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > CloudCostItemCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostItemProperties. Expected %d or less, got %d", CloudCostItemCodecVersion, version)
+	}
+
+	var b string
+	if ctx.IsStringTable() {
+		c := buff.ReadInt() // read string index
+		b = ctx.Table[c]
+	} else {
+		b = buff.ReadString() // read string
+	}
+	a := b
+	target.ProviderID = a
+
+	var e string
+	if ctx.IsStringTable() {
+		f := buff.ReadInt() // read string index
+		e = ctx.Table[f]
+	} else {
+		e = buff.ReadString() // read string
+	}
+	d := e
+	target.Provider = d
+
+	var h string
+	if ctx.IsStringTable() {
+		k := buff.ReadInt() // read string index
+		h = ctx.Table[k]
+	} else {
+		h = buff.ReadString() // read string
+	}
+	g := h
+	target.Account = g
+
+	var m string
+	if ctx.IsStringTable() {
+		n := buff.ReadInt() // read string index
+		m = ctx.Table[n]
+	} else {
+		m = buff.ReadString() // read string
+	}
+	l := m
+	target.Project = l
+
+	var p string
+	if ctx.IsStringTable() {
+		q := buff.ReadInt() // read string index
+		p = ctx.Table[q]
+	} else {
+		p = buff.ReadString() // read string
+	}
+	o := p
+	target.Service = o
+
+	var s string
+	if ctx.IsStringTable() {
+		t := buff.ReadInt() // read string index
+		s = ctx.Table[t]
+	} else {
+		s = buff.ReadString() // read string
+	}
+	r := s
+	target.Category = r
+
+	// --- [begin][read][alias](CloudCostItemLabels) ---
+	var u map[string]string
+	if buff.ReadUInt8() == uint8(0) {
+		u = nil
+	} else {
+		// --- [begin][read][map](map[string]string) ---
+		x := buff.ReadInt() // map len
+		w := make(map[string]string, x)
+		for i := 0; i < x; i++ {
+			var v string
+			var aa string
+			if ctx.IsStringTable() {
+				bb := buff.ReadInt() // read string index
+				aa = ctx.Table[bb]
+			} else {
+				aa = buff.ReadString() // read string
+			}
+			y := aa
+			v = y
+
+			var z string
+			var dd string
+			if ctx.IsStringTable() {
+				ee := buff.ReadInt() // read string index
+				dd = ctx.Table[ee]
+			} else {
+				dd = buff.ReadString() // read string
+			}
+			cc := dd
+			z = cc
+
+			w[v] = z
+		}
+		u = w
+		// --- [end][read][map](map[string]string) ---
+
+	}
+	target.Labels = CloudCostItemLabels(u)
+	// --- [end][read][alias](CloudCostItemLabels) ---
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CloudCostItemSet
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CloudCostItemSet instance
+// into a byte array
+func (target *CloudCostItemSet) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  NewStringTable(),
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	sTableBytes := ctx.Table.ToBytes()
+	merged := appendBytes(sTableBytes, encBytes)
+	return merged, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostItemSet instance
+// into a byte array leveraging a predefined context.
+func (target *CloudCostItemSet) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(CloudCostItemCodecVersion) // version
+
+	if target.CloudCostItems == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][map](map[string]*CloudCostItem) ---
+		buff.WriteInt(len(target.CloudCostItems)) // map length
+		for v, z := range target.CloudCostItems {
+			if ctx.IsStringTable() {
+				a := ctx.Table.AddOrGet(v)
+				buff.WriteInt(a) // write table index
+			} else {
+				buff.WriteString(v) // write string
+			}
+			if z == nil {
+				buff.WriteUInt8(uint8(0)) // write nil byte
+			} else {
+				buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+				// --- [begin][write][struct](CloudCostItem) ---
+				buff.WriteInt(0) // [compatibility, unused]
+				errA := z.MarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				// --- [end][write][struct](CloudCostItem) ---
+
+			}
+		}
+		// --- [end][write][map](map[string]*CloudCostItem) ---
+
+	}
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errB := target.Window.MarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	// --- [end][write][struct](Window) ---
+
+	if ctx.IsStringTable() {
+		b := ctx.Table.AddOrGet(target.Integration)
+		buff.WriteInt(b) // write table index
+	} else {
+		buff.WriteString(target.Integration) // write string
+	}
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CloudCostItemSet type
+func (target *CloudCostItemSet) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CloudCostItemSet type
+func (target *CloudCostItemSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > CloudCostItemCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostItemSet. Expected %d or less, got %d", CloudCostItemCodecVersion, version)
+	}
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.CloudCostItems = nil
+	} else {
+		// --- [begin][read][map](map[string]*CloudCostItem) ---
+		b := buff.ReadInt() // map len
+		a := make(map[string]*CloudCostItem, b)
+		for i := 0; i < b; i++ {
+			var v string
+			var d string
+			if ctx.IsStringTable() {
+				e := buff.ReadInt() // read string index
+				d = ctx.Table[e]
+			} else {
+				d = buff.ReadString() // read string
+			}
+			c := d
+			v = c
+
+			var z *CloudCostItem
+			if buff.ReadUInt8() == uint8(0) {
+				z = nil
+			} else {
+				// --- [begin][read][struct](CloudCostItem) ---
+				f := &CloudCostItem{}
+				buff.ReadInt() // [compatibility, unused]
+				errA := f.UnmarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				z = f
+				// --- [end][read][struct](CloudCostItem) ---
+
+			}
+			a[v] = z
+		}
+		target.CloudCostItems = a
+		// --- [end][read][map](map[string]*CloudCostItem) ---
+
+	}
+	// --- [begin][read][struct](Window) ---
+	g := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errB := g.UnmarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	target.Window = *g
+	// --- [end][read][struct](Window) ---
+
+	var k string
+	if ctx.IsStringTable() {
+		l := buff.ReadInt() // read string index
+		k = ctx.Table[l]
+	} else {
+		k = buff.ReadString() // read string
+	}
+	h := k
+	target.Integration = h
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CloudCostItemSetRange
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CloudCostItemSetRange instance
+// into a byte array
+func (target *CloudCostItemSetRange) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CloudCostItemSetRange instance
+// into a byte array leveraging a predefined context.
+func (target *CloudCostItemSetRange) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(CloudCostItemCodecVersion) // version
+
+	if target.CloudCostItemSets == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][slice]([]*CloudCostItemSet) ---
+		buff.WriteInt(len(target.CloudCostItemSets)) // array length
+		for i := 0; i < len(target.CloudCostItemSets); i++ {
+			if target.CloudCostItemSets[i] == nil {
+				buff.WriteUInt8(uint8(0)) // write nil byte
+			} else {
+				buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+				// --- [begin][write][struct](CloudCostItemSet) ---
+				buff.WriteInt(0) // [compatibility, unused]
+				errA := target.CloudCostItemSets[i].MarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				// --- [end][write][struct](CloudCostItemSet) ---
+
+			}
+		}
+		// --- [end][write][slice]([]*CloudCostItemSet) ---
+
+	}
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errB := target.Window.MarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	// --- [end][write][struct](Window) ---
+
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CloudCostItemSetRange type
+func (target *CloudCostItemSetRange) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CloudCostItemSetRange type
+func (target *CloudCostItemSetRange) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > CloudCostItemCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CloudCostItemSetRange. Expected %d or less, got %d", CloudCostItemCodecVersion, version)
+	}
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.CloudCostItemSets = nil
+	} else {
+		// --- [begin][read][slice]([]*CloudCostItemSet) ---
+		b := buff.ReadInt() // array len
+		a := make([]*CloudCostItemSet, b)
+		for i := 0; i < b; i++ {
+			var c *CloudCostItemSet
+			if buff.ReadUInt8() == uint8(0) {
+				c = nil
+			} else {
+				// --- [begin][read][struct](CloudCostItemSet) ---
+				d := &CloudCostItemSet{}
+				buff.ReadInt() // [compatibility, unused]
+				errA := d.UnmarshalBinaryWithContext(ctx)
+				if errA != nil {
+					return errA
+				}
+				c = d
+				// --- [end][read][struct](CloudCostItemSet) ---
+
+			}
+			a[i] = c
+		}
+		target.CloudCostItemSets = a
+		// --- [end][read][slice]([]*CloudCostItemSet) ---
+
+	}
+	// --- [begin][read][struct](Window) ---
+	e := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errB := e.UnmarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	target.Window = *e
+	// --- [end][read][struct](Window) ---
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  ClusterManagement
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this ClusterManagement instance
+// into a byte array
+func (target *ClusterManagement) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this ClusterManagement instance
+// into a byte array leveraging a predefined context.
+func (target *ClusterManagement) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(AssetsCodecVersion) // version
+
+	// --- [begin][write][alias](AssetLabels) ---
+	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) {
+			if ctx.IsStringTable() {
+				a := ctx.Table.AddOrGet(v)
+				buff.WriteInt(a) // write table index
+			} else {
+				buff.WriteString(v) // write string
+			}
+			if ctx.IsStringTable() {
+				b := ctx.Table.AddOrGet(z)
+				buff.WriteInt(b) // write table index
+			} else {
+				buff.WriteString(z) // write string
+			}
+		}
+		// --- [end][write][map](map[string]string) ---
+
+	}
+	// --- [end][write][alias](AssetLabels) ---
+
+	if target.Properties == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][struct](AssetProperties) ---
+		buff.WriteInt(0) // [compatibility, unused]
+		errA := target.Properties.MarshalBinaryWithContext(ctx)
+		if errA != nil {
+			return errA
+		}
+		// --- [end][write][struct](AssetProperties) ---
+
+	}
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errB := target.Window.MarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	// --- [end][write][struct](Window) ---
+
+	buff.WriteFloat64(target.Cost)       // write float64
+	buff.WriteFloat64(target.Adjustment) // write float64
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the ClusterManagement type
+func (target *ClusterManagement) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the ClusterManagement type
+func (target *ClusterManagement) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > AssetsCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling ClusterManagement. Expected %d or less, got %d", AssetsCodecVersion, version)
+	}
+
+	// --- [begin][read][alias](AssetLabels) ---
+	var a map[string]string
+	if buff.ReadUInt8() == uint8(0) {
+		a = nil
+	} else {
+		// --- [begin][read][map](map[string]string) ---
+		c := buff.ReadInt() // map len
+		b := make(map[string]string, c)
+		for i := 0; i < c; i++ {
+			var v string
+			var e string
+			if ctx.IsStringTable() {
+				f := buff.ReadInt() // read string index
+				e = ctx.Table[f]
+			} else {
+				e = buff.ReadString() // read string
+			}
+			d := e
+			v = d
+
+			var z string
+			var h string
+			if ctx.IsStringTable() {
+				k := buff.ReadInt() // read string index
+				h = ctx.Table[k]
+			} else {
+				h = buff.ReadString() // read string
+			}
+			g := h
+			z = g
+
+			b[v] = z
+		}
+		a = b
+		// --- [end][read][map](map[string]string) ---
+
+	}
+	target.Labels = AssetLabels(a)
+	// --- [end][read][alias](AssetLabels) ---
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.Properties = nil
+	} else {
+		// --- [begin][read][struct](AssetProperties) ---
+		l := &AssetProperties{}
+		buff.ReadInt() // [compatibility, unused]
+		errA := l.UnmarshalBinaryWithContext(ctx)
+		if errA != nil {
+			return errA
+		}
+		target.Properties = l
+		// --- [end][read][struct](AssetProperties) ---
+
+	}
+	// --- [begin][read][struct](Window) ---
+	m := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errB := m.UnmarshalBinaryWithContext(ctx)
+	if errB != nil {
+		return errB
+	}
+	target.Window = *m
+	// --- [end][read][struct](Window) ---
+
+	n := buff.ReadFloat64() // read float64
+	target.Cost = n
+
+	// field version check
+	if uint8(16) <= version {
+		o := buff.ReadFloat64() // read float64
+		target.Adjustment = o
+
+	} else {
+		target.Adjustment = float64(0) // default
+	}
+
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  Coverage
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this Coverage instance
+// into a byte array
+func (target *Coverage) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this Coverage instance
+// into a byte array leveraging a predefined context.
+func (target *Coverage) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(DefaultCodecVersion) // version
+
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errA := target.Window.MarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
+	}
+	// --- [end][write][struct](Window) ---
+
+	if ctx.IsStringTable() {
+		a := ctx.Table.AddOrGet(target.Type)
+		buff.WriteInt(a) // write table index
+	} else {
+		buff.WriteString(target.Type) // write string
+	}
+	buff.WriteInt(target.Count) // write int
+	// --- [begin][write][reference](time.Time) ---
+	b, errB := target.Updated.MarshalBinary()
+	if errB != nil {
+		return errB
+	}
+	buff.WriteInt(len(b))
+	buff.WriteBytes(b)
+	// --- [end][write][reference](time.Time) ---
+
+	if target.Errors == 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.Errors)) // array length
+		for i := 0; i < len(target.Errors); i++ {
+			if ctx.IsStringTable() {
+				c := ctx.Table.AddOrGet(target.Errors[i])
+				buff.WriteInt(c) // write table index
+			} else {
+				buff.WriteString(target.Errors[i]) // write string
+			}
+		}
+		// --- [end][write][slice]([]string) ---
+
+	}
+	if target.Warnings == 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.Warnings)) // array length
+		for j := 0; j < len(target.Warnings); j++ {
+			if ctx.IsStringTable() {
+				d := ctx.Table.AddOrGet(target.Warnings[j])
+				buff.WriteInt(d) // write table index
+			} else {
+				buff.WriteString(target.Warnings[j]) // write string
+			}
+		}
+		// --- [end][write][slice]([]string) ---
+
+	}
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the Coverage type
+func (target *Coverage) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the Coverage type
+func (target *Coverage) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > DefaultCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling Coverage. Expected %d or less, got %d", DefaultCodecVersion, version)
+	}
+
+	// --- [begin][read][struct](Window) ---
+	a := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errA := a.UnmarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
+	}
+	target.Window = *a
+	// --- [end][read][struct](Window) ---
+
+	var c string
+	if ctx.IsStringTable() {
+		d := buff.ReadInt() // read string index
+		c = ctx.Table[d]
+	} else {
+		c = buff.ReadString() // read string
+	}
+	b := c
+	target.Type = b
+
+	e := buff.ReadInt() // read int
+	target.Count = e
+
+	// --- [begin][read][reference](time.Time) ---
+	f := &time.Time{}
+	g := buff.ReadInt()    // byte array length
+	h := buff.ReadBytes(g) // byte array
+	errB := f.UnmarshalBinary(h)
+	if errB != nil {
+		return errB
+	}
+	target.Updated = *f
+	// --- [end][read][reference](time.Time) ---
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.Errors = nil
+	} else {
+		// --- [begin][read][slice]([]string) ---
+		l := buff.ReadInt() // array len
+		k := make([]string, l)
+		for i := 0; i < l; i++ {
+			var m string
+			var o string
+			if ctx.IsStringTable() {
+				p := buff.ReadInt() // read string index
+				o = ctx.Table[p]
+			} else {
+				o = buff.ReadString() // read string
+			}
+			n := o
+			m = n
+
+			k[i] = m
+		}
+		target.Errors = k
+		// --- [end][read][slice]([]string) ---
+
+	}
+	if buff.ReadUInt8() == uint8(0) {
+		target.Warnings = nil
+	} else {
+		// --- [begin][read][slice]([]string) ---
+		r := buff.ReadInt() // array len
+		q := make([]string, r)
+		for j := 0; j < r; j++ {
+			var s string
+			var u string
+			if ctx.IsStringTable() {
+				w := buff.ReadInt() // read string index
+				u = ctx.Table[w]
+			} else {
+				u = buff.ReadString() // read string
+			}
+			t := u
+			s = t
+
+			q[j] = s
+		}
+		target.Warnings = q
+		// --- [end][read][slice]([]string) ---
+
+	}
+	return nil
+}
+
+//--------------------------------------------------------------------------
+//  CoverageSet
+//--------------------------------------------------------------------------
+
+// MarshalBinary serializes the internal properties of this CoverageSet instance
+// into a byte array
+func (target *CoverageSet) MarshalBinary() (data []byte, err error) {
+	ctx := &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  nil,
+	}
+
+	e := target.MarshalBinaryWithContext(ctx)
+	if e != nil {
+		return nil, e
+	}
+
+	encBytes := ctx.Buffer.Bytes()
+	return encBytes, nil
+}
+
+// MarshalBinaryWithContext serializes the internal properties of this CoverageSet instance
+// into a byte array leveraging a predefined context.
+func (target *CoverageSet) MarshalBinaryWithContext(ctx *EncodingContext) (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 := ctx.Buffer
+	buff.WriteUInt8(DefaultCodecVersion) // version
+
+	// --- [begin][write][struct](Window) ---
+	buff.WriteInt(0) // [compatibility, unused]
+	errA := target.Window.MarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
+	}
+	// --- [end][write][struct](Window) ---
+
+	if target.Items == nil {
+		buff.WriteUInt8(uint8(0)) // write nil byte
+	} else {
+		buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+		// --- [begin][write][map](map[string]*Coverage) ---
+		buff.WriteInt(len(target.Items)) // map length
+		for v, z := range target.Items {
+			if ctx.IsStringTable() {
+				a := ctx.Table.AddOrGet(v)
+				buff.WriteInt(a) // write table index
+			} else {
+				buff.WriteString(v) // write string
+			}
+			if z == nil {
+				buff.WriteUInt8(uint8(0)) // write nil byte
+			} else {
+				buff.WriteUInt8(uint8(1)) // write non-nil byte
+
+				// --- [begin][write][struct](Coverage) ---
+				buff.WriteInt(0) // [compatibility, unused]
+				errB := z.MarshalBinaryWithContext(ctx)
+				if errB != nil {
+					return errB
+				}
+				// --- [end][write][struct](Coverage) ---
+
+			}
+		}
+		// --- [end][write][map](map[string]*Coverage) ---
+
+	}
+	return nil
+}
+
+// UnmarshalBinary uses the data passed byte array to set all the internal properties of
+// the CoverageSet type
+func (target *CoverageSet) UnmarshalBinary(data []byte) error {
+	var table []string
+	buff := util.NewBufferFromBytes(data)
+
+	// string table header validation
+	if isBinaryTag(data, BinaryTagStringTable) {
+		buff.ReadBytes(len(BinaryTagStringTable)) // strip tag length
+		tl := buff.ReadInt()                      // table length
+		if tl > 0 {
+			table = make([]string, tl, tl)
+			for i := 0; i < tl; i++ {
+				table[i] = buff.ReadString()
+			}
+		}
+	}
+
+	ctx := &DecodingContext{
+		Buffer: buff,
+		Table:  table,
+	}
+
+	err := target.UnmarshalBinaryWithContext(ctx)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// UnmarshalBinaryWithContext uses the context containing a string table and binary buffer to set all the internal properties of
+// the CoverageSet type
+func (target *CoverageSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (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 := ctx.Buffer
+	version := buff.ReadUInt8()
+
+	if version > DefaultCodecVersion {
+		return fmt.Errorf("Invalid Version Unmarshaling CoverageSet. Expected %d or less, got %d", DefaultCodecVersion, version)
+	}
+
+	// --- [begin][read][struct](Window) ---
+	a := &Window{}
+	buff.ReadInt() // [compatibility, unused]
+	errA := a.UnmarshalBinaryWithContext(ctx)
+	if errA != nil {
+		return errA
+	}
+	target.Window = *a
+	// --- [end][read][struct](Window) ---
+
+	if buff.ReadUInt8() == uint8(0) {
+		target.Items = nil
+	} else {
+		// --- [begin][read][map](map[string]*Coverage) ---
+		c := buff.ReadInt() // map len
+		b := make(map[string]*Coverage, c)
+		for i := 0; i < c; i++ {
+			var v string
+			var e string
+			if ctx.IsStringTable() {
+				f := buff.ReadInt() // read string index
+				e = ctx.Table[f]
+			} else {
+				e = buff.ReadString() // read string
+			}
+			d := e
+			v = d
+
+			var z *Coverage
+			if buff.ReadUInt8() == uint8(0) {
+				z = nil
+			} else {
+				// --- [begin][read][struct](Coverage) ---
+				g := &Coverage{}
+				buff.ReadInt() // [compatibility, unused]
+				errB := g.UnmarshalBinaryWithContext(ctx)
+				if errB != nil {
+					return errB
+				}
+				z = g
+				// --- [end][read][struct](Coverage) ---
+
+			}
+			b[v] = z
+		}
+		target.Items = b
+		// --- [end][read][map](map[string]*Coverage) ---
+
+	}
 	return nil
 }
 

+ 116 - 4
pkg/kubecost/window.go

@@ -458,22 +458,22 @@ func (w Window) Hours() float64 {
 	return w.end.Sub(*w.start).Hours()
 }
 
-//IsEmpty a Window is empty if it does not have a start and an end
+// IsEmpty a Window is empty if it does not have a start and an end
 func (w Window) IsEmpty() bool {
 	return w.start == nil && w.end == nil
 }
 
-//HasDuration a Window has duration if neither start and end are not nil and not equal
+// HasDuration a Window has duration if neither start and end are not nil and not equal
 func (w Window) HasDuration() bool {
 	return !w.IsOpen() && !w.end.Equal(*w.Start())
 }
 
-//IsNegative a Window is negative if start and end are not null and end is before start
+// IsNegative a Window is negative if start and end are not null and end is before start
 func (w Window) IsNegative() bool {
 	return !w.IsOpen() && w.end.Before(*w.Start())
 }
 
-//IsOpen a Window is open if it has a nil start or end
+// IsOpen a Window is open if it has a nil start or end
 func (w Window) IsOpen() bool {
 	return w.start == nil || w.end == nil
 }
@@ -689,6 +689,118 @@ func (w Window) DurationOffsetStrings() (string, string) {
 	return timeutil.DurationOffsetStrings(dur, off)
 }
 
+// GetPercentInWindow Determine pct of item time contained the window.
+// determined by the overlap of the start/end with the given
+// window, which will be negative if there is no overlap. If
+// there is positive overlap, compare it with the total mins.
+//
+// e.g. here are the two possible scenarios as simplidied
+// 10m windows with dashes representing item's time running:
+//
+//  1. item falls entirely within one CloudCostItemSet window
+//     |     ---- |          |          |
+//     totalMins = 4.0
+//     pct := 4.0 / 4.0 = 1.0 for window 1
+//     pct := 0.0 / 4.0 = 0.0 for window 2
+//     pct := 0.0 / 4.0 = 0.0 for window 3
+//
+//  2. item overlaps multiple CloudCostItemSet windows
+//     |      ----|----------|--        |
+//     totalMins = 16.0
+//     pct :=  4.0 / 16.0 = 0.250 for window 1
+//     pct := 10.0 / 16.0 = 0.625 for window 2
+//     pct :=  2.0 / 16.0 = 0.125 for window 3
+func (w Window) GetPercentInWindow(itemStart time.Time, itemEnd time.Time) float64 {
+
+	s := itemStart
+	if s.Before(*w.Start()) {
+		s = *w.Start()
+	}
+
+	e := itemEnd
+	if e.After(*w.End()) {
+		e = *w.End()
+	}
+
+	mins := e.Sub(s).Minutes()
+	if mins <= 0.0 {
+		return 0.0
+	}
+
+	totalMins := itemEnd.Sub(itemStart).Minutes()
+
+	pct := mins / totalMins
+	return pct
+}
+
+// GetWindows returns a slice of Window with equal size between the given start and end. If windowSize does not evenly
+// divide the period between start and end, the last window is not added
+func GetWindows(start time.Time, end time.Time, windowSize time.Duration) ([]Window, error) {
+	// Ensure the range is evenly divisible into windows of the given duration
+	dur := end.Sub(start)
+	if int(dur.Minutes())%int(windowSize.Minutes()) != 0 {
+		return nil, fmt.Errorf("range not divisible by window: [%s, %s] by %s", start, end, windowSize)
+	}
+
+	// Ensure that provided times are multiples of the provided windowSize (e.g. midnight for daily windows, on the hour for hourly windows)
+	if start != start.Truncate(windowSize) {
+		return nil, fmt.Errorf("provided times are not divisible by provided window: [%s, %s] by %s", start, end, windowSize)
+	}
+
+	// Ensure timezones match
+	_, sz := start.Zone()
+	_, ez := end.Zone()
+	if sz != ez {
+		return nil, fmt.Errorf("range has mismatched timezones: %s, %s", start, end)
+	}
+	if sz != int(env.GetParsedUTCOffset().Seconds()) {
+		return nil, fmt.Errorf("range timezone doesn't match configured timezone: expected %s; found %ds", env.GetParsedUTCOffset(), sz)
+	}
+
+	// Build array of windows to cover the CloudCostItemSetRange
+	windows := []Window{}
+	s, e := start, start.Add(windowSize)
+	for !e.After(end) {
+		ws := s
+		we := e
+		windows = append(windows, NewWindow(&ws, &we))
+
+		s = s.Add(windowSize)
+		e = e.Add(windowSize)
+	}
+	return windows, nil
+}
+
+// GetWindowsForQueryWindow breaks up a window into an array of windows with a max size of queryWindow
+func GetWindowsForQueryWindow(start time.Time, end time.Time, queryWindow time.Duration) ([]Window, error) {
+	// Ensure timezones match
+	_, sz := start.Zone()
+	_, ez := end.Zone()
+	if sz != ez {
+		return nil, fmt.Errorf("range has mismatched timezones: %s, %s", start, end)
+	}
+	if sz != int(env.GetParsedUTCOffset().Seconds()) {
+		return nil, fmt.Errorf("range timezone doesn't match configured timezone: expected %s; found %ds", env.GetParsedUTCOffset(), sz)
+	}
+
+	// Build array of windows to cover the CloudCostItemSetRange
+	windows := []Window{}
+	s, e := start, start.Add(queryWindow)
+	for s.Before(end) {
+		ws := s
+		we := e
+		windows = append(windows, NewWindow(&ws, &we))
+
+		s = s.Add(queryWindow)
+		e = e.Add(queryWindow)
+		if e.After(end) {
+			e = end
+		}
+	}
+
+	return windows, nil
+}
+
 type BoundaryError struct {
 	Requested Window
 	Supported Window

+ 300 - 0
pkg/kubecost/window_test.go

@@ -2,6 +2,7 @@ package kubecost
 
 import (
 	"fmt"
+	"github.com/opencost/opencost/pkg/util/timeutil"
 	"strings"
 	"testing"
 	"time"
@@ -845,3 +846,302 @@ func TestWindow_Expand(t *testing.T) {
 
 // TODO
 // func TestWindow_String(t *testing.T) {}
+
+func TestWindow_GetPercentInWindow(t *testing.T) {
+	dayStart := time.Date(2022, 12, 6, 0, 0, 0, 0, time.UTC)
+	dayEnd := dayStart.Add(timeutil.Day)
+	window := NewClosedWindow(dayStart, dayEnd)
+
+	testcases := map[string]struct {
+		window    Window
+		itemStart time.Time
+		itemEnd   time.Time
+		expected  float64
+	}{
+		"matching start/matching end": {
+			window:    window,
+			itemStart: dayStart,
+			itemEnd:   dayEnd,
+			expected:  1.0,
+		},
+		"matching start/contained end": {
+			window:    window,
+			itemStart: dayStart,
+			itemEnd:   dayEnd.Add(-time.Hour * 6),
+			expected:  1.0,
+		},
+		"contained start/matching end": {
+			window:    window,
+			itemStart: dayStart.Add(time.Hour * 6),
+			itemEnd:   dayEnd,
+			expected:  1.0,
+		},
+		"contained start/contained end": {
+			window:    window,
+			itemStart: dayStart.Add(time.Hour * 6),
+			itemEnd:   dayEnd.Add(-time.Hour * 6),
+			expected:  1.0,
+		},
+		"before start/contained end": {
+			window:    window,
+			itemStart: dayStart.Add(-time.Hour * 12),
+			itemEnd:   dayEnd.Add(-time.Hour * 12),
+			expected:  0.5,
+		},
+		"before start/before end": {
+			window:    window,
+			itemStart: dayStart.Add(-time.Hour * 24),
+			itemEnd:   dayEnd.Add(-time.Hour * 24),
+			expected:  0.0,
+		},
+		"contained start/after end": {
+			window:    window,
+			itemStart: dayStart.Add(time.Hour * 12),
+			itemEnd:   dayEnd.Add(time.Hour * 12),
+			expected:  0.5,
+		},
+		"after start/after end": {
+			window:    window,
+			itemStart: dayStart.Add(time.Hour * 24),
+			itemEnd:   dayEnd.Add(time.Hour * 24),
+			expected:  0.0,
+		},
+		"before start/after end": {
+			window:    window,
+			itemStart: dayStart.Add(-time.Hour * 12),
+			itemEnd:   dayEnd.Add(time.Hour * 12),
+			expected:  0.5,
+		},
+	}
+	for name, tc := range testcases {
+		t.Run(name, func(t *testing.T) {
+			if actual := tc.window.GetPercentInWindow(tc.itemStart, tc.itemEnd); actual != tc.expected {
+				t.Errorf("GetPercentInWindow() = %v, want %v", actual, tc.expected)
+			}
+		})
+	}
+}
+
+func TestWindow_GetWindows(t *testing.T) {
+	dayStart := time.Date(2022, 12, 6, 0, 0, 0, 0, time.UTC)
+	dayEnd := dayStart.Add(timeutil.Day)
+	loc, _ := time.LoadLocation("America/Vancouver")
+	testCases := map[string]struct {
+		start       time.Time
+		end         time.Time
+		windowSize  time.Duration
+		expected    []Window
+		expectedErr bool
+	}{
+		"mismatching tz": {
+			start:       dayStart,
+			end:         dayEnd.In(loc),
+			windowSize:  time.Hour,
+			expected:    nil,
+			expectedErr: true,
+		},
+		"hour windows over 1 hours": {
+			start:      dayStart,
+			end:        dayStart.Add(time.Hour),
+			windowSize: time.Hour,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
+			},
+			expectedErr: false,
+		},
+		"hour windows over 3 hours": {
+			start:      dayStart,
+			end:        dayStart.Add(time.Hour * 3),
+			windowSize: time.Hour,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
+				NewClosedWindow(dayStart.Add(time.Hour), dayStart.Add(time.Hour*2)),
+				NewClosedWindow(dayStart.Add(time.Hour*2), dayStart.Add(time.Hour*3)),
+			},
+			expectedErr: false,
+		},
+		"hour windows off hour grid": {
+			start:       dayStart.Add(time.Minute),
+			end:         dayEnd.Add(time.Minute),
+			windowSize:  time.Hour,
+			expected:    nil,
+			expectedErr: true,
+		},
+		"hour windows range not divisible by hour": {
+			start:       dayStart,
+			end:         dayStart.Add(time.Minute * 90),
+			windowSize:  time.Hour,
+			expected:    nil,
+			expectedErr: true,
+		},
+		"day windows over 1 day": {
+			start:      dayStart,
+			end:        dayEnd,
+			windowSize: timeutil.Day,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayEnd),
+			},
+			expectedErr: false,
+		},
+		"day windows over 3 days": {
+			start:      dayStart,
+			end:        dayStart.Add(timeutil.Day * 3),
+			windowSize: timeutil.Day,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayStart.Add(timeutil.Day)),
+				NewClosedWindow(dayStart.Add(timeutil.Day), dayStart.Add(timeutil.Day*2)),
+				NewClosedWindow(dayStart.Add(timeutil.Day*2), dayStart.Add(timeutil.Day*3)),
+			},
+			expectedErr: false,
+		},
+		"day windows off day grid": {
+			start:       dayStart.Add(time.Hour),
+			end:         dayEnd.Add(time.Hour),
+			windowSize:  timeutil.Day,
+			expected:    nil,
+			expectedErr: true,
+		},
+		"day windows range not divisible by day": {
+			start:       dayStart,
+			end:         dayEnd.Add(time.Hour),
+			windowSize:  timeutil.Day,
+			expected:    nil,
+			expectedErr: true,
+		},
+	}
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			actual, err := GetWindows(tc.start, tc.end, tc.windowSize)
+			if (err != nil) != tc.expectedErr {
+				t.Errorf("GetWindows() error = %v, expectedErr %v", err, tc.expectedErr)
+				return
+			}
+			if len(tc.expected) != len(actual) {
+				t.Errorf("GetWindows() []window has incorrect length expected: %d, actual: %d", len(tc.expected), len(actual))
+			}
+			for i, actualWindow := range actual {
+				expectedWindow := tc.expected[i]
+				if !actualWindow.Equal(expectedWindow) {
+					t.Errorf("GetWindow() window at index %d were not equal expected: %s, actual %s", i, expectedWindow.String(), actualWindow)
+				}
+			}
+		})
+	}
+}
+
+func TestWindow_GetWindowsForQueryWindow(t *testing.T) {
+	dayStart := time.Date(2022, 12, 6, 0, 0, 0, 0, time.UTC)
+	dayEnd := dayStart.Add(timeutil.Day)
+	loc, _ := time.LoadLocation("America/Vancouver")
+	testCases := map[string]struct {
+		start       time.Time
+		end         time.Time
+		windowSize  time.Duration
+		expected    []Window
+		expectedErr bool
+	}{
+		"mismatching tz": {
+			start:       dayStart,
+			end:         dayEnd.In(loc),
+			windowSize:  time.Hour,
+			expected:    nil,
+			expectedErr: true,
+		},
+		"hour windows over 1 hours": {
+			start:      dayStart,
+			end:        dayStart.Add(time.Hour),
+			windowSize: time.Hour,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
+			},
+			expectedErr: false,
+		},
+		"hour windows over 3 hours": {
+			start:      dayStart,
+			end:        dayStart.Add(time.Hour * 3),
+			windowSize: time.Hour,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
+				NewClosedWindow(dayStart.Add(time.Hour), dayStart.Add(time.Hour*2)),
+				NewClosedWindow(dayStart.Add(time.Hour*2), dayStart.Add(time.Hour*3)),
+			},
+			expectedErr: false,
+		},
+		"hour windows off hour grid": {
+			start:      dayStart.Add(time.Minute),
+			end:        dayStart.Add(time.Minute * 61),
+			windowSize: time.Hour,
+			expected: []Window{
+				NewClosedWindow(dayStart.Add(time.Minute), dayStart.Add(time.Minute*61)),
+			},
+			expectedErr: false,
+		},
+		"hour windows range not divisible by hour": {
+			start:      dayStart,
+			end:        dayStart.Add(time.Minute * 90),
+			windowSize: time.Hour,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayStart.Add(time.Hour)),
+				NewClosedWindow(dayStart.Add(time.Hour), dayStart.Add(time.Minute*90)),
+			},
+			expectedErr: false,
+		},
+		"day windows over 1 day": {
+			start:      dayStart,
+			end:        dayEnd,
+			windowSize: timeutil.Day,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayEnd),
+			},
+			expectedErr: false,
+		},
+		"day windows over 3 days": {
+			start:      dayStart,
+			end:        dayStart.Add(timeutil.Day * 3),
+			windowSize: timeutil.Day,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayStart.Add(timeutil.Day)),
+				NewClosedWindow(dayStart.Add(timeutil.Day), dayStart.Add(timeutil.Day*2)),
+				NewClosedWindow(dayStart.Add(timeutil.Day*2), dayStart.Add(timeutil.Day*3)),
+			},
+			expectedErr: false,
+		},
+		"day windows off day grid": {
+			start:      dayStart.Add(time.Hour),
+			end:        dayEnd.Add(time.Hour),
+			windowSize: timeutil.Day,
+			expected: []Window{
+				NewClosedWindow(dayStart.Add(time.Hour), dayEnd.Add(time.Hour)),
+			},
+			expectedErr: false,
+		},
+		"day windows range not divisible by day": {
+			start:      dayStart,
+			end:        dayEnd.Add(time.Hour),
+			windowSize: timeutil.Day,
+			expected: []Window{
+				NewClosedWindow(dayStart, dayEnd),
+				NewClosedWindow(dayEnd, dayEnd.Add(time.Hour)),
+			},
+			expectedErr: false,
+		},
+	}
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			actual, err := GetWindowsForQueryWindow(tc.start, tc.end, tc.windowSize)
+			if (err != nil) != tc.expectedErr {
+				t.Errorf("GetWindowsForQueryWindow() error = %v, expectedErr %v", err, tc.expectedErr)
+				return
+			}
+			if len(tc.expected) != len(actual) {
+				t.Errorf("GetWindowsForQueryWindow() []window has incorrect length expected: %d, actual: %d", len(tc.expected), len(actual))
+			}
+			for i, actualWindow := range actual {
+				expectedWindow := tc.expected[i]
+				if !actualWindow.Equal(expectedWindow) {
+					t.Errorf("GetWindowsForQueryWindow() window at index %d were not equal expected: %s, actual %s", i, expectedWindow.String(), actualWindow)
+				}
+			}
+		})
+	}
+}

+ 15 - 0
pkg/util/mathutil/mathutil.go

@@ -0,0 +1,15 @@
+package mathutil
+
+import "math"
+
+func Approximately(exp, act float64) bool {
+	return ApproximatelyPct(exp, act, 0.0001) // within 0.1%
+}
+
+func ApproximatelyPct(exp, act, pct float64) bool {
+	delta := (exp * pct)
+	if delta < 0.00001 {
+		delta = 0.00001
+	}
+	return math.Abs(exp-act) < delta
+}

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

@@ -34,6 +34,9 @@ const (
 
 	// DaysPerMonth expresses the amount of days in a month
 	DaysPerMonth = 30.42
+
+	// Day expresses 24 hours
+	Day = time.Hour * 24.0
 )
 
 // DurationString converts a duration to a Prometheus-compatible string in