Explorar el Código

Merge branch 'develop' into AjayTripathy-fix-receive-transfer

Ajay Tripathy hace 3 años
padre
commit
f7bbd26f3b

+ 5 - 5
go.mod

@@ -25,6 +25,7 @@ require (
 	github.com/davecgh/go-spew v1.1.1
 	github.com/getsentry/sentry-go v0.6.1
 	github.com/goccy/go-json v0.9.4
+	github.com/google/go-cmp v0.5.9
 	github.com/google/uuid v1.3.0
 	github.com/hashicorp/go-multierror v1.0.0
 	github.com/json-iterator/go v1.1.12
@@ -48,7 +49,7 @@ require (
 	golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
 	golang.org/x/oauth2 v0.1.0
 	golang.org/x/sync v0.1.0
-	golang.org/x/text v0.5.0
+	golang.org/x/text v0.8.0
 	google.golang.org/api v0.102.0
 	gopkg.in/yaml.v2 v2.4.0
 	k8s.io/api v0.25.3
@@ -97,7 +98,6 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/gnostic v0.5.7-v3refs // indirect
-	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
 	github.com/googleapis/gax-go/v2 v2.6.0 // indirect
@@ -136,9 +136,9 @@ require (
 	go.opencensus.io v0.23.0 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	golang.org/x/crypto v0.3.0 // indirect
-	golang.org/x/net v0.4.0 // indirect
-	golang.org/x/sys v0.3.0 // indirect
-	golang.org/x/term v0.3.0 // indirect
+	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/term v0.6.0 // indirect
 	golang.org/x/time v0.1.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect

+ 8 - 4
go.sum

@@ -771,8 +771,9 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -869,14 +870,16 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -887,8 +890,9 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

+ 27 - 24
pkg/cloud/awsprovider.go

@@ -179,8 +179,8 @@ type AWS struct {
 	Config                      *ProviderConfig
 	serviceAccountChecks        *ServiceAccountChecks
 	clusterManagementPrice      float64
-	clusterAccountId            string
 	clusterRegion               string
+	clusterAccountID            string
 	clusterProvisioner          string
 	*CustomProvider
 }
@@ -1342,38 +1342,41 @@ func (aws *AWS) NodePricing(k Key) (*Node, error) {
 
 // ClusterInfo returns an object that represents the cluster. TODO: actually return the name of the cluster. Blocked on cluster federation.
 func (awsProvider *AWS) ClusterInfo() (map[string]string, error) {
-	defaultClusterName := "AWS Cluster #1"
+
 	c, err := awsProvider.GetConfig()
 	if err != nil {
 		return nil, err
 	}
 
-	remoteEnabled := env.IsRemoteEnabled()
-
-	makeStructure := func(clusterName string) (map[string]string, error) {
-		m := make(map[string]string)
-		m["name"] = clusterName
-		m["provider"] = kubecost.AWSProvider
-		m["account"] = c.AthenaProjectID // this value requires configuration but is unavailable else where
-		m["region"] = awsProvider.clusterRegion
-		m["id"] = env.GetClusterID()
-		m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
-		m["provisioner"] = awsProvider.clusterProvisioner
-		return m, nil
-	}
-
-	if c.ClusterName != "" {
-		return makeStructure(c.ClusterName)
+	// Determine cluster name
+	clusterName := c.ClusterName
+	if clusterName == "" {
+		awsClusterID := env.GetAWSClusterID()
+		if awsClusterID != "" {
+			log.Infof("Returning \"%s\" as ClusterName", awsClusterID)
+			clusterName = awsClusterID
+		} else {
+			log.Infof("Unable to sniff out cluster ID, perhaps set $%s to force one", env.AWSClusterIDEnvVar)
+			clusterName = "AWS Cluster #1"
+		}
 	}
 
-	maybeClusterId := env.GetAWSClusterID()
-	if len(maybeClusterId) != 0 {
-		log.Infof("Returning \"%s\" as ClusterName", maybeClusterId)
-		return makeStructure(maybeClusterId)
+	// this value requires configuration but is unavailable else where
+	clusterAccountID := c.ClusterAccountID
+	// Use AthenaProjectID if Cluster Account is not set to support older configs
+	if clusterAccountID == "" {
+		clusterAccountID = c.AthenaProjectID
 	}
 
-	log.Infof("Unable to sniff out cluster ID, perhaps set $%s to force one", env.AWSClusterIDEnvVar)
-	return makeStructure(defaultClusterName)
+	m := make(map[string]string)
+	m["name"] = clusterName
+	m["provider"] = kubecost.AWSProvider
+	m["account"] = clusterAccountID
+	m["region"] = awsProvider.clusterRegion
+	m["id"] = env.GetClusterID()
+	m["remoteReadEnabled"] = strconv.FormatBool(env.IsRemoteEnabled())
+	m["provisioner"] = awsProvider.clusterProvisioner
+	return m, nil
 }
 
 // updates the authentication to the latest values (via config or secret)

+ 2 - 2
pkg/cloud/azureprovider.go

@@ -396,7 +396,7 @@ type Azure struct {
 	Config                         *ProviderConfig
 	serviceAccountChecks           *ServiceAccountChecks
 	RateCardPricingError           error
-	clusterAccountId               string
+	clusterAccountID               string
 	clusterRegion                  string
 	loadedAzureSecret              bool
 	azureSecret                    *AzureServiceKey
@@ -1345,7 +1345,7 @@ func (az *Azure) ClusterInfo() (map[string]string, error) {
 		m["name"] = c.ClusterName
 	}
 	m["provider"] = kubecost.AzureProvider
-	m["account"] = az.clusterAccountId
+	m["account"] = az.clusterAccountID
 	m["region"] = az.clusterRegion
 	m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
 	m["id"] = env.GetClusterID()

+ 4 - 0
pkg/cloud/customprovider.go

@@ -30,6 +30,8 @@ type CustomProvider struct {
 	SpotLabelValue          string
 	GPULabel                string
 	GPULabelValue           string
+	clusterRegion           string
+	clusterAccountID        string
 	DownloadPricingDataLock sync.RWMutex
 	Config                  *ProviderConfig
 }
@@ -117,6 +119,8 @@ func (cp *CustomProvider) ClusterInfo() (map[string]string, error) {
 		m["name"] = conf.ClusterName
 	}
 	m["provider"] = kubecost.CustomProvider
+	m["region"] = cp.clusterRegion
+	m["account"] = cp.clusterAccountID
 	m["id"] = env.GetClusterID()
 	return m, nil
 }

+ 4 - 2
pkg/cloud/gcpprovider.go

@@ -102,8 +102,9 @@ type GCP struct {
 	ValidPricingKeys        map[string]bool
 	metadataClient          *metadata.Client
 	clusterManagementPrice  float64
-	clusterProjectId        string
 	clusterRegion           string
+	clusterAccountID        string
+	clusterProjectID        string
 	clusterProvisioner      string
 	*CustomProvider
 }
@@ -333,8 +334,9 @@ func (gcp *GCP) ClusterInfo() (map[string]string, error) {
 	m := make(map[string]string)
 	m["name"] = attribute
 	m["provider"] = kubecost.GCPProvider
-	m["project"] = gcp.clusterProjectId
 	m["region"] = gcp.clusterRegion
+	m["account"] = gcp.clusterAccountID
+	m["project"] = gcp.clusterProjectID
 	m["provisioner"] = gcp.clusterProvisioner
 	m["id"] = env.GetClusterID()
 	m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)

+ 22 - 9
pkg/cloud/provider.go

@@ -221,6 +221,7 @@ type CustomPricing struct {
 	NegotiatedDiscount           string `json:"negotiatedDiscount"`
 	SharedOverhead               string `json:"sharedOverhead"`
 	ClusterName                  string `json:"clusterName"`
+	ClusterAccountID             string `json:"clusterAccount,omitempty"`
 	SharedNamespaces             string `json:"sharedNamespaces"`
 	SharedLabelNames             string `json:"sharedLabelNames"`
 	SharedLabelValues            string `json:"sharedLabelValues"`
@@ -475,6 +476,11 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 	}
 
 	cp := getClusterProperties(nodes[0])
+	providerConfig := NewProviderConfig(config, cp.configFileName)
+	// If ClusterAccount is set apply it to the cluster properties
+	if providerConfig.customPricing != nil && providerConfig.customPricing.ClusterAccountID != "" {
+		cp.accountID = providerConfig.customPricing.ClusterAccountID
+	}
 
 	switch cp.provider {
 	case kubecost.CSVProvider:
@@ -482,8 +488,10 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 		return &CSVProvider{
 			CSVLocation: env.GetCSVPath(),
 			CustomProvider: &CustomProvider{
-				Clientset: cache,
-				Config:    NewProviderConfig(config, cp.configFileName),
+				Clientset:        cache,
+				clusterRegion:    cp.region,
+				clusterAccountID: cp.accountID,
+				Config:           NewProviderConfig(config, cp.configFileName),
 			},
 		}, nil
 	case kubecost.GCPProvider:
@@ -496,7 +504,8 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 			APIKey:           apiKey,
 			Config:           NewProviderConfig(config, cp.configFileName),
 			clusterRegion:    cp.region,
-			clusterProjectId: cp.projectID,
+			clusterAccountID: cp.accountID,
+			clusterProjectID: cp.projectID,
 			metadataClient: metadata.NewClient(&http.Client{
 				Transport: httputil.NewUserAgentTransport("kubecost", http.DefaultTransport),
 			}),
@@ -507,7 +516,7 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 			Clientset:            cache,
 			Config:               NewProviderConfig(config, cp.configFileName),
 			clusterRegion:        cp.region,
-			clusterAccountId:     cp.accountID,
+			clusterAccountID:     cp.accountID,
 			serviceAccountChecks: NewServiceAccountChecks(),
 		}, nil
 	case kubecost.AzureProvider:
@@ -516,7 +525,7 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 			Clientset:            cache,
 			Config:               NewProviderConfig(config, cp.configFileName),
 			clusterRegion:        cp.region,
-			clusterAccountId:     cp.accountID,
+			clusterAccountID:     cp.accountID,
 			serviceAccountChecks: NewServiceAccountChecks(),
 		}, nil
 	case kubecost.AlibabaProvider:
@@ -531,15 +540,19 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 	case kubecost.ScalewayProvider:
 		log.Info("Found ProviderID starting with \"scaleway\", using Scaleway Provider")
 		return &Scaleway{
-			Clientset: cache,
-			Config:    NewProviderConfig(config, cp.configFileName),
+			Clientset:        cache,
+			clusterRegion:    cp.region,
+			clusterAccountID: cp.accountID,
+			Config:           NewProviderConfig(config, cp.configFileName),
 		}, nil
 
 	default:
 		log.Info("Unsupported provider, falling back to default")
 		return &CustomProvider{
-			Clientset: cache,
-			Config:    NewProviderConfig(config, cp.configFileName),
+			Clientset:        cache,
+			clusterRegion:    cp.region,
+			clusterAccountID: cp.accountID,
+			Config:           NewProviderConfig(config, cp.configFileName),
 		}, nil
 	}
 }

+ 4 - 0
pkg/cloud/scalewayprovider.go

@@ -36,6 +36,8 @@ type Scaleway struct {
 	Clientset               clustercache.ClusterCache
 	Config                  *ProviderConfig
 	Pricing                 map[string]*ScalewayPricing
+	clusterRegion           string
+	clusterAccountID        string
 	DownloadPricingDataLock sync.RWMutex
 }
 
@@ -285,6 +287,8 @@ func (scw *Scaleway) ClusterInfo() (map[string]string, error) {
 		m["name"] = c.ClusterName
 	}
 	m["provider"] = kubecost.ScalewayProvider
+	m["region"] = scw.clusterRegion
+	m["account"] = scw.clusterAccountID
 	m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
 	m["id"] = env.GetClusterID()
 	return m, nil

+ 1 - 1
pkg/cmd/agent/agent.go

@@ -181,7 +181,7 @@ func Execute(opts *AgentOpts) error {
 
 	clusterCache.SetConfigMapUpdateFunc(watchConfigFunc)
 
-	configPrefix := env.GetConfigPathWithDefault("/var/configs/")
+	configPrefix := env.GetConfigPathWithDefault(env.DefaultConfigMountPath)
 
 	// Initialize cluster exporting if it's enabled
 	if env.IsExportClusterCacheEnabled() {

+ 1 - 1
pkg/costmodel/router.go

@@ -1363,7 +1363,7 @@ func (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps http
 
 	key := r.PostForm.Get("key")
 	k := []byte(key)
-	err := os.WriteFile(path.Join(env.GetConfigPathWithDefault("/var/configs/"), "key.json"), k, 0644)
+	err := os.WriteFile(path.Join(env.GetConfigPathWithDefault(env.DefaultConfigMountPath), "key.json"), k, 0644)
 	if err != nil {
 		fmt.Fprintf(w, "Error writing service key: "+err.Error())
 	}

+ 6 - 4
pkg/env/costmodelenv.go

@@ -99,6 +99,8 @@ const (
 	regionOverrideList = "REGION_OVERRIDE_LIST"
 )
 
+const DefaultConfigMountPath = "/var/configs"
+
 var offsetRegex = regexp.MustCompile(`^(\+|-)(\d\d):(\d\d)$`)
 
 func IsETLReadOnlyMode() bool {
@@ -314,10 +316,10 @@ func GetCSVPath() string {
 	return Get(CSVPathEnvVar, "")
 }
 
-// GetConfigPath returns the environment variable value for ConfigPathEnvVar which represents the cost
-// model configuration path
-func GetConfigPath() string {
-	return Get(ConfigPathEnvVar, "")
+// GetCostAnalyzerVolumeMountPath is an alias of GetConfigPath, which returns the mount path for the
+// Cost Analyzer volume, which stores configs, persistent data, etc.
+func GetCostAnalyzerVolumeMountPath() string {
+	return GetConfigPathWithDefault(DefaultConfigMountPath)
 }
 
 // GetConfigPath returns the environment variable value for ConfigPathEnvVar which represents the cost

+ 2 - 3
pkg/kubecost/allocation_test.go

@@ -1893,7 +1893,7 @@ func TestAllocationSetRange_AccumulateBy_Hour(t *testing.T) {
 	currentHourAS.Set(NewMockUnitAllocation("", currentHour, time.Hour, nil))
 
 	asr := NewAllocationSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
-	asr, err := asr.Accumulate(AccumulateOptionNone)
+	asr, err := asr.Accumulate(AccumulateOptionHour)
 	if err != nil {
 		t.Fatalf("unexpected error calling accumulateBy: %s", err)
 	}
@@ -2048,8 +2048,7 @@ func TestAllocationSetRange_AccumulateBy_Month(t *testing.T) {
 
 	nextAS := NewAllocationSet(nextMonth1stDay, nextMonth2ndDay)
 	nextAS.Set(NewMockUnitAllocation("", nextMonth1stDay, day, nil))
-	// check there are two allocation sets
-	// check the windows are one month or less
+
 	asr := NewAllocationSetRange(prev1AS, prev2AS, prev3AS, nextAS)
 	asr, err := asr.Accumulate(AccumulateOptionMonth)
 	if err != nil {

+ 170 - 2
pkg/kubecost/asset.go

@@ -3256,9 +3256,166 @@ func NewAssetSetRange(assets ...*AssetSet) *AssetSetRange {
 	}
 }
 
+func (asr *AssetSetRange) Accumulate(accumulateBy AccumulateOption) (*AssetSetRange, error) {
+	switch accumulateBy {
+	case AccumulateOptionNone:
+		return asr.accumulateByNone()
+	case AccumulateOptionAll:
+		return asr.accumulateByAll()
+	case AccumulateOptionHour:
+		return asr.accumulateByHour()
+	case AccumulateOptionDay:
+		return asr.accumulateByDay()
+	case AccumulateOptionWeek:
+		return asr.accumulateByWeek()
+	case AccumulateOptionMonth:
+		return asr.accumulateByMonth()
+	default:
+		// ideally, this should never happen
+		return nil, fmt.Errorf("unexpected error, invalid accumulateByType: %s", accumulateBy)
+	}
+}
+
+func (asr *AssetSetRange) accumulateByNone() (*AssetSetRange, error) {
+	return asr.clone(), nil
+}
+
+func (asr *AssetSetRange) accumulateByAll() (*AssetSetRange, error) {
+	var err error
+	var as *AssetSet
+	as, err = asr.newAccumulation()
+	if err != nil {
+		return nil, fmt.Errorf("error accumulating all:%s", err)
+	}
+
+	accumulated := NewAssetSetRange(as)
+	return accumulated, nil
+}
+
+func (asr *AssetSetRange) accumulateByHour() (*AssetSetRange, error) {
+	// ensure that the asset sets have a 1-hour window, if a set exists
+	if len(asr.Assets) > 0 && asr.Assets[0].Window.Duration() != time.Hour {
+		return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", asr.Assets[0].Window.Duration())
+	}
+
+	return asr.clone(), nil
+}
+
+func (asr *AssetSetRange) accumulateByDay() (*AssetSetRange, error) {
+	// if the asset set window is 1-day, just return the existing asset set range
+	if len(asr.Assets) > 0 && asr.Assets[0].Window.Duration() == time.Hour*24 {
+		return asr, nil
+	}
+
+	var toAccumulate *AssetSetRange
+	result := NewAssetSetRange()
+	for i, as := range asr.Assets {
+
+		if as.Window.Duration() != time.Hour {
+			return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", as.Window.Duration())
+		}
+
+		hour := as.Window.Start().Hour()
+
+		if toAccumulate == nil {
+			toAccumulate = NewAssetSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAssetSetRange(asAccumulated)
+
+		if hour == 23 || i == len(asr.Assets)-1 {
+			if length := len(toAccumulate.Assets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Assets[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (asr *AssetSetRange) accumulateByMonth() (*AssetSetRange, error) {
+	var toAccumulate *AssetSetRange
+	result := NewAssetSetRange()
+	for i, as := range asr.Assets {
+		if as.Window.Duration() != time.Hour*24 {
+			return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
+		}
+
+		_, month, _ := as.Window.Start().Date()
+		_, nextDayMonth, _ := as.Window.Start().Add(time.Hour * 24).Date()
+
+		if toAccumulate == nil {
+			toAccumulate = NewAssetSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAssetSetRange(asAccumulated)
+
+		// either the month has ended, or there are no more asset sets
+		if month != nextDayMonth || i == len(asr.Assets)-1 {
+			if length := len(toAccumulate.Assets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Assets[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (asr *AssetSetRange) accumulateByWeek() (*AssetSetRange, error) {
+	var toAccumulate *AssetSetRange
+	result := NewAssetSetRange()
+	for i, as := range asr.Assets {
+		if as.Window.Duration() != time.Hour*24 {
+			return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
+		}
+
+		dayOfWeek := as.Window.Start().Weekday()
+
+		if toAccumulate == nil {
+			toAccumulate = NewAssetSetRange()
+			as = as.Clone()
+		}
+
+		toAccumulate.Append(as)
+		asAccumulated, err := toAccumulate.accumulate()
+		if err != nil {
+			return nil, fmt.Errorf("error accumulating result: %s", err)
+		}
+		toAccumulate = NewAssetSetRange(asAccumulated)
+
+		// current assumption is the week always ends on Saturday, or there are no more asset sets
+		if dayOfWeek == time.Saturday || i == len(asr.Assets)-1 {
+			if length := len(toAccumulate.Assets); length != 1 {
+				return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
+			}
+			result.Append(toAccumulate.Assets[0])
+			toAccumulate = nil
+		}
+	}
+	return result, nil
+}
+
+func (asr *AssetSetRange) AccumulateToAssetSet() (*AssetSet, error) {
+	return asr.accumulate()
+}
+
 // Accumulate sums each AssetSet in the given range, returning a single cumulative
 // AssetSet for the entire range.
-func (asr *AssetSetRange) Accumulate() (*AssetSet, error) {
+func (asr *AssetSetRange) accumulate() (*AssetSet, error) {
 	var assetSet *AssetSet
 	var err error
 
@@ -3274,7 +3431,7 @@ func (asr *AssetSetRange) Accumulate() (*AssetSet, error) {
 
 // NewAccumulation clones the first available AssetSet to use as the data structure to
 // accumulate the remaining data. This leaves the original AssetSetRange intact.
-func (asr *AssetSetRange) NewAccumulation() (*AssetSet, error) {
+func (asr *AssetSetRange) newAccumulation() (*AssetSet, error) {
 	var assetSet *AssetSet
 	var err error
 
@@ -3618,6 +3775,17 @@ func (asr *AssetSetRange) TotalCost() float64 {
 	return tc
 }
 
+func (asr *AssetSetRange) clone() *AssetSetRange {
+	asrClone := NewAssetSetRange()
+	asrClone.FromStore = asr.FromStore
+	for _, as := range asr.Assets {
+		asClone := as.Clone()
+		asrClone.Append(asClone)
+	}
+
+	return asrClone
+}
+
 // This is a helper type. The Asset API returns a json which cannot be natively
 // unmarshaled into any Asset struct. Therefore, this struct IN COMBINATION WITH
 // DESERIALIZATION LOGIC DEFINED IN asset_json.go can unmarshal a json directly

+ 237 - 31
pkg/kubecost/asset_test.go

@@ -673,7 +673,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	// 1  Single-aggregation
 
 	// 1a []AssetProperty=[Cluster]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetClusterProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -685,7 +685,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1b []AssetProperty=[Type]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetTypeProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -697,7 +697,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1c []AssetProperty=[Nil]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -707,7 +707,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1d []AssetProperty=nil
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy(nil, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -727,7 +727,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	}, nil)
 
 	// 1e aggregateBy []string=["label:test"]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{"label:test"}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -740,7 +740,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	// 2  Multi-aggregation
 
 	// 2a []AssetProperty=[Cluster,Type]
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetClusterProp), string(AssetTypeProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSet.AggregateBy: unexpected error: %s", err)
@@ -758,7 +758,7 @@ func TestAssetSet_AggregateBy(t *testing.T) {
 	// 3  Share resources
 
 	// 3a Shared hourly cost > 0.0
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	err = as.AggregateBy([]string{string(AssetTypeProp)}, &AssetAggregationOptions{
 		SharedHourlyCosts: map[string]float64{"shared1": 0.5},
 	})
@@ -784,7 +784,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	var err error
 
 	// Assert success of a simple match of Type and ProviderID
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	query = NewNode("", "", "gcp-node3", s, e, w)
 	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err != nil {
@@ -792,7 +792,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	}
 
 	// Assert error of a simple non-match of Type and ProviderID
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	query = NewNode("", "", "aws-node3", s, e, w)
 	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err == nil {
@@ -800,7 +800,7 @@ func TestAssetSet_FindMatch(t *testing.T) {
 	}
 
 	// Assert error of matching ProviderID, but not Type
-	as = GenerateMockAssetSet(startYesterday)
+	as = GenerateMockAssetSet(startYesterday, day)
 	query = NewCloud(ComputeCategory, "gcp-node3", s, e, w)
 	match, err = as.FindMatch(query, []string{string(AssetTypeProp), string(AssetProviderIDProp)}, nil)
 	if err == nil {
@@ -854,7 +854,7 @@ func TestAssetSet_ReconciliationMatchMap(t *testing.T) {
 	endYesterday := time.Now().UTC().Truncate(day)
 	startYesterday := endYesterday.Add(-day)
 
-	as := GenerateMockAssetSet(startYesterday)
+	as := GenerateMockAssetSet(startYesterday, day)
 	matchMap := as.ReconciliationMatchMap()
 
 	// Determine the number of assets by provider ID
@@ -876,7 +876,7 @@ func TestAssetSet_ReconciliationMatchMap(t *testing.T) {
 	}
 }
 
-func TestAssetSetRange_Accumulate(t *testing.T) {
+func TestAssetSetRange_AccumulateToAssetSet(t *testing.T) {
 	endYesterday := time.Now().UTC().Truncate(day)
 	startYesterday := endYesterday.Add(-day)
 
@@ -891,12 +891,12 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	var err error
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy(nil, nil)
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -915,12 +915,12 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	}, nil)
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy([]string{}, nil)
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -929,15 +929,15 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	}, nil)
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy([]string{string(AssetTypeProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -948,15 +948,15 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	}, nil)
 
 	asr = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 	err = asr.AggregateBy([]string{string(AssetClusterProp)}, nil)
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -970,12 +970,12 @@ func TestAssetSetRange_Accumulate(t *testing.T) {
 	// is empty (this was previously an issue)
 	asr = NewAssetSetRange(
 		NewAssetSet(startD0, startD1),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 
 	err = asr.AggregateBy([]string{string(AssetTypeProp)}, nil)
-	as, err = asr.Accumulate()
+	as, err = asr.AccumulateToAssetSet()
 	if err != nil {
 		t.Fatalf("AssetSetRange.AggregateBy: unexpected error: %s", err)
 	}
@@ -1479,3 +1479,209 @@ func TestAssetSetRange_MarshalJSON(t *testing.T) {
 		// asset don't unmarshal back from json
 	}
 }
+
+func TestAssetSetRange_AccumulateBy_None(t *testing.T) {
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionNone)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 5 {
+		t.Fatalf("expected 5 asset sets, got:%d", len(asr.Assets))
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_All(t *testing.T) {
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionAll)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 1 {
+		t.Fatalf("expected 1 asset set, got:%d", len(asr.Assets))
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Hour(t *testing.T) {
+	ago4h := time.Now().UTC().Truncate(time.Hour).Add(-4 * time.Hour)
+	ago3h := time.Now().UTC().Truncate(time.Hour).Add(-3 * time.Hour)
+	ago2h := time.Now().UTC().Truncate(time.Hour).Add(-2 * time.Hour)
+	ago1h := time.Now().UTC().Truncate(time.Hour).Add(-time.Hour)
+	currentHour := time.Now().UTC().Truncate(time.Hour)
+
+	ago4hAS := GenerateMockAssetSet(ago4h, time.Hour)
+	ago3hAS := GenerateMockAssetSet(ago3h, time.Hour)
+	ago2hAS := GenerateMockAssetSet(ago2h, time.Hour)
+	ago1hAS := GenerateMockAssetSet(ago1h, time.Hour)
+	currentHourAS := GenerateMockAssetSet(currentHour, time.Hour)
+
+	asr := NewAssetSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionHour)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 5 {
+		t.Fatalf("expected 5 asset sets, got:%d", len(asr.Assets))
+	}
+
+	allocMap := asr.Assets[0].Assets
+	alloc := allocMap["__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4"]
+	if alloc.Minutes() != 60.0 {
+		t.Errorf("accumulating asset set range: expected %f minutes; actual %f", 60.0, alloc.Minutes())
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Day_From_Day(t *testing.T) {
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionDay)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 5 {
+		t.Fatalf("expected 5 asset sets, got:%d", len(asr.Assets))
+	}
+
+	allocMap := asr.Assets[0].Assets
+	alloc := allocMap["__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4"]
+	if alloc.Minutes() != 1440.0 {
+		t.Errorf("accumulating asset set range: expected %f minutes; actual %f", 1440.0, alloc.Minutes())
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Day_From_Hours(t *testing.T) {
+	ago4h := time.Now().UTC().Truncate(time.Hour).Add(-4 * time.Hour)
+	ago3h := time.Now().UTC().Truncate(time.Hour).Add(-3 * time.Hour)
+	ago2h := time.Now().UTC().Truncate(time.Hour).Add(-2 * time.Hour)
+	ago1h := time.Now().UTC().Truncate(time.Hour).Add(-time.Hour)
+	currentHour := time.Now().UTC().Truncate(time.Hour)
+
+	ago4hAS := GenerateMockAssetSet(ago4h, time.Hour)
+	ago3hAS := GenerateMockAssetSet(ago3h, time.Hour)
+	ago2hAS := GenerateMockAssetSet(ago2h, time.Hour)
+	ago1hAS := GenerateMockAssetSet(ago1h, time.Hour)
+	currentHourAS := GenerateMockAssetSet(currentHour, time.Hour)
+
+	asr := NewAssetSetRange(ago4hAS, ago3hAS, ago2hAS, ago1hAS, currentHourAS)
+	asr, err := asr.Accumulate(AccumulateOptionDay)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 1 && len(asr.Assets) != 2 {
+		t.Fatalf("expected 1 allocation set, got:%d", len(asr.Assets))
+	}
+
+	allocMap := asr.Assets[0].Assets
+	alloc := allocMap["__undefined__/__undefined__/__undefined__/Storage/cluster2/Disk/Kubernetes/gcp-disk4/disk4"]
+	if alloc.Minutes() > 300.0 {
+		t.Errorf("accumulating AllocationSetRange: expected %f or less minutes; actual %f", 300.0, alloc.Minutes())
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Week(t *testing.T) {
+	ago9d := time.Now().UTC().Truncate(day).Add(-9 * day)
+	ago8d := time.Now().UTC().Truncate(day).Add(-8 * day)
+	ago7d := time.Now().UTC().Truncate(day).Add(-7 * day)
+	ago6d := time.Now().UTC().Truncate(day).Add(-6 * day)
+	ago5d := time.Now().UTC().Truncate(day).Add(-5 * day)
+	ago4d := time.Now().UTC().Truncate(day).Add(-4 * day)
+	ago3d := time.Now().UTC().Truncate(day).Add(-3 * day)
+	ago2d := time.Now().UTC().Truncate(day).Add(-2 * day)
+	yesterday := time.Now().UTC().Truncate(day).Add(-day)
+	today := time.Now().UTC().Truncate(day)
+
+	ago9dAS := GenerateMockAssetSet(ago9d, day)
+	ago8dAS := GenerateMockAssetSet(ago8d, day)
+	ago7dAS := GenerateMockAssetSet(ago7d, day)
+	ago6dAS := GenerateMockAssetSet(ago6d, day)
+	ago5dAS := GenerateMockAssetSet(ago5d, day)
+	ago4dAS := GenerateMockAssetSet(ago4d, day)
+	ago3dAS := GenerateMockAssetSet(ago3d, day)
+	ago2dAS := GenerateMockAssetSet(ago2d, day)
+	yesterdayAS := GenerateMockAssetSet(yesterday, day)
+	todayAS := GenerateMockAssetSet(today, day)
+
+	asr := NewAssetSetRange(ago9dAS, ago8dAS, ago7dAS, ago6dAS, ago5dAS, ago4dAS, ago3dAS, ago2dAS, yesterdayAS, todayAS)
+	asr, err := asr.Accumulate(AccumulateOptionWeek)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 2 && len(asr.Assets) != 3 {
+		t.Fatalf("expected 2 or 3 asset sets, got:%d", len(asr.Assets))
+	}
+
+	for _, as := range asr.Assets {
+		if as.Window.Duration() < time.Hour*24 || as.Window.Duration() > time.Hour*24*7 {
+			t.Fatalf("expected window duration to be between 1 and 7 days, got:%s", as.Window.Duration().String())
+		}
+	}
+}
+
+func TestAssetSetRange_AccumulateBy_Month(t *testing.T) {
+	prevMonth1stDay := time.Date(2020, 01, 29, 0, 0, 0, 0, time.UTC)
+	prevMonth2ndDay := time.Date(2020, 01, 30, 0, 0, 0, 0, time.UTC)
+	prevMonth3ndDay := time.Date(2020, 01, 31, 0, 0, 0, 0, time.UTC)
+	nextMonth1stDay := time.Date(2020, 02, 01, 0, 0, 0, 0, time.UTC)
+
+	prev1AS := GenerateMockAssetSet(prevMonth1stDay, day)
+	prev2AS := GenerateMockAssetSet(prevMonth2ndDay, day)
+	prev3AS := GenerateMockAssetSet(prevMonth3ndDay, day)
+	nextAS := GenerateMockAssetSet(nextMonth1stDay, day)
+
+	asr := NewAssetSetRange(prev1AS, prev2AS, prev3AS, nextAS)
+	asr, err := asr.Accumulate(AccumulateOptionMonth)
+	if err != nil {
+		t.Fatalf("unexpected error calling accumulateBy: %s", err)
+	}
+
+	if len(asr.Assets) != 2 {
+		t.Fatalf("expected 2 assets sets, got:%d", len(asr.Assets))
+	}
+
+	for _, as := range asr.Assets {
+		if as.Window.Duration() < time.Hour*24 || as.Window.Duration() > time.Hour*24*31 {
+			t.Fatalf("expected window duration to be between 1 and 31 days, got:%s", as.Window.Duration().String())
+		}
+	}
+}

+ 3 - 3
pkg/kubecost/kubecost_codecs_test.go

@@ -212,9 +212,9 @@ func TestAssetSetRange_BinaryEncoding(t *testing.T) {
 	var err error
 
 	asr0 = NewAssetSetRange(
-		GenerateMockAssetSet(startD0),
-		GenerateMockAssetSet(startD1),
-		GenerateMockAssetSet(startD2),
+		GenerateMockAssetSet(startD0, day),
+		GenerateMockAssetSet(startD1, day),
+		GenerateMockAssetSet(startD2, day),
 	)
 
 	bs, err = asr0.MarshalBinary()

+ 2 - 2
pkg/kubecost/mock.go

@@ -608,8 +608,8 @@ func GenerateMockAssetSets(start, end time.Time) []*AssetSet {
 //	total                          57.00   3.00
 //
 // +------------------------------+------+------+
-func GenerateMockAssetSet(start time.Time) *AssetSet {
-	end := start.Add(day)
+func GenerateMockAssetSet(start time.Time, duration time.Duration) *AssetSet {
+	end := start.Add(duration)
 	window := NewWindow(&start, &end)
 
 	hours := window.Duration().Hours()

+ 1 - 1
pkg/metrics/metricsconfig.go

@@ -13,7 +13,7 @@ import (
 
 var (
 	metricsConfigLock = new(sync.Mutex)
-	metricsFilePath   = path.Join(env.GetConfigPathWithDefault("/var/configs/"), "metrics.json")
+	metricsFilePath   = path.Join(env.GetCostAnalyzerVolumeMountPath(), "metrics.json")
 )
 
 type MetricsConfig struct {

+ 1 - 1
pkg/services/clusterservice.go

@@ -15,7 +15,7 @@ func NewClusterManagerService() HTTPService {
 
 // newClusterManager creates a new cluster manager instance for use in the service
 func newClusterManager() *clusters.ClusterManager {
-	clustersConfigFile := path.Join(env.GetConfigPathWithDefault("/var/configs/"), "clusters/default-clusters.yaml")
+	clustersConfigFile := path.Join(env.GetCostAnalyzerVolumeMountPath(), "clusters/default-clusters.yaml")
 
 	// Return a memory-backed cluster manager populated by configmap
 	return clusters.NewConfiguredClusterManager(clusters.NewMapDBClusterStorage(), clustersConfigFile)