|
|
@@ -0,0 +1,2810 @@
|
|
|
+package kubecost
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "encoding"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "math"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/kubecost/cost-model/pkg/log"
|
|
|
+)
|
|
|
+
|
|
|
+const timeFmt = "2006-01-02T15:04:05-0700"
|
|
|
+
|
|
|
+// Asset defines an entity within a cluster that has a defined cost over a
|
|
|
+// given period of time.
|
|
|
+type Asset interface {
|
|
|
+ // Type identifies the kind of Asset, which must always exist and should
|
|
|
+ // be defined by the underlying type implementing the interface.
|
|
|
+ Type() AssetType
|
|
|
+
|
|
|
+ // Properties are a map of predefined traits, which may or may not exist,
|
|
|
+ // but must conform to the AssetProperty schema
|
|
|
+ Properties() *AssetProperties
|
|
|
+ SetProperties(*AssetProperties)
|
|
|
+
|
|
|
+ // Labels are a map of undefined string-to-string values
|
|
|
+ Labels() AssetLabels
|
|
|
+ SetLabels(AssetLabels)
|
|
|
+
|
|
|
+ // Monetary values
|
|
|
+ Adjustment() float64
|
|
|
+ SetAdjustment(float64)
|
|
|
+ TotalCost() float64
|
|
|
+
|
|
|
+ // Temporal values
|
|
|
+ Start() time.Time
|
|
|
+ End() time.Time
|
|
|
+ Minutes() float64
|
|
|
+ Window() Window
|
|
|
+ ExpandWindow(Window)
|
|
|
+ SetStartEnd(time.Time, time.Time)
|
|
|
+
|
|
|
+ // Operations and comparisons
|
|
|
+ Add(Asset) Asset
|
|
|
+ Clone() Asset
|
|
|
+ Equal(Asset) bool
|
|
|
+
|
|
|
+ // Representations
|
|
|
+ encoding.BinaryMarshaler
|
|
|
+ encoding.BinaryUnmarshaler
|
|
|
+ json.Marshaler
|
|
|
+ fmt.Stringer
|
|
|
+}
|
|
|
+
|
|
|
+// key is used to determine uniqueness of an Asset, for instance during Insert
|
|
|
+// to determine if two Assets should be combined. Passing nil props indicates
|
|
|
+// that all available props should be used. Passing empty props indicates that
|
|
|
+// no props should be used (e.g. to aggregate all assets). Passing one or more
|
|
|
+// props will key by only those props.
|
|
|
+func key(a Asset, props []AssetProperty) string {
|
|
|
+ keys := []string{}
|
|
|
+
|
|
|
+ if props == nil {
|
|
|
+ props = []AssetProperty{
|
|
|
+ AssetProviderProp,
|
|
|
+ AssetAccountProp,
|
|
|
+ AssetProjectProp,
|
|
|
+ AssetCategoryProp,
|
|
|
+ AssetClusterProp,
|
|
|
+ AssetTypeProp,
|
|
|
+ AssetServiceProp,
|
|
|
+ AssetProviderIDProp,
|
|
|
+ AssetNameProp,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, prop := range props {
|
|
|
+ switch true {
|
|
|
+ case prop == AssetProviderProp && a.Properties().Provider != "":
|
|
|
+ keys = append(keys, a.Properties().Provider)
|
|
|
+ case prop == AssetAccountProp && a.Properties().Account != "":
|
|
|
+ keys = append(keys, a.Properties().Account)
|
|
|
+ case prop == AssetProjectProp && a.Properties().Project != "":
|
|
|
+ keys = append(keys, a.Properties().Project)
|
|
|
+ case prop == AssetClusterProp && a.Properties().Cluster != "":
|
|
|
+ keys = append(keys, a.Properties().Cluster)
|
|
|
+ case prop == AssetCategoryProp && a.Properties().Category != "":
|
|
|
+ keys = append(keys, a.Properties().Category)
|
|
|
+ case prop == AssetTypeProp && a.Type().String() != "":
|
|
|
+ keys = append(keys, a.Type().String())
|
|
|
+ case prop == AssetServiceProp && a.Properties().Service != "":
|
|
|
+ keys = append(keys, a.Properties().Service)
|
|
|
+ case prop == AssetProviderIDProp && a.Properties().ProviderID != "":
|
|
|
+ keys = append(keys, a.Properties().ProviderID)
|
|
|
+ case prop == AssetNameProp && a.Properties().Name != "":
|
|
|
+ keys = append(keys, a.Properties().Name)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return strings.Join(keys, "/")
|
|
|
+}
|
|
|
+
|
|
|
+func toString(a Asset) string {
|
|
|
+ return fmt.Sprintf("%s{%s}%s=%.2f", a.Type().String(), a.Properties(), a.Window(), a.TotalCost())
|
|
|
+}
|
|
|
+
|
|
|
+// AssetLabels is a schema-free mapping of key/value pairs that can be
|
|
|
+// attributed to an Asset as a flexible a
|
|
|
+type AssetLabels map[string]string
|
|
|
+
|
|
|
+// Clone returns a cloned map of labels
|
|
|
+func (al AssetLabels) Clone() AssetLabels {
|
|
|
+ clone := AssetLabels{}
|
|
|
+
|
|
|
+ for label, value := range al {
|
|
|
+ clone[label] = value
|
|
|
+ }
|
|
|
+
|
|
|
+ return clone
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true only if the two set of labels are exact matches
|
|
|
+func (al AssetLabels) Equal(that AssetLabels) bool {
|
|
|
+ if len(al) != len(that) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ for label, value := range al {
|
|
|
+ if thatValue, ok := that[label]; !ok || thatValue != value {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// Merge retains only the labels shared with the given AssetLabels
|
|
|
+func (al AssetLabels) Merge(that AssetLabels) AssetLabels {
|
|
|
+ result := AssetLabels{}
|
|
|
+
|
|
|
+ for label, value := range al {
|
|
|
+ if thatValue, ok := that[label]; ok && thatValue == value {
|
|
|
+ result[label] = value
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+// AssetMatchFunc is a function that can be used to match Assets by
|
|
|
+// returning true for any given Asset if a condition is met.
|
|
|
+type AssetMatchFunc func(Asset) bool
|
|
|
+
|
|
|
+// AssetType identifies a type of Asset
|
|
|
+type AssetType int
|
|
|
+
|
|
|
+const (
|
|
|
+ // AnyAssetType describes the Any AssetType
|
|
|
+ AnyAssetType AssetType = iota
|
|
|
+
|
|
|
+ // CloudAssetType describes the Cloud AssetType
|
|
|
+ CloudAssetType
|
|
|
+
|
|
|
+ // ClusterManagementAssetType describes the ClusterManagement AssetType
|
|
|
+ ClusterManagementAssetType
|
|
|
+
|
|
|
+ // DiskAssetType describes the Disk AssetType
|
|
|
+ DiskAssetType
|
|
|
+
|
|
|
+ // LoadBalancerAssetType describes the LoadBalancer AssetType
|
|
|
+ LoadBalancerAssetType
|
|
|
+
|
|
|
+ // NetworkAssetType describes the Network AssetType
|
|
|
+ NetworkAssetType
|
|
|
+
|
|
|
+ // NodeAssetType describes the Node AssetType
|
|
|
+ NodeAssetType
|
|
|
+
|
|
|
+ // SharedAssetType describes the Shared AssetType
|
|
|
+ SharedAssetType
|
|
|
+)
|
|
|
+
|
|
|
+// ParseAssetType attempts to parse the given string into an AssetType
|
|
|
+func ParseAssetType(text string) (AssetType, error) {
|
|
|
+ switch strings.TrimSpace(strings.ToLower(text)) {
|
|
|
+ case "cloud":
|
|
|
+ return CloudAssetType, nil
|
|
|
+ case "clustermanagement":
|
|
|
+ return ClusterManagementAssetType, nil
|
|
|
+ case "disk":
|
|
|
+ return DiskAssetType, nil
|
|
|
+ case "loadbalancer":
|
|
|
+ return LoadBalancerAssetType, nil
|
|
|
+ case "network":
|
|
|
+ return NetworkAssetType, nil
|
|
|
+ case "node":
|
|
|
+ return NodeAssetType, nil
|
|
|
+ case "shared":
|
|
|
+ return SharedAssetType, nil
|
|
|
+ }
|
|
|
+ return AnyAssetType, fmt.Errorf("invalid asset type: %s", text)
|
|
|
+}
|
|
|
+
|
|
|
+// String converts the given AssetType to a string
|
|
|
+func (at AssetType) String() string {
|
|
|
+ return [...]string{
|
|
|
+ "Asset",
|
|
|
+ "Cloud",
|
|
|
+ "ClusterManagement",
|
|
|
+ "Disk",
|
|
|
+ "LoadBalancer",
|
|
|
+ "Network",
|
|
|
+ "Node",
|
|
|
+ "Shared",
|
|
|
+ }[at]
|
|
|
+}
|
|
|
+
|
|
|
+// Any is the most general Asset, which is usually created as a result of
|
|
|
+// adding two Assets of different types.
|
|
|
+type Any struct {
|
|
|
+ labels AssetLabels
|
|
|
+ properties *AssetProperties
|
|
|
+ start time.Time
|
|
|
+ end time.Time
|
|
|
+ window Window
|
|
|
+ adjustment float64
|
|
|
+ Cost float64
|
|
|
+}
|
|
|
+
|
|
|
+// NewAsset creates a new Any-type Asset for the given period of time
|
|
|
+func NewAsset(start, end time.Time, window Window) *Any {
|
|
|
+ return &Any{
|
|
|
+ labels: AssetLabels{},
|
|
|
+ properties: &AssetProperties{},
|
|
|
+ start: start,
|
|
|
+ end: end,
|
|
|
+ window: window.Clone(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the Asset's type
|
|
|
+func (a *Any) Type() AssetType {
|
|
|
+ return AnyAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the Asset's properties
|
|
|
+func (a *Any) Properties() *AssetProperties {
|
|
|
+ return a.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (a *Any) SetProperties(props *AssetProperties) {
|
|
|
+ a.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the Asset's labels
|
|
|
+func (a *Any) Labels() AssetLabels {
|
|
|
+ return a.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's labels
|
|
|
+func (a *Any) SetLabels(labels AssetLabels) {
|
|
|
+ a.labels = labels
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment returns the Asset's cost adjustment
|
|
|
+func (a *Any) Adjustment() float64 {
|
|
|
+ return a.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment sets the Asset's cost adjustment
|
|
|
+func (a *Any) SetAdjustment(adj float64) {
|
|
|
+ a.adjustment = adj
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the Asset's TotalCost
|
|
|
+func (a *Any) TotalCost() float64 {
|
|
|
+ return a.Cost + a.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the Asset's start time within the window
|
|
|
+func (a *Any) Start() time.Time {
|
|
|
+ return a.start
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the Asset's end time within the window
|
|
|
+func (a *Any) End() time.Time {
|
|
|
+ return a.end
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of minutes the Asset was active within the window
|
|
|
+func (a *Any) Minutes() float64 {
|
|
|
+ return a.End().Sub(a.Start()).Minutes()
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the Asset's window
|
|
|
+func (a *Any) Window() Window {
|
|
|
+ return a.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window by the given window
|
|
|
+func (a *Any) ExpandWindow(window Window) {
|
|
|
+ a.window = a.window.Expand(window)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields
|
|
|
+func (a *Any) SetStartEnd(start, end time.Time) {
|
|
|
+ if a.Window().Contains(start) {
|
|
|
+ a.start = start
|
|
|
+ } else {
|
|
|
+ log.Warningf("Any.SetStartEnd: start %s not in %s", start, a.Window())
|
|
|
+ }
|
|
|
+
|
|
|
+ if a.Window().Contains(end) {
|
|
|
+ a.end = end
|
|
|
+ } else {
|
|
|
+ log.Warningf("Any.SetStartEnd: end %s not in %s", end, a.Window())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (a *Any) Add(that Asset) Asset {
|
|
|
+ this := a.Clone().(*Any)
|
|
|
+
|
|
|
+ props := a.Properties().Merge(that.Properties())
|
|
|
+ labels := a.Labels().Merge(that.Labels())
|
|
|
+
|
|
|
+ start := a.Start()
|
|
|
+ if that.Start().Before(start) {
|
|
|
+ start = that.Start()
|
|
|
+ }
|
|
|
+ end := a.End()
|
|
|
+ if that.End().After(end) {
|
|
|
+ end = that.End()
|
|
|
+ }
|
|
|
+ window := a.Window().Expand(that.Window())
|
|
|
+
|
|
|
+ this.start = start
|
|
|
+ this.end = end
|
|
|
+ this.window = window
|
|
|
+ this.SetProperties(props)
|
|
|
+ this.SetLabels(labels)
|
|
|
+ this.adjustment += that.Adjustment()
|
|
|
+ this.Cost += (that.TotalCost() - that.Adjustment())
|
|
|
+
|
|
|
+ return this
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a cloned instance of the Asset
|
|
|
+func (a *Any) Clone() Asset {
|
|
|
+ return &Any{
|
|
|
+ labels: a.labels.Clone(),
|
|
|
+ properties: a.properties.Clone(),
|
|
|
+ start: a.start,
|
|
|
+ end: a.end,
|
|
|
+ window: a.window.Clone(),
|
|
|
+ adjustment: a.adjustment,
|
|
|
+ Cost: a.Cost,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the given Asset is an exact match of the receiver
|
|
|
+func (a *Any) Equal(that Asset) bool {
|
|
|
+ t, ok := that.(*Any)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !a.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !a.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !a.start.Equal(t.start) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !a.end.Equal(t.end) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !a.window.Equal(t.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if a.Cost != t.Cost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements json.Marshaler
|
|
|
+func (a *Any) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncode(buffer, "properties", a.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", a.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", a.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", a.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", a.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", a.Minutes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "adjustment", a.Adjustment(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", a.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (a *Any) String() string {
|
|
|
+ return toString(a)
|
|
|
+}
|
|
|
+
|
|
|
+// Cloud describes a cloud asset
|
|
|
+type Cloud struct {
|
|
|
+ labels AssetLabels
|
|
|
+ properties *AssetProperties
|
|
|
+ start time.Time
|
|
|
+ end time.Time
|
|
|
+ window Window
|
|
|
+ adjustment float64
|
|
|
+ Cost float64
|
|
|
+}
|
|
|
+
|
|
|
+// NewCloud returns a new Cloud Asset
|
|
|
+func NewCloud(category, providerID string, start, end time.Time, window Window) *Cloud {
|
|
|
+ properties := &AssetProperties{
|
|
|
+ Category: category,
|
|
|
+ ProviderID: providerID,
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Cloud{
|
|
|
+ labels: AssetLabels{},
|
|
|
+ properties: properties,
|
|
|
+ start: start,
|
|
|
+ end: end,
|
|
|
+ window: window.Clone(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the AssetType
|
|
|
+func (ca *Cloud) Type() AssetType {
|
|
|
+ return CloudAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the AssetProperties
|
|
|
+func (ca *Cloud) Properties() *AssetProperties {
|
|
|
+ return ca.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (ca *Cloud) SetProperties(props *AssetProperties) {
|
|
|
+ ca.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the AssetLabels
|
|
|
+func (ca *Cloud) Labels() AssetLabels {
|
|
|
+ return ca.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's labels
|
|
|
+func (ca *Cloud) SetLabels(labels AssetLabels) {
|
|
|
+ ca.labels = labels
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment returns the Asset's adjustment value
|
|
|
+func (ca *Cloud) Adjustment() float64 {
|
|
|
+ return ca.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment sets the Asset's adjustment value
|
|
|
+func (ca *Cloud) SetAdjustment(adj float64) {
|
|
|
+ ca.adjustment = adj
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the Asset's total cost
|
|
|
+func (ca *Cloud) TotalCost() float64 {
|
|
|
+ return ca.Cost + ca.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the Asset's precise start time within the window
|
|
|
+func (ca *Cloud) Start() time.Time {
|
|
|
+ return ca.start
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the Asset's precise end time within the window
|
|
|
+func (ca *Cloud) End() time.Time {
|
|
|
+ return ca.end
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of Minutes the Asset ran
|
|
|
+func (ca *Cloud) Minutes() float64 {
|
|
|
+ return ca.End().Sub(ca.Start()).Minutes()
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the window within which the Asset ran
|
|
|
+func (ca *Cloud) Window() Window {
|
|
|
+ return ca.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window by the given window
|
|
|
+func (ca *Cloud) ExpandWindow(window Window) {
|
|
|
+ ca.window = ca.window.Expand(window)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields
|
|
|
+func (ca *Cloud) SetStartEnd(start, end time.Time) {
|
|
|
+ if ca.Window().Contains(start) {
|
|
|
+ ca.start = start
|
|
|
+ } else {
|
|
|
+ log.Warningf("Cloud.SetStartEnd: start %s not in %s", start, ca.Window())
|
|
|
+ }
|
|
|
+
|
|
|
+ if ca.Window().Contains(end) {
|
|
|
+ ca.end = end
|
|
|
+ } else {
|
|
|
+ log.Warningf("Cloud.SetStartEnd: end %s not in %s", end, ca.Window())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (ca *Cloud) Add(a Asset) Asset {
|
|
|
+ // Cloud + Cloud = Cloud
|
|
|
+ if that, ok := a.(*Cloud); ok {
|
|
|
+ this := ca.Clone().(*Cloud)
|
|
|
+ this.add(that)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ props := ca.Properties().Merge(a.Properties())
|
|
|
+ labels := ca.Labels().Merge(a.Labels())
|
|
|
+
|
|
|
+ start := ca.Start()
|
|
|
+ if a.Start().Before(start) {
|
|
|
+ start = a.Start()
|
|
|
+ }
|
|
|
+ end := ca.End()
|
|
|
+ if a.End().After(end) {
|
|
|
+ end = a.End()
|
|
|
+ }
|
|
|
+ window := ca.Window().Expand(a.Window())
|
|
|
+
|
|
|
+ // Cloud + !Cloud = Any
|
|
|
+ any := NewAsset(start, end, window)
|
|
|
+ any.SetProperties(props)
|
|
|
+ any.SetLabels(labels)
|
|
|
+ any.adjustment = ca.Adjustment() + a.Adjustment()
|
|
|
+ any.Cost = (ca.TotalCost() - ca.Adjustment()) + (a.TotalCost() - a.Adjustment())
|
|
|
+
|
|
|
+ return any
|
|
|
+}
|
|
|
+
|
|
|
+func (ca *Cloud) add(that *Cloud) {
|
|
|
+ if ca == nil {
|
|
|
+ ca = that
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ props := ca.Properties().Merge(that.Properties())
|
|
|
+ labels := ca.Labels().Merge(that.Labels())
|
|
|
+
|
|
|
+ start := ca.Start()
|
|
|
+ if that.Start().Before(start) {
|
|
|
+ start = that.Start()
|
|
|
+ }
|
|
|
+ end := ca.End()
|
|
|
+ if that.End().After(end) {
|
|
|
+ end = that.End()
|
|
|
+ }
|
|
|
+ window := ca.Window().Expand(that.Window())
|
|
|
+
|
|
|
+ ca.start = start
|
|
|
+ ca.end = end
|
|
|
+ ca.window = window
|
|
|
+ ca.SetProperties(props)
|
|
|
+ ca.SetLabels(labels)
|
|
|
+ ca.adjustment += that.adjustment
|
|
|
+ ca.Cost += that.Cost
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a cloned instance of the Asset
|
|
|
+func (ca *Cloud) Clone() Asset {
|
|
|
+ return &Cloud{
|
|
|
+ labels: ca.labels.Clone(),
|
|
|
+ properties: ca.properties.Clone(),
|
|
|
+ start: ca.start,
|
|
|
+ end: ca.end,
|
|
|
+ window: ca.window.Clone(),
|
|
|
+ adjustment: ca.adjustment,
|
|
|
+ Cost: ca.Cost,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the given Asset precisely equals the Asset
|
|
|
+func (ca *Cloud) Equal(a Asset) bool {
|
|
|
+ that, ok := a.(*Cloud)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !ca.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !ca.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !ca.start.Equal(that.start) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !ca.end.Equal(that.end) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !ca.window.Equal(that.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if ca.adjustment != that.adjustment {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if ca.Cost != that.Cost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements json.Marshaler
|
|
|
+func (ca *Cloud) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncodeString(buffer, "type", ca.Type().String(), ",")
|
|
|
+ jsonEncode(buffer, "properties", ca.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", ca.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", ca.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", ca.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", ca.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", ca.Minutes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "adjustment", ca.Adjustment(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", ca.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (ca *Cloud) String() string {
|
|
|
+ return toString(ca)
|
|
|
+}
|
|
|
+
|
|
|
+// ClusterManagement describes a provider's cluster management fee
|
|
|
+type ClusterManagement struct {
|
|
|
+ labels AssetLabels
|
|
|
+ properties *AssetProperties
|
|
|
+ window Window
|
|
|
+ Cost float64
|
|
|
+}
|
|
|
+
|
|
|
+// NewClusterManagement creates and returns a new ClusterManagement instance
|
|
|
+func NewClusterManagement(provider, cluster string, window Window) *ClusterManagement {
|
|
|
+ properties := &AssetProperties{
|
|
|
+ Category: ManagementCategory,
|
|
|
+ Provider: ParseProvider(provider),
|
|
|
+ Cluster: cluster,
|
|
|
+ Service: KubernetesService,
|
|
|
+ }
|
|
|
+
|
|
|
+ return &ClusterManagement{
|
|
|
+ labels: AssetLabels{},
|
|
|
+ properties: properties,
|
|
|
+ window: window.Clone(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the Asset's type
|
|
|
+func (cm *ClusterManagement) Type() AssetType {
|
|
|
+ return ClusterManagementAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the Asset's properties
|
|
|
+func (cm *ClusterManagement) Properties() *AssetProperties {
|
|
|
+ return cm.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (cm *ClusterManagement) SetProperties(props *AssetProperties) {
|
|
|
+ cm.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the Asset's labels
|
|
|
+func (cm *ClusterManagement) Labels() AssetLabels {
|
|
|
+ return cm.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's properties
|
|
|
+func (cm *ClusterManagement) SetLabels(props AssetLabels) {
|
|
|
+ cm.labels = props
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment does not apply to ClusterManagement
|
|
|
+func (cm *ClusterManagement) Adjustment() float64 {
|
|
|
+ return 0.0
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment does not apply to ClusterManagement
|
|
|
+func (cm *ClusterManagement) SetAdjustment(float64) {
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the Asset's total cost
|
|
|
+func (cm *ClusterManagement) TotalCost() float64 {
|
|
|
+ return cm.Cost
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the Asset's precise start time within the window
|
|
|
+func (cm *ClusterManagement) Start() time.Time {
|
|
|
+ return *cm.window.Start()
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the Asset's precise end time within the window
|
|
|
+func (cm *ClusterManagement) End() time.Time {
|
|
|
+ return *cm.window.End()
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of minutes the Asset ran
|
|
|
+func (cm *ClusterManagement) Minutes() float64 {
|
|
|
+ return cm.Window().Minutes()
|
|
|
+}
|
|
|
+
|
|
|
+// Window return the Asset's window
|
|
|
+func (cm *ClusterManagement) Window() Window {
|
|
|
+ return cm.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window by the given window
|
|
|
+func (cm *ClusterManagement) ExpandWindow(window Window) {
|
|
|
+ cm.window = cm.window.Expand(window)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields (not applicable here)
|
|
|
+func (cm *ClusterManagement) SetStartEnd(start, end time.Time) {
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (cm *ClusterManagement) Add(a Asset) Asset {
|
|
|
+ // ClusterManagement + ClusterManagement = ClusterManagement
|
|
|
+ if that, ok := a.(*ClusterManagement); ok {
|
|
|
+ this := cm.Clone().(*ClusterManagement)
|
|
|
+ this.add(that)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ props := cm.Properties().Merge(a.Properties())
|
|
|
+ labels := cm.Labels().Merge(a.Labels())
|
|
|
+
|
|
|
+ start := cm.Start()
|
|
|
+ if a.Start().Before(start) {
|
|
|
+ start = a.Start()
|
|
|
+ }
|
|
|
+ end := cm.End()
|
|
|
+ if a.End().After(end) {
|
|
|
+ end = a.End()
|
|
|
+ }
|
|
|
+ window := cm.Window().Expand(a.Window())
|
|
|
+
|
|
|
+ // ClusterManagement + !ClusterManagement = Any
|
|
|
+ any := NewAsset(start, end, window)
|
|
|
+ any.SetProperties(props)
|
|
|
+ any.SetLabels(labels)
|
|
|
+ any.adjustment = cm.Adjustment() + a.Adjustment()
|
|
|
+ any.Cost = (cm.TotalCost() - cm.Adjustment()) + (a.TotalCost() - a.Adjustment())
|
|
|
+
|
|
|
+ return any
|
|
|
+}
|
|
|
+
|
|
|
+func (cm *ClusterManagement) add(that *ClusterManagement) {
|
|
|
+ if cm == nil {
|
|
|
+ cm = that
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ props := cm.Properties().Merge(that.Properties())
|
|
|
+ labels := cm.Labels().Merge(that.Labels())
|
|
|
+ window := cm.Window().Expand(that.Window())
|
|
|
+
|
|
|
+ cm.window = window
|
|
|
+ cm.SetProperties(props)
|
|
|
+ cm.SetLabels(labels)
|
|
|
+ cm.Cost += that.Cost
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a cloned instance of the Asset
|
|
|
+func (cm *ClusterManagement) Clone() Asset {
|
|
|
+ return &ClusterManagement{
|
|
|
+ labels: cm.labels.Clone(),
|
|
|
+ properties: cm.properties.Clone(),
|
|
|
+ window: cm.window.Clone(),
|
|
|
+ Cost: cm.Cost,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the given Asset exactly matches the Asset
|
|
|
+func (cm *ClusterManagement) Equal(a Asset) bool {
|
|
|
+ that, ok := a.(*ClusterManagement)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !cm.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !cm.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !cm.window.Equal(that.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if cm.Cost != that.Cost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements json.Marshler
|
|
|
+func (cm *ClusterManagement) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncodeString(buffer, "type", cm.Type().String(), ",")
|
|
|
+ jsonEncode(buffer, "properties", cm.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", cm.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", cm.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", cm.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", cm.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", cm.Minutes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", cm.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (cm *ClusterManagement) String() string {
|
|
|
+ return toString(cm)
|
|
|
+}
|
|
|
+
|
|
|
+// Disk represents an in-cluster disk Asset
|
|
|
+type Disk struct {
|
|
|
+ labels AssetLabels
|
|
|
+ properties *AssetProperties
|
|
|
+ start time.Time
|
|
|
+ end time.Time
|
|
|
+ window Window
|
|
|
+ adjustment float64
|
|
|
+ Cost float64
|
|
|
+ ByteHours float64
|
|
|
+ Local float64
|
|
|
+ Breakdown *Breakdown
|
|
|
+}
|
|
|
+
|
|
|
+// NewDisk creates and returns a new Disk Asset
|
|
|
+func NewDisk(name, cluster, providerID string, start, end time.Time, window Window) *Disk {
|
|
|
+ properties := &AssetProperties{
|
|
|
+ Category: StorageCategory,
|
|
|
+ Name: name,
|
|
|
+ Cluster: cluster,
|
|
|
+ ProviderID: providerID,
|
|
|
+ Service: KubernetesService,
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Disk{
|
|
|
+ labels: AssetLabels{},
|
|
|
+ properties: properties,
|
|
|
+ start: start,
|
|
|
+ end: end,
|
|
|
+ window: window,
|
|
|
+ Breakdown: &Breakdown{},
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the AssetType of the Asset
|
|
|
+func (d *Disk) Type() AssetType {
|
|
|
+ return DiskAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the Asset's properties
|
|
|
+func (d *Disk) Properties() *AssetProperties {
|
|
|
+ return d.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (d *Disk) SetProperties(props *AssetProperties) {
|
|
|
+ d.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the Asset's labels
|
|
|
+func (d *Disk) Labels() AssetLabels {
|
|
|
+ return d.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's labels
|
|
|
+func (d *Disk) SetLabels(labels AssetLabels) {
|
|
|
+ d.labels = labels
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment returns the Asset's cost adjustment
|
|
|
+func (d *Disk) Adjustment() float64 {
|
|
|
+ return d.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment sets the Asset's cost adjustment
|
|
|
+func (d *Disk) SetAdjustment(adj float64) {
|
|
|
+ d.adjustment = adj
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the Asset's total cost
|
|
|
+func (d *Disk) TotalCost() float64 {
|
|
|
+ return d.Cost + d.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the precise start time of the Asset within the window
|
|
|
+func (d *Disk) Start() time.Time {
|
|
|
+ return d.start
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the precise start time of the Asset within the window
|
|
|
+func (d *Disk) End() time.Time {
|
|
|
+ return d.end
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of minutes the Asset ran
|
|
|
+func (d *Disk) Minutes() float64 {
|
|
|
+ diskMins := d.end.Sub(d.start).Minutes()
|
|
|
+ windowMins := d.window.Minutes()
|
|
|
+
|
|
|
+ if diskMins > windowMins {
|
|
|
+ log.Warningf("Asset ETL: Disk.Minutes exceeds window: %.2f > %.2f", diskMins, windowMins)
|
|
|
+ diskMins = windowMins
|
|
|
+ }
|
|
|
+
|
|
|
+ if diskMins < 0 {
|
|
|
+ diskMins = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ return diskMins
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the window within which the Asset
|
|
|
+func (d *Disk) Window() Window {
|
|
|
+ return d.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window by the given window
|
|
|
+func (d *Disk) ExpandWindow(window Window) {
|
|
|
+ d.window = d.window.Expand(window)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields
|
|
|
+func (d *Disk) SetStartEnd(start, end time.Time) {
|
|
|
+ if d.Window().Contains(start) {
|
|
|
+ d.start = start
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: start %s not in %s", start, d.Window())
|
|
|
+ }
|
|
|
+
|
|
|
+ if d.Window().Contains(end) {
|
|
|
+ d.end = end
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: end %s not in %s", end, d.Window())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (d *Disk) Add(a Asset) Asset {
|
|
|
+ // Disk + Disk = Disk
|
|
|
+ if that, ok := a.(*Disk); ok {
|
|
|
+ this := d.Clone().(*Disk)
|
|
|
+ this.add(that)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ props := d.Properties().Merge(a.Properties())
|
|
|
+ labels := d.Labels().Merge(a.Labels())
|
|
|
+
|
|
|
+ start := d.Start()
|
|
|
+ if a.Start().Before(start) {
|
|
|
+ start = a.Start()
|
|
|
+ }
|
|
|
+ end := d.End()
|
|
|
+ if a.End().After(end) {
|
|
|
+ end = a.End()
|
|
|
+ }
|
|
|
+ window := d.Window().Expand(a.Window())
|
|
|
+
|
|
|
+ // Disk + !Disk = Any
|
|
|
+ any := NewAsset(start, end, window)
|
|
|
+ any.SetProperties(props)
|
|
|
+ any.SetLabels(labels)
|
|
|
+ any.adjustment = d.Adjustment() + a.Adjustment()
|
|
|
+ any.Cost = (d.TotalCost() - d.Adjustment()) + (a.TotalCost() - a.Adjustment())
|
|
|
+
|
|
|
+ return any
|
|
|
+}
|
|
|
+
|
|
|
+func (d *Disk) add(that *Disk) {
|
|
|
+ if d == nil {
|
|
|
+ d = that
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ props := d.Properties().Merge(that.Properties())
|
|
|
+ labels := d.Labels().Merge(that.Labels())
|
|
|
+ d.SetProperties(props)
|
|
|
+ d.SetLabels(labels)
|
|
|
+
|
|
|
+ start := d.Start()
|
|
|
+ if that.Start().Before(start) {
|
|
|
+ start = that.Start()
|
|
|
+ }
|
|
|
+ end := d.End()
|
|
|
+ if that.End().After(end) {
|
|
|
+ end = that.End()
|
|
|
+ }
|
|
|
+ window := d.Window().Expand(that.Window())
|
|
|
+ d.start = start
|
|
|
+ d.end = end
|
|
|
+ d.window = window
|
|
|
+
|
|
|
+ totalCost := d.Cost + that.Cost
|
|
|
+ if totalCost > 0.0 {
|
|
|
+ d.Breakdown.Idle = (d.Breakdown.Idle*d.Cost + that.Breakdown.Idle*that.Cost) / totalCost
|
|
|
+ d.Breakdown.Other = (d.Breakdown.Other*d.Cost + that.Breakdown.Other*that.Cost) / totalCost
|
|
|
+ d.Breakdown.System = (d.Breakdown.System*d.Cost + that.Breakdown.System*that.Cost) / totalCost
|
|
|
+ d.Breakdown.User = (d.Breakdown.User*d.Cost + that.Breakdown.User*that.Cost) / totalCost
|
|
|
+
|
|
|
+ d.Local = (d.TotalCost()*d.Local + that.TotalCost()*that.Local) / (d.TotalCost() + that.TotalCost())
|
|
|
+ } else {
|
|
|
+ d.Local = (d.Local + that.Local) / 2.0
|
|
|
+ }
|
|
|
+
|
|
|
+ d.adjustment += that.adjustment
|
|
|
+ d.Cost += that.Cost
|
|
|
+
|
|
|
+ d.ByteHours += that.ByteHours
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a cloned instance of the Asset
|
|
|
+func (d *Disk) Clone() Asset {
|
|
|
+ return &Disk{
|
|
|
+ properties: d.properties.Clone(),
|
|
|
+ labels: d.labels.Clone(),
|
|
|
+ start: d.start,
|
|
|
+ end: d.end,
|
|
|
+ window: d.window.Clone(),
|
|
|
+ adjustment: d.adjustment,
|
|
|
+ Cost: d.Cost,
|
|
|
+ ByteHours: d.ByteHours,
|
|
|
+ Local: d.Local,
|
|
|
+ Breakdown: d.Breakdown.Clone(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the two Assets match exactly
|
|
|
+func (d *Disk) Equal(a Asset) bool {
|
|
|
+ that, ok := a.(*Disk)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !d.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !d.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !d.Start().Equal(that.Start()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !d.End().Equal(that.End()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !d.window.Equal(that.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if d.adjustment != that.adjustment {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if d.Cost != that.Cost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if d.ByteHours != that.ByteHours {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if d.Local != that.Local {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !d.Breakdown.Equal(that.Breakdown) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements the json.Marshaler interface
|
|
|
+func (d *Disk) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncodeString(buffer, "type", d.Type().String(), ",")
|
|
|
+ jsonEncode(buffer, "properties", d.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", d.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", d.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", d.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", d.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", d.Minutes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "byteHours", d.ByteHours, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "bytes", d.Bytes(), ",")
|
|
|
+ jsonEncode(buffer, "breakdown", d.Breakdown, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "adjustment", d.Adjustment(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", d.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (d *Disk) String() string {
|
|
|
+ return toString(d)
|
|
|
+}
|
|
|
+
|
|
|
+// Bytes returns the number of bytes belonging to the disk. This could be
|
|
|
+// fractional because it's the number of byte*hours divided by the number of
|
|
|
+// hours running; e.g. the sum of a 100GiB disk running for the first 10 hours
|
|
|
+// and a 30GiB disk running for the last 20 hours of the same 24-hour window
|
|
|
+// would produce:
|
|
|
+// (100*10 + 30*20) / 24 = 66.667GiB
|
|
|
+// However, any number of disks running for the full span of a window will
|
|
|
+// report the actual number of bytes of the static disk; e.g. the above
|
|
|
+// scenario for one entire 24-hour window:
|
|
|
+// (100*24 + 30*24) / 24 = (100 + 30) = 130GiB
|
|
|
+func (d *Disk) Bytes() float64 {
|
|
|
+ // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
|
|
|
+ return d.ByteHours * (60.0 / d.Minutes())
|
|
|
+}
|
|
|
+
|
|
|
+// Breakdown describes a resource's use as a percentage of various usage types
|
|
|
+type Breakdown struct {
|
|
|
+ Idle float64 `json:"idle"`
|
|
|
+ Other float64 `json:"other"`
|
|
|
+ System float64 `json:"system"`
|
|
|
+ User float64 `json:"user"`
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a cloned instance of the Breakdown
|
|
|
+func (b *Breakdown) Clone() *Breakdown {
|
|
|
+ if b == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Breakdown{
|
|
|
+ Idle: b.Idle,
|
|
|
+ Other: b.Other,
|
|
|
+ System: b.System,
|
|
|
+ User: b.User,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the two Breakdowns are exact matches
|
|
|
+func (b *Breakdown) Equal(that *Breakdown) bool {
|
|
|
+ if b == nil || that == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if b.Idle != that.Idle {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if b.Other != that.Other {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if b.System != that.System {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if b.User != that.User {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// Network is an Asset representing a single node's network costs
|
|
|
+type Network struct {
|
|
|
+ properties *AssetProperties
|
|
|
+ labels AssetLabels
|
|
|
+ start time.Time
|
|
|
+ end time.Time
|
|
|
+ window Window
|
|
|
+ adjustment float64
|
|
|
+ Cost float64
|
|
|
+}
|
|
|
+
|
|
|
+// NewNetwork creates and returns a new Network Asset
|
|
|
+func NewNetwork(name, cluster, providerID string, start, end time.Time, window Window) *Network {
|
|
|
+ properties := &AssetProperties{
|
|
|
+ Category: NetworkCategory,
|
|
|
+ Name: name,
|
|
|
+ Cluster: cluster,
|
|
|
+ ProviderID: providerID,
|
|
|
+ Service: KubernetesService,
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Network{
|
|
|
+ properties: properties,
|
|
|
+ labels: AssetLabels{},
|
|
|
+ start: start,
|
|
|
+ end: end,
|
|
|
+ window: window.Clone(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the AssetType of the Asset
|
|
|
+func (n *Network) Type() AssetType {
|
|
|
+ return NetworkAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the Asset's properties
|
|
|
+func (n *Network) Properties() *AssetProperties {
|
|
|
+ return n.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (n *Network) SetProperties(props *AssetProperties) {
|
|
|
+ n.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the Asset's labels
|
|
|
+func (n *Network) Labels() AssetLabels {
|
|
|
+ return n.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's labels
|
|
|
+func (n *Network) SetLabels(labels AssetLabels) {
|
|
|
+ n.labels = labels
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment returns the Asset's cost adjustment
|
|
|
+func (n *Network) Adjustment() float64 {
|
|
|
+ return n.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment sets the Asset's cost adjustment
|
|
|
+func (n *Network) SetAdjustment(adj float64) {
|
|
|
+ n.adjustment = adj
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the Asset's total cost
|
|
|
+func (n *Network) TotalCost() float64 {
|
|
|
+ return n.Cost + n.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the precise start time of the Asset within the window
|
|
|
+func (n *Network) Start() time.Time {
|
|
|
+ return n.start
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the precise end time of the Asset within the window
|
|
|
+func (n *Network) End() time.Time {
|
|
|
+ return n.end
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of minutes the Asset ran within the window
|
|
|
+func (n *Network) Minutes() float64 {
|
|
|
+ netMins := n.end.Sub(n.start).Minutes()
|
|
|
+ windowMins := n.window.Minutes()
|
|
|
+
|
|
|
+ if netMins > windowMins {
|
|
|
+ log.Warningf("Asset ETL: Network.Minutes exceeds window: %.2f > %.2f", netMins, windowMins)
|
|
|
+ netMins = windowMins
|
|
|
+ }
|
|
|
+
|
|
|
+ if netMins < 0 {
|
|
|
+ netMins = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ return netMins
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the window within which the Asset ran
|
|
|
+func (n *Network) Window() Window {
|
|
|
+ return n.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window by the given window
|
|
|
+func (n *Network) ExpandWindow(window Window) {
|
|
|
+ n.window = n.window.Expand(window)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields
|
|
|
+func (n *Network) SetStartEnd(start, end time.Time) {
|
|
|
+ if n.Window().Contains(start) {
|
|
|
+ n.start = start
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: start %s not in %s", start, n.Window())
|
|
|
+ }
|
|
|
+
|
|
|
+ if n.Window().Contains(end) {
|
|
|
+ n.end = end
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: end %s not in %s", end, n.Window())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (n *Network) Add(a Asset) Asset {
|
|
|
+ // Network + Network = Network
|
|
|
+ if that, ok := a.(*Network); ok {
|
|
|
+ this := n.Clone().(*Network)
|
|
|
+ this.add(that)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ props := n.Properties().Merge(a.Properties())
|
|
|
+ labels := n.Labels().Merge(a.Labels())
|
|
|
+
|
|
|
+ start := n.Start()
|
|
|
+ if a.Start().Before(start) {
|
|
|
+ start = a.Start()
|
|
|
+ }
|
|
|
+ end := n.End()
|
|
|
+ if a.End().After(end) {
|
|
|
+ end = a.End()
|
|
|
+ }
|
|
|
+ window := n.Window().Expand(a.Window())
|
|
|
+
|
|
|
+ // Network + !Network = Any
|
|
|
+ any := NewAsset(start, end, window)
|
|
|
+ any.SetProperties(props)
|
|
|
+ any.SetLabels(labels)
|
|
|
+ any.adjustment = n.Adjustment() + a.Adjustment()
|
|
|
+ any.Cost = (n.TotalCost() - n.Adjustment()) + (a.TotalCost() - a.Adjustment())
|
|
|
+
|
|
|
+ return any
|
|
|
+}
|
|
|
+
|
|
|
+func (n *Network) add(that *Network) {
|
|
|
+ if n == nil {
|
|
|
+ n = that
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ props := n.Properties().Merge(that.Properties())
|
|
|
+ labels := n.Labels().Merge(that.Labels())
|
|
|
+ n.SetProperties(props)
|
|
|
+ n.SetLabels(labels)
|
|
|
+
|
|
|
+ start := n.Start()
|
|
|
+ if that.Start().Before(start) {
|
|
|
+ start = that.Start()
|
|
|
+ }
|
|
|
+ end := n.End()
|
|
|
+ if that.End().After(end) {
|
|
|
+ end = that.End()
|
|
|
+ }
|
|
|
+ window := n.Window().Expand(that.Window())
|
|
|
+ n.start = start
|
|
|
+ n.end = end
|
|
|
+ n.window = window
|
|
|
+
|
|
|
+ n.Cost += that.Cost
|
|
|
+ n.adjustment += that.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a deep copy of the given Network
|
|
|
+func (n *Network) Clone() Asset {
|
|
|
+ if n == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Network{
|
|
|
+ properties: n.properties.Clone(),
|
|
|
+ labels: n.labels.Clone(),
|
|
|
+ start: n.start,
|
|
|
+ end: n.end,
|
|
|
+ window: n.window.Clone(),
|
|
|
+ adjustment: n.adjustment,
|
|
|
+ Cost: n.Cost,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the tow Assets match exactly
|
|
|
+func (n *Network) Equal(a Asset) bool {
|
|
|
+ that, ok := a.(*Network)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !n.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !n.Start().Equal(that.Start()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.End().Equal(that.End()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.window.Equal(that.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if n.adjustment != that.adjustment {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.Cost != that.Cost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements json.Marshal interface
|
|
|
+func (n *Network) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncodeString(buffer, "type", n.Type().String(), ",")
|
|
|
+ jsonEncode(buffer, "properties", n.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", n.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", n.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", n.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", n.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (n *Network) String() string {
|
|
|
+ return toString(n)
|
|
|
+}
|
|
|
+
|
|
|
+// Node is an Asset representing a single node in a cluster
|
|
|
+type Node struct {
|
|
|
+ properties *AssetProperties
|
|
|
+ labels AssetLabels
|
|
|
+ start time.Time
|
|
|
+ end time.Time
|
|
|
+ window Window
|
|
|
+ adjustment float64
|
|
|
+ NodeType string
|
|
|
+ CPUCoreHours float64
|
|
|
+ RAMByteHours float64
|
|
|
+ CPUBreakdown *Breakdown
|
|
|
+ RAMBreakdown *Breakdown
|
|
|
+ CPUCost float64
|
|
|
+ GPUCost float64
|
|
|
+ RAMCost float64
|
|
|
+ Discount float64
|
|
|
+ Preemptible float64
|
|
|
+}
|
|
|
+
|
|
|
+// NewNode creates and returns a new Node Asset
|
|
|
+func NewNode(name, cluster, providerID string, start, end time.Time, window Window) *Node {
|
|
|
+ properties := &AssetProperties{
|
|
|
+ Category: ComputeCategory,
|
|
|
+ Name: name,
|
|
|
+ Cluster: cluster,
|
|
|
+ ProviderID: providerID,
|
|
|
+ Service: KubernetesService,
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Node{
|
|
|
+ properties: properties,
|
|
|
+ labels: AssetLabels{},
|
|
|
+ start: start,
|
|
|
+ end: end,
|
|
|
+ window: window.Clone(),
|
|
|
+ CPUBreakdown: &Breakdown{},
|
|
|
+ RAMBreakdown: &Breakdown{},
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the AssetType of the Asset
|
|
|
+func (n *Node) Type() AssetType {
|
|
|
+ return NodeAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the Asset's properties
|
|
|
+func (n *Node) Properties() *AssetProperties {
|
|
|
+ return n.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (n *Node) SetProperties(props *AssetProperties) {
|
|
|
+ n.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the Asset's labels
|
|
|
+func (n *Node) Labels() AssetLabels {
|
|
|
+ return n.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's labels
|
|
|
+func (n *Node) SetLabels(labels AssetLabels) {
|
|
|
+ n.labels = labels
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment returns the Asset's cost adjustment
|
|
|
+func (n *Node) Adjustment() float64 {
|
|
|
+ return n.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment sets the Asset's cost adjustment
|
|
|
+func (n *Node) SetAdjustment(adj float64) {
|
|
|
+ n.adjustment = adj
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the Asset's total cost
|
|
|
+func (n *Node) TotalCost() float64 {
|
|
|
+ return ((n.CPUCost + n.RAMCost) * (1.0 - n.Discount)) + n.GPUCost + n.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the precise start time of the Asset within the window
|
|
|
+func (n *Node) Start() time.Time {
|
|
|
+ return n.start
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the precise end time of the Asset within the window
|
|
|
+func (n *Node) End() time.Time {
|
|
|
+ return n.end
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of minutes the Asset ran within the window
|
|
|
+func (n *Node) Minutes() float64 {
|
|
|
+ nodeMins := n.end.Sub(n.start).Minutes()
|
|
|
+ windowMins := n.window.Minutes()
|
|
|
+
|
|
|
+ if nodeMins > windowMins {
|
|
|
+ log.Warningf("Asset ETL: Node.Minutes exceeds window: %.2f > %.2f", nodeMins, windowMins)
|
|
|
+ nodeMins = windowMins
|
|
|
+ }
|
|
|
+
|
|
|
+ if nodeMins < 0 {
|
|
|
+ nodeMins = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ return nodeMins
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the window within which the Asset ran
|
|
|
+func (n *Node) Window() Window {
|
|
|
+ return n.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window by the given window
|
|
|
+func (n *Node) ExpandWindow(window Window) {
|
|
|
+ n.window = n.window.Expand(window)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields
|
|
|
+func (n *Node) SetStartEnd(start, end time.Time) {
|
|
|
+ if n.Window().Contains(start) {
|
|
|
+ n.start = start
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: start %s not in %s", start, n.Window())
|
|
|
+ }
|
|
|
+
|
|
|
+ if n.Window().Contains(end) {
|
|
|
+ n.end = end
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: end %s not in %s", end, n.Window())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (n *Node) Add(a Asset) Asset {
|
|
|
+ // Node + Node = Node
|
|
|
+ if that, ok := a.(*Node); ok {
|
|
|
+ this := n.Clone().(*Node)
|
|
|
+ this.add(that)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ props := n.Properties().Merge(a.Properties())
|
|
|
+ labels := n.Labels().Merge(a.Labels())
|
|
|
+
|
|
|
+ start := n.Start()
|
|
|
+ if a.Start().Before(start) {
|
|
|
+ start = a.Start()
|
|
|
+ }
|
|
|
+ end := n.End()
|
|
|
+ if a.End().After(end) {
|
|
|
+ end = a.End()
|
|
|
+ }
|
|
|
+ window := n.Window().Expand(a.Window())
|
|
|
+
|
|
|
+ // Node + !Node = Any
|
|
|
+ any := NewAsset(start, end, window)
|
|
|
+ any.SetProperties(props)
|
|
|
+ any.SetLabels(labels)
|
|
|
+ any.adjustment = n.Adjustment() + a.Adjustment()
|
|
|
+ any.Cost = (n.TotalCost() - n.Adjustment()) + (a.TotalCost() - a.Adjustment())
|
|
|
+
|
|
|
+ return any
|
|
|
+}
|
|
|
+
|
|
|
+func (n *Node) add(that *Node) {
|
|
|
+ if n == nil {
|
|
|
+ n = that
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ props := n.Properties().Merge(that.Properties())
|
|
|
+ labels := n.Labels().Merge(that.Labels())
|
|
|
+ n.SetProperties(props)
|
|
|
+ n.SetLabels(labels)
|
|
|
+
|
|
|
+ if n.NodeType != that.NodeType {
|
|
|
+ n.NodeType = ""
|
|
|
+ }
|
|
|
+
|
|
|
+ start := n.Start()
|
|
|
+ if that.Start().Before(start) {
|
|
|
+ start = that.Start()
|
|
|
+ }
|
|
|
+ end := n.End()
|
|
|
+ if that.End().After(end) {
|
|
|
+ end = that.End()
|
|
|
+ }
|
|
|
+ window := n.Window().Expand(that.Window())
|
|
|
+ n.start = start
|
|
|
+ n.end = end
|
|
|
+ n.window = window
|
|
|
+
|
|
|
+ // Order of operations for node costs is:
|
|
|
+ // Discount(CPU + RAM) + GPU + Adjustment
|
|
|
+
|
|
|
+ // Combining discounts, then involves weighting each discount by each
|
|
|
+ // respective (CPU + RAM) cost. Combining preemptible, on the other
|
|
|
+ // hand, is done with all three (but not Adjustment, which can change
|
|
|
+ // without triggering a re-computation of Preemtible).
|
|
|
+
|
|
|
+ disc := (n.CPUCost+n.RAMCost)*(1.0-n.Discount) + (that.CPUCost+that.RAMCost)*(1.0-that.Discount)
|
|
|
+ nonDisc := (n.CPUCost + n.RAMCost) + (that.CPUCost + that.RAMCost)
|
|
|
+ if nonDisc > 0 {
|
|
|
+ n.Discount = 1.0 - (disc / nonDisc)
|
|
|
+ } else {
|
|
|
+ n.Discount = (n.Discount + that.Discount) / 2.0
|
|
|
+ }
|
|
|
+
|
|
|
+ nNoAdj := n.TotalCost() - n.Adjustment()
|
|
|
+ thatNoAdj := that.TotalCost() - that.Adjustment()
|
|
|
+ if (nNoAdj + thatNoAdj) > 0 {
|
|
|
+ n.Preemptible = (nNoAdj*n.Preemptible + thatNoAdj*that.Preemptible) / (nNoAdj + thatNoAdj)
|
|
|
+ } else {
|
|
|
+ n.Preemptible = (n.Preemptible + that.Preemptible) / 2.0
|
|
|
+ }
|
|
|
+
|
|
|
+ totalCPUCost := n.CPUCost + that.CPUCost
|
|
|
+ if totalCPUCost > 0.0 {
|
|
|
+ n.CPUBreakdown.Idle = (n.CPUBreakdown.Idle*n.CPUCost + that.CPUBreakdown.Idle*that.CPUCost) / totalCPUCost
|
|
|
+ n.CPUBreakdown.Other = (n.CPUBreakdown.Other*n.CPUCost + that.CPUBreakdown.Other*that.CPUCost) / totalCPUCost
|
|
|
+ n.CPUBreakdown.System = (n.CPUBreakdown.System*n.CPUCost + that.CPUBreakdown.System*that.CPUCost) / totalCPUCost
|
|
|
+ n.CPUBreakdown.User = (n.CPUBreakdown.User*n.CPUCost + that.CPUBreakdown.User*that.CPUCost) / totalCPUCost
|
|
|
+ }
|
|
|
+
|
|
|
+ totalRAMCost := n.RAMCost + that.RAMCost
|
|
|
+ if totalRAMCost > 0.0 {
|
|
|
+ n.RAMBreakdown.Idle = (n.RAMBreakdown.Idle*n.RAMCost + that.RAMBreakdown.Idle*that.RAMCost) / totalRAMCost
|
|
|
+ n.RAMBreakdown.Other = (n.RAMBreakdown.Other*n.RAMCost + that.RAMBreakdown.Other*that.RAMCost) / totalRAMCost
|
|
|
+ n.RAMBreakdown.System = (n.RAMBreakdown.System*n.RAMCost + that.RAMBreakdown.System*that.RAMCost) / totalRAMCost
|
|
|
+ n.RAMBreakdown.User = (n.RAMBreakdown.User*n.RAMCost + that.RAMBreakdown.User*that.RAMCost) / totalRAMCost
|
|
|
+ }
|
|
|
+
|
|
|
+ n.CPUCoreHours += that.CPUCoreHours
|
|
|
+ n.RAMByteHours += that.RAMByteHours
|
|
|
+
|
|
|
+ n.CPUCost += that.CPUCost
|
|
|
+ n.GPUCost += that.GPUCost
|
|
|
+ n.RAMCost += that.RAMCost
|
|
|
+ n.adjustment += that.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a deep copy of the given Node
|
|
|
+func (n *Node) Clone() Asset {
|
|
|
+ if n == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return &Node{
|
|
|
+ properties: n.properties.Clone(),
|
|
|
+ labels: n.labels.Clone(),
|
|
|
+ start: n.start,
|
|
|
+ end: n.end,
|
|
|
+ window: n.window.Clone(),
|
|
|
+ adjustment: n.adjustment,
|
|
|
+ NodeType: n.NodeType,
|
|
|
+ CPUCoreHours: n.CPUCoreHours,
|
|
|
+ RAMByteHours: n.RAMByteHours,
|
|
|
+ CPUBreakdown: n.CPUBreakdown.Clone(),
|
|
|
+ RAMBreakdown: n.RAMBreakdown.Clone(),
|
|
|
+ CPUCost: n.CPUCost,
|
|
|
+ GPUCost: n.GPUCost,
|
|
|
+ RAMCost: n.RAMCost,
|
|
|
+ Preemptible: n.Preemptible,
|
|
|
+ Discount: n.Discount,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the tow Assets match exactly
|
|
|
+func (n *Node) Equal(a Asset) bool {
|
|
|
+ that, ok := a.(*Node)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !n.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !n.Start().Equal(that.Start()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.End().Equal(that.End()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.window.Equal(that.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if n.adjustment != that.adjustment {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if n.NodeType != that.NodeType {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.CPUCoreHours != that.CPUCoreHours {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.RAMByteHours != that.RAMByteHours {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.CPUBreakdown.Equal(that.CPUBreakdown) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !n.RAMBreakdown.Equal(that.RAMBreakdown) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.CPUCost != that.CPUCost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.GPUCost != that.GPUCost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.RAMCost != that.RAMCost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.Discount != that.Discount {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if n.Preemptible != that.Preemptible {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements json.Marshal interface
|
|
|
+func (n *Node) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncodeString(buffer, "type", n.Type().String(), ",")
|
|
|
+ jsonEncode(buffer, "properties", n.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", n.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", n.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", n.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", n.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
|
|
|
+ jsonEncodeString(buffer, "nodeType", n.NodeType, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "cpuCores", n.CPUCores(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "ramBytes", n.RAMBytes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "cpuCoreHours", n.CPUCoreHours, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "ramByteHours", n.RAMByteHours, ",")
|
|
|
+ jsonEncode(buffer, "cpuBreakdown", n.CPUBreakdown, ",")
|
|
|
+ jsonEncode(buffer, "ramBreakdown", n.RAMBreakdown, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "preemptible", n.Preemptible, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "discount", n.Discount, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "cpuCost", n.CPUCost, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "gpuCost", n.GPUCost, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "ramCost", n.RAMCost, ",")
|
|
|
+ jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (n *Node) String() string {
|
|
|
+ return toString(n)
|
|
|
+}
|
|
|
+
|
|
|
+// IsPreemptible returns true if the node is 100% preemptible. It's possible
|
|
|
+// to be "partially preemptible" by adding a preemptible node with a
|
|
|
+// non-preemptible node.
|
|
|
+func (n *Node) IsPreemptible() bool {
|
|
|
+ return n.Preemptible == 1.0
|
|
|
+}
|
|
|
+
|
|
|
+// CPUCores returns the number of cores belonging to the node. This could be
|
|
|
+// fractional because it's the number of core*hours divided by the number of
|
|
|
+// hours running; e.g. the sum of a 4-core node running for the first 10 hours
|
|
|
+// and a 3-core node running for the last 20 hours of the same 24-hour window
|
|
|
+// would produce:
|
|
|
+// (4*10 + 3*20) / 24 = 4.167 cores
|
|
|
+// However, any number of cores running for the full span of a window will
|
|
|
+// report the actual number of cores of the static node; e.g. the above
|
|
|
+// scenario for one entire 24-hour window:
|
|
|
+// (4*24 + 3*24) / 24 = (4 + 3) = 7 cores
|
|
|
+func (n *Node) CPUCores() float64 {
|
|
|
+ // [core*hr]*([min/hr]*[1/min]) = [core*hr]/[hr] = core
|
|
|
+ return n.CPUCoreHours * (60.0 / n.Minutes())
|
|
|
+}
|
|
|
+
|
|
|
+// RAMBytes returns the amount of RAM belonging to the node. This could be
|
|
|
+// fractional because it's the number of byte*hours divided by the number of
|
|
|
+// hours running; e.g. the sum of a 12GiB-RAM node running for the first 10 hours
|
|
|
+// and a 16GiB-RAM node running for the last 20 hours of the same 24-hour window
|
|
|
+// would produce:
|
|
|
+// (12*10 + 16*20) / 24 = 18.333GiB RAM
|
|
|
+// However, any number of cores running for the full span of a window will
|
|
|
+// report the actual number of cores of the static node; e.g. the above
|
|
|
+// scenario for one entire 24-hour window:
|
|
|
+// (12*24 + 16*24) / 24 = (12 + 16) = 28 cores
|
|
|
+func (n *Node) RAMBytes() float64 {
|
|
|
+ // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
|
|
|
+ return n.RAMByteHours * (60.0 / n.Minutes())
|
|
|
+}
|
|
|
+
|
|
|
+// LoadBalancer is an Asset representing a single load balancer in a cluster
|
|
|
+// TODO: add GB of ingress processed, numForwardingRules once we start recording those to prometheus metric
|
|
|
+type LoadBalancer struct {
|
|
|
+ properties *AssetProperties
|
|
|
+ labels AssetLabels
|
|
|
+ start time.Time
|
|
|
+ end time.Time
|
|
|
+ window Window
|
|
|
+ adjustment float64
|
|
|
+ Cost float64
|
|
|
+}
|
|
|
+
|
|
|
+// NewLoadBalancer instantiates and returns a new LoadBalancer
|
|
|
+func NewLoadBalancer(name, cluster, providerID string, start, end time.Time, window Window) *LoadBalancer {
|
|
|
+ properties := &AssetProperties{
|
|
|
+ Category: NetworkCategory,
|
|
|
+ Name: name,
|
|
|
+ Cluster: cluster,
|
|
|
+ ProviderID: providerID,
|
|
|
+ Service: KubernetesService,
|
|
|
+ }
|
|
|
+
|
|
|
+ return &LoadBalancer{
|
|
|
+ properties: properties,
|
|
|
+ labels: AssetLabels{},
|
|
|
+ start: start,
|
|
|
+ end: end,
|
|
|
+ window: window,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the AssetType of the Asset
|
|
|
+func (lb *LoadBalancer) Type() AssetType {
|
|
|
+ return LoadBalancerAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the Asset's properties
|
|
|
+func (lb *LoadBalancer) Properties() *AssetProperties {
|
|
|
+ return lb.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (lb *LoadBalancer) SetProperties(props *AssetProperties) {
|
|
|
+ lb.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the Asset's labels
|
|
|
+func (lb *LoadBalancer) Labels() AssetLabels {
|
|
|
+ return lb.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's labels
|
|
|
+func (lb *LoadBalancer) SetLabels(labels AssetLabels) {
|
|
|
+ lb.labels = labels
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment returns the Asset's cost adjustment
|
|
|
+func (lb *LoadBalancer) Adjustment() float64 {
|
|
|
+ return lb.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment sets the Asset's cost adjustment
|
|
|
+func (lb *LoadBalancer) SetAdjustment(adj float64) {
|
|
|
+ lb.adjustment = adj
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the total cost of the Asset
|
|
|
+func (lb *LoadBalancer) TotalCost() float64 {
|
|
|
+ return lb.Cost + lb.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the preceise start point of the Asset within the window
|
|
|
+func (lb *LoadBalancer) Start() time.Time {
|
|
|
+ return lb.start
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the preceise end point of the Asset within the window
|
|
|
+func (lb *LoadBalancer) End() time.Time {
|
|
|
+ return lb.end
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of minutes the Asset ran within the window
|
|
|
+func (lb *LoadBalancer) Minutes() float64 {
|
|
|
+ return lb.end.Sub(lb.start).Minutes()
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the window within which the Asset ran
|
|
|
+func (lb *LoadBalancer) Window() Window {
|
|
|
+ return lb.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window by the given window
|
|
|
+func (lb *LoadBalancer) ExpandWindow(w Window) {
|
|
|
+ lb.window = lb.window.Expand(w)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields
|
|
|
+func (lb *LoadBalancer) SetStartEnd(start, end time.Time) {
|
|
|
+ if lb.Window().Contains(start) {
|
|
|
+ lb.start = start
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: start %s not in %s", start, lb.Window())
|
|
|
+ }
|
|
|
+
|
|
|
+ if lb.Window().Contains(end) {
|
|
|
+ lb.end = end
|
|
|
+ } else {
|
|
|
+ log.Warningf("Disk.SetStartEnd: end %s not in %s", end, lb.Window())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (lb *LoadBalancer) Add(a Asset) Asset {
|
|
|
+ // LoadBalancer + LoadBalancer = LoadBalancer
|
|
|
+ if that, ok := a.(*LoadBalancer); ok {
|
|
|
+ this := lb.Clone().(*LoadBalancer)
|
|
|
+ this.add(that)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ props := lb.Properties().Merge(a.Properties())
|
|
|
+ labels := lb.Labels().Merge(a.Labels())
|
|
|
+
|
|
|
+ start := lb.Start()
|
|
|
+ if a.Start().Before(start) {
|
|
|
+ start = a.Start()
|
|
|
+ }
|
|
|
+ end := lb.End()
|
|
|
+ if a.End().After(end) {
|
|
|
+ end = a.End()
|
|
|
+ }
|
|
|
+ window := lb.Window().Expand(a.Window())
|
|
|
+
|
|
|
+ // LoadBalancer + !LoadBalancer = Any
|
|
|
+ any := NewAsset(start, end, window)
|
|
|
+ any.SetProperties(props)
|
|
|
+ any.SetLabels(labels)
|
|
|
+ any.adjustment = lb.Adjustment() + a.Adjustment()
|
|
|
+ any.Cost = (lb.TotalCost() - lb.Adjustment()) + (a.TotalCost() - a.Adjustment())
|
|
|
+
|
|
|
+ return any
|
|
|
+}
|
|
|
+
|
|
|
+func (lb *LoadBalancer) add(that *LoadBalancer) {
|
|
|
+ if lb == nil {
|
|
|
+ lb = that
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ props := lb.Properties().Merge(that.Properties())
|
|
|
+ labels := lb.Labels().Merge(that.Labels())
|
|
|
+ lb.SetProperties(props)
|
|
|
+ lb.SetLabels(labels)
|
|
|
+
|
|
|
+ start := lb.Start()
|
|
|
+ if that.Start().Before(start) {
|
|
|
+ start = that.Start()
|
|
|
+ }
|
|
|
+ end := lb.End()
|
|
|
+ if that.End().After(end) {
|
|
|
+ end = that.End()
|
|
|
+ }
|
|
|
+ window := lb.Window().Expand(that.Window())
|
|
|
+ lb.start = start
|
|
|
+ lb.end = end
|
|
|
+ lb.window = window
|
|
|
+
|
|
|
+ lb.Cost += that.Cost
|
|
|
+ lb.adjustment += that.adjustment
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a cloned instance of the given Asset
|
|
|
+func (lb *LoadBalancer) Clone() Asset {
|
|
|
+ return &LoadBalancer{
|
|
|
+ properties: lb.properties.Clone(),
|
|
|
+ labels: lb.labels.Clone(),
|
|
|
+ start: lb.start,
|
|
|
+ end: lb.end,
|
|
|
+ window: lb.window.Clone(),
|
|
|
+ adjustment: lb.adjustment,
|
|
|
+ Cost: lb.Cost,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the tow Assets match precisely
|
|
|
+func (lb *LoadBalancer) Equal(a Asset) bool {
|
|
|
+ that, ok := a.(*LoadBalancer)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !lb.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !lb.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !lb.Start().Equal(that.Start()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !lb.End().Equal(that.End()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !lb.window.Equal(that.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if lb.adjustment != that.adjustment {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if lb.Cost != that.Cost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements json.Marshal
|
|
|
+func (lb *LoadBalancer) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncodeString(buffer, "type", lb.Type().String(), ",")
|
|
|
+ jsonEncode(buffer, "properties", lb.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", lb.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", lb.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", lb.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", lb.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", lb.Minutes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "adjustment", lb.Adjustment(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", lb.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (lb *LoadBalancer) String() string {
|
|
|
+ return toString(lb)
|
|
|
+}
|
|
|
+
|
|
|
+// SharedAsset is an Asset representing a shared cost
|
|
|
+type SharedAsset struct {
|
|
|
+ properties *AssetProperties
|
|
|
+ labels AssetLabels
|
|
|
+ window Window
|
|
|
+ Cost float64
|
|
|
+}
|
|
|
+
|
|
|
+// NewSharedAsset creates and returns a new SharedAsset
|
|
|
+func NewSharedAsset(name string, window Window) *SharedAsset {
|
|
|
+ properties := &AssetProperties{
|
|
|
+ Name: name,
|
|
|
+ Category: SharedCategory,
|
|
|
+ Service: OtherCategory,
|
|
|
+ }
|
|
|
+
|
|
|
+ return &SharedAsset{
|
|
|
+ properties: properties,
|
|
|
+ labels: AssetLabels{},
|
|
|
+ window: window.Clone(),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the AssetType of the Asset
|
|
|
+func (sa *SharedAsset) Type() AssetType {
|
|
|
+ return SharedAssetType
|
|
|
+}
|
|
|
+
|
|
|
+// Properties returns the Asset's properties
|
|
|
+func (sa *SharedAsset) Properties() *AssetProperties {
|
|
|
+ return sa.properties
|
|
|
+}
|
|
|
+
|
|
|
+// SetProperties sets the Asset's properties
|
|
|
+func (sa *SharedAsset) SetProperties(props *AssetProperties) {
|
|
|
+ sa.properties = props
|
|
|
+}
|
|
|
+
|
|
|
+// Labels returns the Asset's labels
|
|
|
+func (sa *SharedAsset) Labels() AssetLabels {
|
|
|
+ return sa.labels
|
|
|
+}
|
|
|
+
|
|
|
+// SetLabels sets the Asset's labels
|
|
|
+func (sa *SharedAsset) SetLabels(labels AssetLabels) {
|
|
|
+ sa.labels = labels
|
|
|
+}
|
|
|
+
|
|
|
+// Adjustment is not relevant to SharedAsset, but required to implement Asset
|
|
|
+func (sa *SharedAsset) Adjustment() float64 {
|
|
|
+ return 0.0
|
|
|
+}
|
|
|
+
|
|
|
+// SetAdjustment is not relevant to SharedAsset, but required to implement Asset
|
|
|
+func (sa *SharedAsset) SetAdjustment(float64) {
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// TotalCost returns the Asset's total cost
|
|
|
+func (sa *SharedAsset) TotalCost() float64 {
|
|
|
+ return sa.Cost
|
|
|
+}
|
|
|
+
|
|
|
+// Start returns the start time of the Asset
|
|
|
+func (sa *SharedAsset) Start() time.Time {
|
|
|
+ return *sa.window.start
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the end time of the Asset
|
|
|
+func (sa *SharedAsset) End() time.Time {
|
|
|
+ return *sa.window.end
|
|
|
+}
|
|
|
+
|
|
|
+// Minutes returns the number of minutes the SharedAsset ran within the window
|
|
|
+func (sa *SharedAsset) Minutes() float64 {
|
|
|
+ return sa.window.Minutes()
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the window within the SharedAsset ran
|
|
|
+func (sa *SharedAsset) Window() Window {
|
|
|
+ return sa.window
|
|
|
+}
|
|
|
+
|
|
|
+// ExpandWindow expands the Asset's window
|
|
|
+func (sa *SharedAsset) ExpandWindow(w Window) {
|
|
|
+ sa.window = sa.window.Expand(w)
|
|
|
+}
|
|
|
+
|
|
|
+// SetStartEnd sets the Asset's Start and End fields (not applicable here)
|
|
|
+func (sa *SharedAsset) SetStartEnd(start, end time.Time) {
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// Add sums the Asset with the given Asset to produce a new Asset, maintaining
|
|
|
+// as much relevant information as possible (i.e. type, properties, labels).
|
|
|
+func (sa *SharedAsset) Add(a Asset) Asset {
|
|
|
+ // SharedAsset + SharedAsset = SharedAsset
|
|
|
+ if that, ok := a.(*SharedAsset); ok {
|
|
|
+ this := sa.Clone().(*SharedAsset)
|
|
|
+ this.add(that)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ props := sa.Properties().Merge(a.Properties())
|
|
|
+ labels := sa.Labels().Merge(a.Labels())
|
|
|
+
|
|
|
+ start := sa.Start()
|
|
|
+ if a.Start().Before(start) {
|
|
|
+ start = a.Start()
|
|
|
+ }
|
|
|
+ end := sa.End()
|
|
|
+ if a.End().After(end) {
|
|
|
+ end = a.End()
|
|
|
+ }
|
|
|
+ window := sa.Window().Expand(a.Window())
|
|
|
+
|
|
|
+ // SharedAsset + !SharedAsset = Any
|
|
|
+ any := NewAsset(start, end, window)
|
|
|
+ any.SetProperties(props)
|
|
|
+ any.SetLabels(labels)
|
|
|
+ any.adjustment = sa.Adjustment() + a.Adjustment()
|
|
|
+ any.Cost = (sa.TotalCost() - sa.Adjustment()) + (a.TotalCost() - a.Adjustment())
|
|
|
+
|
|
|
+ return any
|
|
|
+}
|
|
|
+
|
|
|
+func (sa *SharedAsset) add(that *SharedAsset) {
|
|
|
+ if sa == nil {
|
|
|
+ sa = that
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ props := sa.Properties().Merge(that.Properties())
|
|
|
+ labels := sa.Labels().Merge(that.Labels())
|
|
|
+ sa.SetProperties(props)
|
|
|
+ sa.SetLabels(labels)
|
|
|
+
|
|
|
+ window := sa.Window().Expand(that.Window())
|
|
|
+ sa.window = window
|
|
|
+
|
|
|
+ sa.Cost += that.Cost
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a deep copy of the given SharedAsset
|
|
|
+func (sa *SharedAsset) Clone() Asset {
|
|
|
+ if sa == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return &SharedAsset{
|
|
|
+ properties: sa.properties.Clone(),
|
|
|
+ labels: sa.labels.Clone(),
|
|
|
+ window: sa.window.Clone(),
|
|
|
+ Cost: sa.Cost,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Equal returns true if the two Assets are exact matches
|
|
|
+func (sa *SharedAsset) Equal(a Asset) bool {
|
|
|
+ that, ok := a.(*SharedAsset)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !sa.Labels().Equal(that.Labels()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !sa.Properties().Equal(that.Properties()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if !sa.Start().Equal(that.Start()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !sa.End().Equal(that.End()) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if !sa.window.Equal(that.window) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if sa.Cost != that.Cost {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON implements json.Marshaler
|
|
|
+func (sa *SharedAsset) MarshalJSON() ([]byte, error) {
|
|
|
+ buffer := bytes.NewBufferString("{")
|
|
|
+ jsonEncodeString(buffer, "type", sa.Type().String(), ",")
|
|
|
+ jsonEncode(buffer, "properties", sa.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", sa.Labels(), ",")
|
|
|
+ jsonEncode(buffer, "properties", sa.Properties(), ",")
|
|
|
+ jsonEncode(buffer, "labels", sa.Labels(), ",")
|
|
|
+ jsonEncodeString(buffer, "window", sa.Window().String(), ",")
|
|
|
+ jsonEncodeString(buffer, "start", sa.Start().Format(timeFmt), ",")
|
|
|
+ jsonEncodeString(buffer, "end", sa.End().Format(timeFmt), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "minutes", sa.Minutes(), ",")
|
|
|
+ jsonEncodeFloat64(buffer, "totalCost", sa.TotalCost(), "")
|
|
|
+ buffer.WriteString("}")
|
|
|
+ return buffer.Bytes(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// String implements fmt.Stringer
|
|
|
+func (sa *SharedAsset) String() string {
|
|
|
+ return toString(sa)
|
|
|
+}
|
|
|
+
|
|
|
+// AssetSet stores a set of Assets, each with a unique name, that share
|
|
|
+// a window. An AssetSet is mutable, so treat it like a threadsafe map.
|
|
|
+type AssetSet struct {
|
|
|
+ sync.RWMutex
|
|
|
+ assets map[string]Asset
|
|
|
+ props []AssetProperty
|
|
|
+ Window Window
|
|
|
+}
|
|
|
+
|
|
|
+// NewAssetSet instantiates a new AssetSet and, optionally, inserts
|
|
|
+// the given list of Assets
|
|
|
+func NewAssetSet(start, end time.Time, assets ...Asset) *AssetSet {
|
|
|
+ as := &AssetSet{
|
|
|
+ assets: map[string]Asset{},
|
|
|
+ Window: NewWindow(&start, &end),
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, a := range assets {
|
|
|
+ as.Insert(a)
|
|
|
+ }
|
|
|
+
|
|
|
+ return as
|
|
|
+}
|
|
|
+
|
|
|
+// AggregateBy aggregates the Assets in the AssetSet by the given list of
|
|
|
+// AssetProperties, such that each asset is binned by a key determined by its
|
|
|
+// relevant property values.
|
|
|
+func (as *AssetSet) AggregateBy(props []AssetProperty, opts *AssetAggregationOptions) error {
|
|
|
+ if opts == nil {
|
|
|
+ opts = &AssetAggregationOptions{}
|
|
|
+ }
|
|
|
+
|
|
|
+ if as.IsEmpty() && len(opts.SharedHourlyCosts) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ as.Lock()
|
|
|
+ defer as.Unlock()
|
|
|
+
|
|
|
+ aggSet := NewAssetSet(as.Start(), as.End())
|
|
|
+ aggSet.props = props
|
|
|
+
|
|
|
+ // Compute hours of the given AssetSet, and if it ends in the future,
|
|
|
+ // adjust the hours accordingly
|
|
|
+ hours := as.Window.Minutes() / 60.0
|
|
|
+ diff := time.Now().Sub(as.End())
|
|
|
+ if diff < 0.0 {
|
|
|
+ hours += diff.Hours()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Insert a shared asset for each shared cost
|
|
|
+ for name, hourlyCost := range opts.SharedHourlyCosts {
|
|
|
+ sa := NewSharedAsset(name, as.Window.Clone())
|
|
|
+ sa.Cost = hourlyCost * hours
|
|
|
+
|
|
|
+ aggSet.Insert(sa)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Delete the Assets that don't pass each filter
|
|
|
+ for _, ff := range opts.FilterFuncs {
|
|
|
+ for key, asset := range as.assets {
|
|
|
+ if !ff(asset) {
|
|
|
+ delete(as.assets, key)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Insert each asset into the new set, which will be keyed by the props
|
|
|
+ // on aggSet, resulting in aggregation.
|
|
|
+ for _, asset := range as.assets {
|
|
|
+ aggSet.Insert(asset)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Assign the aggregated values back to the original set
|
|
|
+ as.assets = aggSet.assets
|
|
|
+ as.props = props
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Clone returns a new AssetSet with a deep copy of the given
|
|
|
+// AssetSet's assets.
|
|
|
+func (as *AssetSet) Clone() *AssetSet {
|
|
|
+ if as == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+
|
|
|
+ assets := map[string]Asset{}
|
|
|
+ for k, v := range as.assets {
|
|
|
+ assets[k] = v.Clone()
|
|
|
+ }
|
|
|
+
|
|
|
+ var props []AssetProperty
|
|
|
+ if as.props != nil {
|
|
|
+ props = []AssetProperty{}
|
|
|
+ for _, p := range as.props {
|
|
|
+ props = append(props, p)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ s := as.Start()
|
|
|
+ e := as.End()
|
|
|
+
|
|
|
+ return &AssetSet{
|
|
|
+ Window: NewWindow(&s, &e),
|
|
|
+ assets: assets,
|
|
|
+ props: props,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Each invokes the given function for each Asset in the set
|
|
|
+func (as *AssetSet) Each(f func(string, Asset)) {
|
|
|
+ if as == nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ for k, a := range as.assets {
|
|
|
+ f(k, a)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// End returns the end time of the AssetSet's window
|
|
|
+func (as *AssetSet) End() time.Time {
|
|
|
+ return *as.Window.End()
|
|
|
+}
|
|
|
+
|
|
|
+// FindMatch attempts to find a match in the AssetSet for the given Asset on
|
|
|
+// the provided properties and labels. If a match is not found, FindMatch
|
|
|
+// returns nil and a Not Found error.
|
|
|
+func (as *AssetSet) FindMatch(query Asset, props []AssetProperty) (Asset, error) {
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+
|
|
|
+ matchKey := key(query, props)
|
|
|
+ for _, asset := range as.assets {
|
|
|
+ if key(asset, props) == matchKey {
|
|
|
+ return asset, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil, fmt.Errorf("Asset not found to match %s on %v", query, props)
|
|
|
+}
|
|
|
+
|
|
|
+// ReconciliationMatch attempts to find an exact match in the AssetSet on
|
|
|
+// (Category, ProviderID). If a match is found, it returns the Asset with the
|
|
|
+// intent to adjuts it. If no match exists, it attempts to find one on only
|
|
|
+// (ProviderID). If that match is found, it returns the Asset with the intent
|
|
|
+// to insert the associated Cloud cost.
|
|
|
+func (as *AssetSet) ReconciliationMatch(query Asset) (Asset, bool, error) {
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+
|
|
|
+ // Full match means matching on (Category, ProviderID)
|
|
|
+ fullMatchProps := []AssetProperty{AssetCategoryProp, AssetProviderIDProp}
|
|
|
+ fullMatchKey := key(query, fullMatchProps)
|
|
|
+
|
|
|
+ // Partial match means matching only on (ProviderID)
|
|
|
+ providerIDMatchProps := []AssetProperty{AssetProviderIDProp}
|
|
|
+ providerIDMatchKey := key(query, providerIDMatchProps)
|
|
|
+
|
|
|
+ var providerIDMatch Asset
|
|
|
+ for _, asset := range as.assets {
|
|
|
+ if key(asset, fullMatchProps) == fullMatchKey {
|
|
|
+ return asset, true, nil
|
|
|
+ }
|
|
|
+ if key(asset, providerIDMatchProps) == providerIDMatchKey {
|
|
|
+ // Found a partial match. Save it until after all other options
|
|
|
+ // have been checked for full matches.
|
|
|
+ providerIDMatch = asset
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // No full match was found, so return partial match, if found.
|
|
|
+ if providerIDMatch != nil {
|
|
|
+ return providerIDMatch, false, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil, false, fmt.Errorf("Asset not found to match %s", query)
|
|
|
+}
|
|
|
+
|
|
|
+// Get returns the Asset in the AssetSet at the given key, or nil and false
|
|
|
+// if no Asset exists for the given key
|
|
|
+func (as *AssetSet) Get(key string) (Asset, bool) {
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+
|
|
|
+ if a, ok := as.assets[key]; ok {
|
|
|
+ return a, true
|
|
|
+ }
|
|
|
+ return nil, false
|
|
|
+}
|
|
|
+
|
|
|
+// Insert inserts the given Asset into the AssetSet, using the AssetSet's
|
|
|
+// configured properties to determine the key under which the Asset will
|
|
|
+// be inserted.
|
|
|
+func (as *AssetSet) Insert(asset Asset) error {
|
|
|
+ if as.IsEmpty() {
|
|
|
+ as.Lock()
|
|
|
+ as.assets = map[string]Asset{}
|
|
|
+ as.Unlock()
|
|
|
+ }
|
|
|
+
|
|
|
+ as.Lock()
|
|
|
+ defer as.Unlock()
|
|
|
+
|
|
|
+ // Determine key into which to Insert the Asset.
|
|
|
+ k := key(asset, as.props)
|
|
|
+
|
|
|
+ // Add the given Asset to the existing entry, if there is one;
|
|
|
+ // otherwise just set directly into assets
|
|
|
+ if _, ok := as.assets[k]; !ok {
|
|
|
+ as.assets[k] = asset
|
|
|
+ } else {
|
|
|
+ as.assets[k] = as.assets[k].Add(asset)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Expand the window, just to be safe. It's possible that the asset will
|
|
|
+ // be set into the map without expanding it to the AssetSet's window.
|
|
|
+ as.assets[k].ExpandWindow(as.Window)
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// IsEmpty returns true if the AssetSet is nil, or if it contains
|
|
|
+// zero assets.
|
|
|
+func (as *AssetSet) IsEmpty() bool {
|
|
|
+ if as == nil || len(as.assets) == 0 {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+ return as.assets == nil || len(as.assets) == 0
|
|
|
+}
|
|
|
+
|
|
|
+func (as *AssetSet) Length() int {
|
|
|
+ if as == nil {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+ return len(as.assets)
|
|
|
+}
|
|
|
+
|
|
|
+// Map clones and returns a map of the AssetSet's Assets
|
|
|
+func (as *AssetSet) Map() map[string]Asset {
|
|
|
+ if as.IsEmpty() {
|
|
|
+ return map[string]Asset{}
|
|
|
+ }
|
|
|
+
|
|
|
+ return as.Clone().assets
|
|
|
+}
|
|
|
+
|
|
|
+// MarshalJSON JSON-encodes the AssetSet
|
|
|
+func (as *AssetSet) MarshalJSON() ([]byte, error) {
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+ return json.Marshal(as.assets)
|
|
|
+}
|
|
|
+
|
|
|
+func (as *AssetSet) Set(asset Asset, props []AssetProperty) {
|
|
|
+ if as.IsEmpty() {
|
|
|
+ as.Lock()
|
|
|
+ as.assets = map[string]Asset{}
|
|
|
+ as.Unlock()
|
|
|
+ }
|
|
|
+
|
|
|
+ as.Lock()
|
|
|
+ defer as.Unlock()
|
|
|
+
|
|
|
+ // Expand the window to match the AssetSet, then set it
|
|
|
+ asset.ExpandWindow(as.Window)
|
|
|
+ as.assets[key(asset, props)] = asset
|
|
|
+}
|
|
|
+
|
|
|
+func (as *AssetSet) Start() time.Time {
|
|
|
+ return *as.Window.Start()
|
|
|
+}
|
|
|
+
|
|
|
+func (as *AssetSet) TotalCost() float64 {
|
|
|
+ tc := 0.0
|
|
|
+
|
|
|
+ as.Lock()
|
|
|
+ defer as.Unlock()
|
|
|
+
|
|
|
+ for _, a := range as.assets {
|
|
|
+ tc += a.TotalCost()
|
|
|
+ }
|
|
|
+
|
|
|
+ return tc
|
|
|
+}
|
|
|
+
|
|
|
+func (as *AssetSet) UTCOffset() time.Duration {
|
|
|
+ _, zone := as.Start().Zone()
|
|
|
+ return time.Duration(zone) * time.Second
|
|
|
+}
|
|
|
+
|
|
|
+func (as *AssetSet) accumulate(that *AssetSet) (*AssetSet, error) {
|
|
|
+ if as == nil {
|
|
|
+ return that, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if that == nil {
|
|
|
+ return as, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // In the case of an AssetSetRange with empty entries, we may end up with
|
|
|
+ // an incoming as without props, even though we are trying to aggregate
|
|
|
+ // by props. This handles that case, assigning the correct props.
|
|
|
+ if !propsEqual(as.props, that.props) {
|
|
|
+ if len(as.props) == 0 {
|
|
|
+ as.props = that.props
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set start, end to min(start), max(end)
|
|
|
+ start := as.Start()
|
|
|
+ end := as.End()
|
|
|
+ if that.Start().Before(start) {
|
|
|
+ start = that.Start()
|
|
|
+ }
|
|
|
+ if that.End().After(end) {
|
|
|
+ end = that.End()
|
|
|
+ }
|
|
|
+
|
|
|
+ if as.IsEmpty() && that.IsEmpty() {
|
|
|
+ return NewAssetSet(start, end), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ acc := NewAssetSet(start, end)
|
|
|
+ acc.props = as.props
|
|
|
+
|
|
|
+ as.RLock()
|
|
|
+ defer as.RUnlock()
|
|
|
+
|
|
|
+ that.RLock()
|
|
|
+ defer that.RUnlock()
|
|
|
+
|
|
|
+ for _, asset := range as.assets {
|
|
|
+ err := acc.Insert(asset)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, asset := range that.assets {
|
|
|
+ err := acc.Insert(asset)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return acc, nil
|
|
|
+}
|
|
|
+
|
|
|
+type AssetSetRange struct {
|
|
|
+ sync.RWMutex
|
|
|
+ assets []*AssetSet
|
|
|
+}
|
|
|
+
|
|
|
+func NewAssetSetRange(assets ...*AssetSet) *AssetSetRange {
|
|
|
+ return &AssetSetRange{
|
|
|
+ assets: assets,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Accumulate sums each AssetSet in the given range, returning a single cumulative
|
|
|
+// AssetSet for the entire range.
|
|
|
+func (asr *AssetSetRange) Accumulate() (*AssetSet, error) {
|
|
|
+ var assetSet *AssetSet
|
|
|
+ var err error
|
|
|
+
|
|
|
+ asr.RLock()
|
|
|
+ defer asr.RUnlock()
|
|
|
+
|
|
|
+ for _, as := range asr.assets {
|
|
|
+ assetSet, err = assetSet.accumulate(as)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return assetSet, nil
|
|
|
+}
|
|
|
+
|
|
|
+type AssetAggregationOptions struct {
|
|
|
+ SharedHourlyCosts map[string]float64
|
|
|
+ FilterFuncs []AssetMatchFunc
|
|
|
+}
|
|
|
+
|
|
|
+func (asr *AssetSetRange) AggregateBy(props []AssetProperty, opts *AssetAggregationOptions) error {
|
|
|
+ aggRange := &AssetSetRange{assets: []*AssetSet{}}
|
|
|
+
|
|
|
+ asr.Lock()
|
|
|
+ defer asr.Unlock()
|
|
|
+
|
|
|
+ for _, as := range asr.assets {
|
|
|
+ err := as.AggregateBy(props, opts)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ aggRange.assets = append(aggRange.assets, as)
|
|
|
+ }
|
|
|
+
|
|
|
+ asr.assets = aggRange.assets
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (asr *AssetSetRange) Append(that *AssetSet) {
|
|
|
+ asr.Lock()
|
|
|
+ defer asr.Unlock()
|
|
|
+ asr.assets = append(asr.assets, that)
|
|
|
+}
|
|
|
+
|
|
|
+// Each invokes the given function for each AssetSet in the range
|
|
|
+func (asr *AssetSetRange) Each(f func(int, *AssetSet)) {
|
|
|
+ if asr == nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ for i, as := range asr.assets {
|
|
|
+ f(i, as)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (asr *AssetSetRange) Get(i int) (*AssetSet, error) {
|
|
|
+ if i < 0 || i >= len(asr.assets) {
|
|
|
+ return nil, fmt.Errorf("AssetSetRange: index out of range: %d", i)
|
|
|
+ }
|
|
|
+
|
|
|
+ asr.RLock()
|
|
|
+ defer asr.RUnlock()
|
|
|
+ return asr.assets[i], nil
|
|
|
+}
|
|
|
+
|
|
|
+func (asr *AssetSetRange) Length() int {
|
|
|
+ if asr == nil || asr.assets == nil {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ asr.RLock()
|
|
|
+ defer asr.RUnlock()
|
|
|
+ return len(asr.assets)
|
|
|
+}
|
|
|
+
|
|
|
+func (asr *AssetSetRange) MarshalJSON() ([]byte, error) {
|
|
|
+ asr.RLock()
|
|
|
+ asr.RUnlock()
|
|
|
+ return json.Marshal(asr.assets)
|
|
|
+}
|
|
|
+
|
|
|
+func (asr *AssetSetRange) UTCOffset() time.Duration {
|
|
|
+ if asr.Length() == 0 {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ as, err := asr.Get(0)
|
|
|
+ if err != nil {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return as.UTCOffset()
|
|
|
+}
|
|
|
+
|
|
|
+// Window returns the full window that the AssetSetRange spans, from the
|
|
|
+// start of the first AssetSet to the end of the last one.
|
|
|
+func (asr *AssetSetRange) Window() Window {
|
|
|
+ if asr == nil || asr.Length() == 0 {
|
|
|
+ return NewWindow(nil, nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ start := asr.assets[0].Start()
|
|
|
+ end := asr.assets[asr.Length()-1].End()
|
|
|
+
|
|
|
+ return NewWindow(&start, &end)
|
|
|
+}
|
|
|
+
|
|
|
+// TODO move everything below to a separate package
|
|
|
+
|
|
|
+func jsonEncodeFloat64(buffer *bytes.Buffer, name string, val float64, comma string) {
|
|
|
+ var encoding string
|
|
|
+ if math.IsNaN(val) {
|
|
|
+ encoding = fmt.Sprintf("\"%s\":null%s", name, comma)
|
|
|
+ } else {
|
|
|
+ encoding = fmt.Sprintf("\"%s\":%f%s", name, val, comma)
|
|
|
+ }
|
|
|
+
|
|
|
+ buffer.WriteString(encoding)
|
|
|
+}
|
|
|
+
|
|
|
+func jsonEncodeString(buffer *bytes.Buffer, name, val, comma string) {
|
|
|
+ buffer.WriteString(fmt.Sprintf("\"%s\":\"%s\"%s", name, val, comma))
|
|
|
+}
|
|
|
+
|
|
|
+func jsonEncode(buffer *bytes.Buffer, name string, obj interface{}, comma string) {
|
|
|
+ buffer.WriteString(fmt.Sprintf("\"%s\":", name))
|
|
|
+ if bytes, err := json.Marshal(obj); err != nil {
|
|
|
+ buffer.WriteString("null")
|
|
|
+ } else {
|
|
|
+ buffer.Write(bytes)
|
|
|
+ }
|
|
|
+ buffer.WriteString(comma)
|
|
|
+}
|