asset.go 77 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078
  1. package kubecost
  2. import (
  3. "bytes"
  4. "encoding"
  5. "fmt"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/kubecost/cost-model/pkg/log"
  10. "github.com/kubecost/cost-model/pkg/util/json"
  11. )
  12. // UndefinedKey is used in composing Asset group keys if the group does not have that property defined.
  13. // E.g. if aggregating on Cluster, Assets in the AssetSet where Asset has no cluster will be grouped under key "__undefined__"
  14. const UndefinedKey = "__undefined__"
  15. // Asset defines an entity within a cluster that has a defined cost over a
  16. // given period of time.
  17. type Asset interface {
  18. // Type identifies the kind of Asset, which must always exist and should
  19. // be defined by the underlying type implementing the interface.
  20. Type() AssetType
  21. // Properties are a map of predefined traits, which may or may not exist,
  22. // but must conform to the AssetProperty schema
  23. Properties() *AssetProperties
  24. SetProperties(*AssetProperties)
  25. // Labels are a map of undefined string-to-string values
  26. Labels() AssetLabels
  27. SetLabels(AssetLabels)
  28. // Monetary values
  29. Adjustment() float64
  30. SetAdjustment(float64)
  31. TotalCost() float64
  32. // Temporal values
  33. Start() time.Time
  34. End() time.Time
  35. SetStartEnd(time.Time, time.Time)
  36. Window() Window
  37. ExpandWindow(Window)
  38. Minutes() float64
  39. // Operations and comparisons
  40. Add(Asset) Asset
  41. Clone() Asset
  42. Equal(Asset) bool
  43. // Representations
  44. encoding.BinaryMarshaler
  45. encoding.BinaryUnmarshaler
  46. json.Marshaler
  47. fmt.Stringer
  48. }
  49. // AssetToExternalAllocation converts the given asset to an Allocation, given
  50. // the properties to use to aggregate, and the mapping from Allocation property
  51. // to Asset label. For example, consider this asset:
  52. //
  53. // CURRENT: Asset ETL stores its data ALREADY MAPPED from label to k8s concept. This isn't ideal-- see the TOOD.
  54. // Cloud {
  55. // TotalCost: 10.00,
  56. // Labels{
  57. // "kubernetes_namespace":"monitoring",
  58. // "env":"prod"
  59. // }
  60. // }
  61. //
  62. // Given the following parameters, we expect to return:
  63. //
  64. // 1) single-prop full match
  65. // aggregateBy = ["namespace"]
  66. // => Allocation{Name: "monitoring", ExternalCost: 10.00, TotalCost: 10.00}, nil
  67. //
  68. // 2) multi-prop full match
  69. // aggregateBy = ["namespace", "label:env"]
  70. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  71. // => Allocation{Name: "monitoring/env=prod", ExternalCost: 10.00, TotalCost: 10.00}, nil
  72. //
  73. // 3) multi-prop partial match
  74. // aggregateBy = ["namespace", "label:foo"]
  75. // => Allocation{Name: "monitoring/__unallocated__", ExternalCost: 10.00, TotalCost: 10.00}, nil
  76. //
  77. // 4) no match
  78. // aggregateBy = ["cluster"]
  79. // => nil, err
  80. //
  81. // TODO:
  82. // Cloud {
  83. // TotalCost: 10.00,
  84. // Labels{
  85. // "kubernetes_namespace":"monitoring",
  86. // "env":"prod"
  87. // }
  88. // }
  89. //
  90. // Given the following parameters, we expect to return:
  91. //
  92. // 1) single-prop full match
  93. // aggregateBy = ["namespace"]
  94. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  95. // => Allocation{Name: "monitoring", ExternalCost: 10.00, TotalCost: 10.00}, nil
  96. //
  97. // 2) multi-prop full match
  98. // aggregateBy = ["namespace", "label:env"]
  99. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  100. // => Allocation{Name: "monitoring/env=prod", ExternalCost: 10.00, TotalCost: 10.00}, nil
  101. //
  102. // 3) multi-prop partial match
  103. // aggregateBy = ["namespace", "label:foo"]
  104. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  105. // => Allocation{Name: "monitoring/__unallocated__", ExternalCost: 10.00, TotalCost: 10.00}, nil
  106. //
  107. // 4) no match
  108. // aggregateBy = ["cluster"]
  109. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  110. // => nil, err
  111. //
  112. // (See asset_test.go for assertions of these examples and more.)
  113. func AssetToExternalAllocation(asset Asset, aggregateBy []string, externalLabelsCfg map[string]string) (*Allocation, error) {
  114. if asset == nil {
  115. return nil, fmt.Errorf("asset is nil")
  116. }
  117. // names will collect the slash-separated names accrued by iterating over
  118. // aggregateBy and checking the relevant labels.
  119. names := []string{}
  120. // match records whether or not a match was found in the Asset labels,
  121. // such that is can genuinely be turned into an external Allocation.
  122. match := false
  123. // props records the relevant Properties to set on the resultant Allocation
  124. props := AllocationProperties{}
  125. for _, aggBy := range aggregateBy {
  126. // labelName should be derived from the mapping of properties to
  127. // label names, unless the aggBy is explicitly a label, in which
  128. // case we should pull the label name from the aggBy string.
  129. // Unless this matches a special aggregation, as we have that mapping already transformed...
  130. labelName := aggBy
  131. agglName := aggBy
  132. if strings.HasPrefix(aggBy, "label:") {
  133. labelName = strings.TrimPrefix(aggBy, "label:")
  134. agglName = labelName
  135. if v, ok := externalLabelsCfg[labelName]; ok {
  136. agglName = v
  137. }
  138. }
  139. if labelName == "" {
  140. // No matching label has been defined in the cost-analyzer label config
  141. // relating to the given aggregateBy property.
  142. names = append(names, UnallocatedSuffix)
  143. continue
  144. }
  145. if value := asset.Labels()[agglName]; value != "" {
  146. // Valid label value was found for one of the aggregation properties,
  147. // so add it to the name.
  148. if strings.HasPrefix(aggBy, "label:") {
  149. // Use naming convention labelName=labelValue for labels
  150. // e.g. aggBy="label:env", value="prod" => "env=prod"
  151. names = append(names, fmt.Sprintf("%s=%s", strings.TrimPrefix(aggBy, "label:"), value))
  152. match = true
  153. // Set the corresponding label in props
  154. labels := props.Labels
  155. if labels == nil {
  156. labels = map[string]string{}
  157. }
  158. labels[labelName] = value
  159. props.Labels = labels
  160. } else {
  161. names = append(names, value)
  162. match = true
  163. // Set the corresponding property on props
  164. switch aggBy {
  165. case AllocationClusterProp:
  166. props.Cluster = value
  167. case AllocationNodeProp:
  168. props.Node = value
  169. case AllocationNamespaceProp:
  170. props.Namespace = value
  171. case AllocationControllerKindProp:
  172. props.ControllerKind = value
  173. case AllocationControllerProp:
  174. props.Controller = value
  175. case AllocationPodProp:
  176. props.Pod = value
  177. case AllocationContainerProp:
  178. props.Container = value
  179. case AllocationServiceProp:
  180. // TODO: external allocation: how to do this? multi-service?
  181. props.Services = []string{value}
  182. }
  183. }
  184. } else {
  185. // No value label value was found on the Asset; consider it
  186. // unallocated. Note that this case is only truly relevant if at
  187. // least one other property matches (e.g. case 3 in the examples)
  188. // because if there are no matches, then an error is returned.
  189. names = append(names, UnallocatedSuffix)
  190. }
  191. }
  192. if !match {
  193. return nil, fmt.Errorf("asset does not qualify as an external allocation")
  194. }
  195. names = append(names, ExternalSuffix)
  196. // TODO: external allocation: efficiency?
  197. // TODO: external allocation: resource totals?
  198. return &Allocation{
  199. Name: strings.Join(names, "/"),
  200. Properties: &props,
  201. Window: asset.Window().Clone(),
  202. Start: asset.Start(),
  203. End: asset.End(),
  204. ExternalCost: asset.TotalCost(),
  205. }, nil
  206. }
  207. // key is used to determine uniqueness of an Asset, for instance during Insert
  208. // to determine if two Assets should be combined. Passing `nil` `aggregateBy` indicates
  209. // that all available `AssetProperty` keys should be used. Passing empty `aggregateBy` indicates that
  210. // no key should be used (e.g. to aggregate all assets). Passing one or more `aggregateBy`
  211. // values will key by only those values.
  212. // Valid values of `aggregateBy` elements are strings which are an `AssetProperty`, and strings prefixed
  213. // with `"label:"`.
  214. func key(a Asset, aggregateBy []string) (string, error) {
  215. keys := []string{}
  216. if aggregateBy == nil {
  217. aggregateBy = []string{
  218. string(AssetProviderProp),
  219. string(AssetAccountProp),
  220. string(AssetProjectProp),
  221. string(AssetCategoryProp),
  222. string(AssetClusterProp),
  223. string(AssetTypeProp),
  224. string(AssetServiceProp),
  225. string(AssetProviderIDProp),
  226. string(AssetNameProp),
  227. }
  228. }
  229. for _, s := range aggregateBy {
  230. key := ""
  231. switch true {
  232. case s == string(AssetProviderProp):
  233. key = a.Properties().Provider
  234. case s == string(AssetAccountProp):
  235. key = a.Properties().Account
  236. case s == string(AssetProjectProp):
  237. key = a.Properties().Project
  238. case s == string(AssetClusterProp):
  239. key = a.Properties().Cluster
  240. case s == string(AssetCategoryProp):
  241. key = a.Properties().Category
  242. case s == string(AssetTypeProp):
  243. key = a.Type().String()
  244. case s == string(AssetServiceProp):
  245. key = a.Properties().Service
  246. case s == string(AssetProviderIDProp):
  247. key = a.Properties().ProviderID
  248. case s == string(AssetNameProp):
  249. key = a.Properties().Name
  250. case strings.HasPrefix(s, "label:"):
  251. if labelKey := strings.TrimPrefix(s, "label:"); labelKey != "" {
  252. labelVal := a.Labels()[labelKey]
  253. if labelVal == "" {
  254. key = "__undefined__"
  255. } else {
  256. key = fmt.Sprintf("%s=%s", labelKey, labelVal)
  257. }
  258. } else {
  259. // Don't allow aggregating on label ""
  260. return "", fmt.Errorf("attempted to aggregate on invalid key: %s", s)
  261. }
  262. default:
  263. return "", fmt.Errorf("attempted to aggregate on invalid key: %s", s)
  264. }
  265. if key != "" {
  266. keys = append(keys, key)
  267. } else {
  268. keys = append(keys, UndefinedKey)
  269. }
  270. }
  271. return strings.Join(keys, "/"), nil
  272. }
  273. func toString(a Asset) string {
  274. return fmt.Sprintf("%s{%s}%s=%.2f", a.Type().String(), a.Properties(), a.Window(), a.TotalCost())
  275. }
  276. // AssetLabels is a schema-free mapping of key/value pairs that can be
  277. // attributed to an Asset as a flexible a
  278. type AssetLabels map[string]string
  279. // Clone returns a cloned map of labels
  280. func (al AssetLabels) Clone() AssetLabels {
  281. clone := make(AssetLabels, len(al))
  282. for label, value := range al {
  283. clone[label] = value
  284. }
  285. return clone
  286. }
  287. // Equal returns true only if the two set of labels are exact matches
  288. func (al AssetLabels) Equal(that AssetLabels) bool {
  289. if len(al) != len(that) {
  290. return false
  291. }
  292. for label, value := range al {
  293. if thatValue, ok := that[label]; !ok || thatValue != value {
  294. return false
  295. }
  296. }
  297. return true
  298. }
  299. // Merge retains only the labels shared with the given AssetLabels
  300. func (al AssetLabels) Merge(that AssetLabels) AssetLabels {
  301. result := AssetLabels{}
  302. for label, value := range al {
  303. if thatValue, ok := that[label]; ok && thatValue == value {
  304. result[label] = value
  305. }
  306. }
  307. return result
  308. }
  309. // AssetMatchFunc is a function that can be used to match Assets by
  310. // returning true for any given Asset if a condition is met.
  311. type AssetMatchFunc func(Asset) bool
  312. // AssetType identifies a type of Asset
  313. type AssetType int
  314. const (
  315. // AnyAssetType describes the Any AssetType
  316. AnyAssetType AssetType = iota
  317. // CloudAssetType describes the Cloud AssetType
  318. CloudAssetType
  319. // ClusterManagementAssetType describes the ClusterManagement AssetType
  320. ClusterManagementAssetType
  321. // DiskAssetType describes the Disk AssetType
  322. DiskAssetType
  323. // LoadBalancerAssetType describes the LoadBalancer AssetType
  324. LoadBalancerAssetType
  325. // NetworkAssetType describes the Network AssetType
  326. NetworkAssetType
  327. // NodeAssetType describes the Node AssetType
  328. NodeAssetType
  329. // SharedAssetType describes the Shared AssetType
  330. SharedAssetType
  331. )
  332. // ParseAssetType attempts to parse the given string into an AssetType
  333. func ParseAssetType(text string) (AssetType, error) {
  334. switch strings.TrimSpace(strings.ToLower(text)) {
  335. case "cloud":
  336. return CloudAssetType, nil
  337. case "clustermanagement":
  338. return ClusterManagementAssetType, nil
  339. case "disk":
  340. return DiskAssetType, nil
  341. case "loadbalancer":
  342. return LoadBalancerAssetType, nil
  343. case "network":
  344. return NetworkAssetType, nil
  345. case "node":
  346. return NodeAssetType, nil
  347. case "shared":
  348. return SharedAssetType, nil
  349. }
  350. return AnyAssetType, fmt.Errorf("invalid asset type: %s", text)
  351. }
  352. // String converts the given AssetType to a string
  353. func (at AssetType) String() string {
  354. return [...]string{
  355. "Asset",
  356. "Cloud",
  357. "ClusterManagement",
  358. "Disk",
  359. "LoadBalancer",
  360. "Network",
  361. "Node",
  362. "Shared",
  363. }[at]
  364. }
  365. // Any is the most general Asset, which is usually created as a result of
  366. // adding two Assets of different types.
  367. type Any struct {
  368. labels AssetLabels
  369. properties *AssetProperties
  370. start time.Time
  371. end time.Time
  372. window Window
  373. adjustment float64
  374. Cost float64
  375. }
  376. // NewAsset creates a new Any-type Asset for the given period of time
  377. func NewAsset(start, end time.Time, window Window) *Any {
  378. return &Any{
  379. labels: AssetLabels{},
  380. properties: &AssetProperties{},
  381. start: start,
  382. end: end,
  383. window: window.Clone(),
  384. }
  385. }
  386. // Type returns the Asset's type
  387. func (a *Any) Type() AssetType {
  388. return AnyAssetType
  389. }
  390. // Properties returns the Asset's properties
  391. func (a *Any) Properties() *AssetProperties {
  392. return a.properties
  393. }
  394. // SetProperties sets the Asset's properties
  395. func (a *Any) SetProperties(props *AssetProperties) {
  396. a.properties = props
  397. }
  398. // Labels returns the Asset's labels
  399. func (a *Any) Labels() AssetLabels {
  400. return a.labels
  401. }
  402. // SetLabels sets the Asset's labels
  403. func (a *Any) SetLabels(labels AssetLabels) {
  404. a.labels = labels
  405. }
  406. // Adjustment returns the Asset's cost adjustment
  407. func (a *Any) Adjustment() float64 {
  408. return a.adjustment
  409. }
  410. // SetAdjustment sets the Asset's cost adjustment
  411. func (a *Any) SetAdjustment(adj float64) {
  412. a.adjustment = adj
  413. }
  414. // TotalCost returns the Asset's TotalCost
  415. func (a *Any) TotalCost() float64 {
  416. return a.Cost + a.adjustment
  417. }
  418. // Start returns the Asset's start time within the window
  419. func (a *Any) Start() time.Time {
  420. return a.start
  421. }
  422. // End returns the Asset's end time within the window
  423. func (a *Any) End() time.Time {
  424. return a.end
  425. }
  426. // Minutes returns the number of minutes the Asset was active within the window
  427. func (a *Any) Minutes() float64 {
  428. return a.End().Sub(a.Start()).Minutes()
  429. }
  430. // Window returns the Asset's window
  431. func (a *Any) Window() Window {
  432. return a.window
  433. }
  434. // ExpandWindow expands the Asset's window by the given window
  435. func (a *Any) ExpandWindow(window Window) {
  436. a.window = a.window.Expand(window)
  437. }
  438. // SetStartEnd sets the Asset's Start and End fields
  439. func (a *Any) SetStartEnd(start, end time.Time) {
  440. if a.Window().Contains(start) {
  441. a.start = start
  442. } else {
  443. log.Warningf("Any.SetStartEnd: start %s not in %s", start, a.Window())
  444. }
  445. if a.Window().Contains(end) {
  446. a.end = end
  447. } else {
  448. log.Warningf("Any.SetStartEnd: end %s not in %s", end, a.Window())
  449. }
  450. }
  451. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  452. // as much relevant information as possible (i.e. type, properties, labels).
  453. func (a *Any) Add(that Asset) Asset {
  454. this := a.Clone().(*Any)
  455. props := a.Properties().Merge(that.Properties())
  456. labels := a.Labels().Merge(that.Labels())
  457. start := a.Start()
  458. if that.Start().Before(start) {
  459. start = that.Start()
  460. }
  461. end := a.End()
  462. if that.End().After(end) {
  463. end = that.End()
  464. }
  465. window := a.Window().Expand(that.Window())
  466. this.start = start
  467. this.end = end
  468. this.window = window
  469. this.SetProperties(props)
  470. this.SetLabels(labels)
  471. this.adjustment += that.Adjustment()
  472. this.Cost += (that.TotalCost() - that.Adjustment())
  473. return this
  474. }
  475. // Clone returns a cloned instance of the Asset
  476. func (a *Any) Clone() Asset {
  477. return &Any{
  478. labels: a.labels.Clone(),
  479. properties: a.properties.Clone(),
  480. start: a.start,
  481. end: a.end,
  482. window: a.window.Clone(),
  483. adjustment: a.adjustment,
  484. Cost: a.Cost,
  485. }
  486. }
  487. // Equal returns true if the given Asset is an exact match of the receiver
  488. func (a *Any) Equal(that Asset) bool {
  489. t, ok := that.(*Any)
  490. if !ok {
  491. return false
  492. }
  493. if !a.Labels().Equal(that.Labels()) {
  494. return false
  495. }
  496. if !a.Properties().Equal(that.Properties()) {
  497. return false
  498. }
  499. if !a.start.Equal(t.start) {
  500. return false
  501. }
  502. if !a.end.Equal(t.end) {
  503. return false
  504. }
  505. if !a.window.Equal(t.window) {
  506. return false
  507. }
  508. if a.Cost != t.Cost {
  509. return false
  510. }
  511. return true
  512. }
  513. // MarshalJSON implements json.Marshaler
  514. func (a *Any) MarshalJSON() ([]byte, error) {
  515. buffer := bytes.NewBufferString("{")
  516. jsonEncode(buffer, "properties", a.Properties(), ",")
  517. jsonEncode(buffer, "labels", a.Labels(), ",")
  518. jsonEncode(buffer, "window", a.Window(), ",")
  519. jsonEncodeString(buffer, "start", a.Start().Format(time.RFC3339), ",")
  520. jsonEncodeString(buffer, "end", a.End().Format(time.RFC3339), ",")
  521. jsonEncodeFloat64(buffer, "minutes", a.Minutes(), ",")
  522. jsonEncodeFloat64(buffer, "adjustment", a.Adjustment(), ",")
  523. jsonEncodeFloat64(buffer, "totalCost", a.TotalCost(), "")
  524. buffer.WriteString("}")
  525. return buffer.Bytes(), nil
  526. }
  527. // String implements fmt.Stringer
  528. func (a *Any) String() string {
  529. return toString(a)
  530. }
  531. // Cloud describes a cloud asset
  532. type Cloud struct {
  533. labels AssetLabels
  534. properties *AssetProperties
  535. start time.Time
  536. end time.Time
  537. window Window
  538. adjustment float64
  539. Cost float64
  540. Credit float64 // Credit is a negative value representing dollars credited back to a given line-item
  541. }
  542. // NewCloud returns a new Cloud Asset
  543. func NewCloud(category, providerID string, start, end time.Time, window Window) *Cloud {
  544. properties := &AssetProperties{
  545. Category: category,
  546. ProviderID: providerID,
  547. }
  548. return &Cloud{
  549. labels: AssetLabels{},
  550. properties: properties,
  551. start: start,
  552. end: end,
  553. window: window.Clone(),
  554. }
  555. }
  556. // Type returns the AssetType
  557. func (ca *Cloud) Type() AssetType {
  558. return CloudAssetType
  559. }
  560. // Properties returns the AssetProperties
  561. func (ca *Cloud) Properties() *AssetProperties {
  562. return ca.properties
  563. }
  564. // SetProperties sets the Asset's properties
  565. func (ca *Cloud) SetProperties(props *AssetProperties) {
  566. ca.properties = props
  567. }
  568. // Labels returns the AssetLabels
  569. func (ca *Cloud) Labels() AssetLabels {
  570. return ca.labels
  571. }
  572. // SetLabels sets the Asset's labels
  573. func (ca *Cloud) SetLabels(labels AssetLabels) {
  574. ca.labels = labels
  575. }
  576. // Adjustment returns the Asset's adjustment value
  577. func (ca *Cloud) Adjustment() float64 {
  578. return ca.adjustment
  579. }
  580. // SetAdjustment sets the Asset's adjustment value
  581. func (ca *Cloud) SetAdjustment(adj float64) {
  582. ca.adjustment = adj
  583. }
  584. // TotalCost returns the Asset's total cost
  585. func (ca *Cloud) TotalCost() float64 {
  586. return ca.Cost + ca.adjustment + ca.Credit
  587. }
  588. // Start returns the Asset's precise start time within the window
  589. func (ca *Cloud) Start() time.Time {
  590. return ca.start
  591. }
  592. // End returns the Asset's precise end time within the window
  593. func (ca *Cloud) End() time.Time {
  594. return ca.end
  595. }
  596. // Minutes returns the number of Minutes the Asset ran
  597. func (ca *Cloud) Minutes() float64 {
  598. return ca.End().Sub(ca.Start()).Minutes()
  599. }
  600. // Window returns the window within which the Asset ran
  601. func (ca *Cloud) Window() Window {
  602. return ca.window
  603. }
  604. // ExpandWindow expands the Asset's window by the given window
  605. func (ca *Cloud) ExpandWindow(window Window) {
  606. ca.window = ca.window.Expand(window)
  607. }
  608. // SetStartEnd sets the Asset's Start and End fields
  609. func (ca *Cloud) SetStartEnd(start, end time.Time) {
  610. if ca.Window().Contains(start) {
  611. ca.start = start
  612. } else {
  613. log.Warningf("Cloud.SetStartEnd: start %s not in %s", start, ca.Window())
  614. }
  615. if ca.Window().Contains(end) {
  616. ca.end = end
  617. } else {
  618. log.Warningf("Cloud.SetStartEnd: end %s not in %s", end, ca.Window())
  619. }
  620. }
  621. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  622. // as much relevant information as possible (i.e. type, properties, labels).
  623. func (ca *Cloud) Add(a Asset) Asset {
  624. // Cloud + Cloud = Cloud
  625. if that, ok := a.(*Cloud); ok {
  626. this := ca.Clone().(*Cloud)
  627. this.add(that)
  628. return this
  629. }
  630. props := ca.Properties().Merge(a.Properties())
  631. labels := ca.Labels().Merge(a.Labels())
  632. start := ca.Start()
  633. if a.Start().Before(start) {
  634. start = a.Start()
  635. }
  636. end := ca.End()
  637. if a.End().After(end) {
  638. end = a.End()
  639. }
  640. window := ca.Window().Expand(a.Window())
  641. // Cloud + !Cloud = Any
  642. any := NewAsset(start, end, window)
  643. any.SetProperties(props)
  644. any.SetLabels(labels)
  645. any.adjustment = ca.Adjustment() + a.Adjustment()
  646. any.Cost = (ca.TotalCost() - ca.Adjustment() - ca.Credit) + (a.TotalCost() - a.Adjustment() - ca.Credit)
  647. return any
  648. }
  649. func (ca *Cloud) add(that *Cloud) {
  650. if ca == nil {
  651. ca = that
  652. return
  653. }
  654. props := ca.Properties().Merge(that.Properties())
  655. labels := ca.Labels().Merge(that.Labels())
  656. start := ca.Start()
  657. if that.Start().Before(start) {
  658. start = that.Start()
  659. }
  660. end := ca.End()
  661. if that.End().After(end) {
  662. end = that.End()
  663. }
  664. window := ca.Window().Expand(that.Window())
  665. ca.start = start
  666. ca.end = end
  667. ca.window = window
  668. ca.SetProperties(props)
  669. ca.SetLabels(labels)
  670. ca.adjustment += that.adjustment
  671. ca.Cost += that.Cost
  672. ca.Credit += that.Credit
  673. }
  674. // Clone returns a cloned instance of the Asset
  675. func (ca *Cloud) Clone() Asset {
  676. return &Cloud{
  677. labels: ca.labels.Clone(),
  678. properties: ca.properties.Clone(),
  679. start: ca.start,
  680. end: ca.end,
  681. window: ca.window.Clone(),
  682. adjustment: ca.adjustment,
  683. Cost: ca.Cost,
  684. Credit: ca.Credit,
  685. }
  686. }
  687. // Equal returns true if the given Asset precisely equals the Asset
  688. func (ca *Cloud) Equal(a Asset) bool {
  689. that, ok := a.(*Cloud)
  690. if !ok {
  691. return false
  692. }
  693. if !ca.Labels().Equal(that.Labels()) {
  694. return false
  695. }
  696. if !ca.Properties().Equal(that.Properties()) {
  697. return false
  698. }
  699. if !ca.start.Equal(that.start) {
  700. return false
  701. }
  702. if !ca.end.Equal(that.end) {
  703. return false
  704. }
  705. if !ca.window.Equal(that.window) {
  706. return false
  707. }
  708. if ca.adjustment != that.adjustment {
  709. return false
  710. }
  711. if ca.Cost != that.Cost {
  712. return false
  713. }
  714. if ca.Credit != that.Credit {
  715. return false
  716. }
  717. return true
  718. }
  719. // MarshalJSON implements json.Marshaler
  720. func (ca *Cloud) MarshalJSON() ([]byte, error) {
  721. buffer := bytes.NewBufferString("{")
  722. jsonEncodeString(buffer, "type", ca.Type().String(), ",")
  723. jsonEncode(buffer, "properties", ca.Properties(), ",")
  724. jsonEncode(buffer, "labels", ca.Labels(), ",")
  725. jsonEncode(buffer, "window", ca.Window(), ",")
  726. jsonEncodeString(buffer, "start", ca.Start().Format(time.RFC3339), ",")
  727. jsonEncodeString(buffer, "end", ca.End().Format(time.RFC3339), ",")
  728. jsonEncodeFloat64(buffer, "minutes", ca.Minutes(), ",")
  729. jsonEncodeFloat64(buffer, "adjustment", ca.Adjustment(), ",")
  730. jsonEncodeFloat64(buffer, "credit", ca.Credit, ",")
  731. jsonEncodeFloat64(buffer, "totalCost", ca.TotalCost(), "")
  732. buffer.WriteString("}")
  733. return buffer.Bytes(), nil
  734. }
  735. // String implements fmt.Stringer
  736. func (ca *Cloud) String() string {
  737. return toString(ca)
  738. }
  739. // ClusterManagement describes a provider's cluster management fee
  740. type ClusterManagement struct {
  741. labels AssetLabels
  742. properties *AssetProperties
  743. window Window
  744. Cost float64
  745. }
  746. // NewClusterManagement creates and returns a new ClusterManagement instance
  747. func NewClusterManagement(provider, cluster string, window Window) *ClusterManagement {
  748. properties := &AssetProperties{
  749. Category: ManagementCategory,
  750. Provider: ParseProvider(provider),
  751. Cluster: cluster,
  752. Service: KubernetesService,
  753. }
  754. return &ClusterManagement{
  755. labels: AssetLabels{},
  756. properties: properties,
  757. window: window.Clone(),
  758. }
  759. }
  760. // Type returns the Asset's type
  761. func (cm *ClusterManagement) Type() AssetType {
  762. return ClusterManagementAssetType
  763. }
  764. // Properties returns the Asset's properties
  765. func (cm *ClusterManagement) Properties() *AssetProperties {
  766. return cm.properties
  767. }
  768. // SetProperties sets the Asset's properties
  769. func (cm *ClusterManagement) SetProperties(props *AssetProperties) {
  770. cm.properties = props
  771. }
  772. // Labels returns the Asset's labels
  773. func (cm *ClusterManagement) Labels() AssetLabels {
  774. return cm.labels
  775. }
  776. // SetLabels sets the Asset's properties
  777. func (cm *ClusterManagement) SetLabels(props AssetLabels) {
  778. cm.labels = props
  779. }
  780. // Adjustment does not apply to ClusterManagement
  781. func (cm *ClusterManagement) Adjustment() float64 {
  782. return 0.0
  783. }
  784. // SetAdjustment does not apply to ClusterManagement
  785. func (cm *ClusterManagement) SetAdjustment(float64) {
  786. return
  787. }
  788. // TotalCost returns the Asset's total cost
  789. func (cm *ClusterManagement) TotalCost() float64 {
  790. return cm.Cost
  791. }
  792. // Start returns the Asset's precise start time within the window
  793. func (cm *ClusterManagement) Start() time.Time {
  794. return *cm.window.Start()
  795. }
  796. // End returns the Asset's precise end time within the window
  797. func (cm *ClusterManagement) End() time.Time {
  798. return *cm.window.End()
  799. }
  800. // Minutes returns the number of minutes the Asset ran
  801. func (cm *ClusterManagement) Minutes() float64 {
  802. return cm.Window().Minutes()
  803. }
  804. // Window return the Asset's window
  805. func (cm *ClusterManagement) Window() Window {
  806. return cm.window
  807. }
  808. // ExpandWindow expands the Asset's window by the given window
  809. func (cm *ClusterManagement) ExpandWindow(window Window) {
  810. cm.window = cm.window.Expand(window)
  811. }
  812. // SetStartEnd sets the Asset's Start and End fields (not applicable here)
  813. func (cm *ClusterManagement) SetStartEnd(start, end time.Time) {
  814. return
  815. }
  816. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  817. // as much relevant information as possible (i.e. type, properties, labels).
  818. func (cm *ClusterManagement) Add(a Asset) Asset {
  819. // ClusterManagement + ClusterManagement = ClusterManagement
  820. if that, ok := a.(*ClusterManagement); ok {
  821. this := cm.Clone().(*ClusterManagement)
  822. this.add(that)
  823. return this
  824. }
  825. props := cm.Properties().Merge(a.Properties())
  826. labels := cm.Labels().Merge(a.Labels())
  827. start := cm.Start()
  828. if a.Start().Before(start) {
  829. start = a.Start()
  830. }
  831. end := cm.End()
  832. if a.End().After(end) {
  833. end = a.End()
  834. }
  835. window := cm.Window().Expand(a.Window())
  836. // ClusterManagement + !ClusterManagement = Any
  837. any := NewAsset(start, end, window)
  838. any.SetProperties(props)
  839. any.SetLabels(labels)
  840. any.adjustment = cm.Adjustment() + a.Adjustment()
  841. any.Cost = (cm.TotalCost() - cm.Adjustment()) + (a.TotalCost() - a.Adjustment())
  842. return any
  843. }
  844. func (cm *ClusterManagement) add(that *ClusterManagement) {
  845. if cm == nil {
  846. cm = that
  847. return
  848. }
  849. props := cm.Properties().Merge(that.Properties())
  850. labels := cm.Labels().Merge(that.Labels())
  851. window := cm.Window().Expand(that.Window())
  852. cm.window = window
  853. cm.SetProperties(props)
  854. cm.SetLabels(labels)
  855. cm.Cost += that.Cost
  856. }
  857. // Clone returns a cloned instance of the Asset
  858. func (cm *ClusterManagement) Clone() Asset {
  859. return &ClusterManagement{
  860. labels: cm.labels.Clone(),
  861. properties: cm.properties.Clone(),
  862. window: cm.window.Clone(),
  863. Cost: cm.Cost,
  864. }
  865. }
  866. // Equal returns true if the given Asset exactly matches the Asset
  867. func (cm *ClusterManagement) Equal(a Asset) bool {
  868. that, ok := a.(*ClusterManagement)
  869. if !ok {
  870. return false
  871. }
  872. if !cm.Labels().Equal(that.Labels()) {
  873. return false
  874. }
  875. if !cm.Properties().Equal(that.Properties()) {
  876. return false
  877. }
  878. if !cm.window.Equal(that.window) {
  879. return false
  880. }
  881. if cm.Cost != that.Cost {
  882. return false
  883. }
  884. return true
  885. }
  886. // MarshalJSON implements json.Marshler
  887. func (cm *ClusterManagement) MarshalJSON() ([]byte, error) {
  888. buffer := bytes.NewBufferString("{")
  889. jsonEncodeString(buffer, "type", cm.Type().String(), ",")
  890. jsonEncode(buffer, "properties", cm.Properties(), ",")
  891. jsonEncode(buffer, "labels", cm.Labels(), ",")
  892. jsonEncode(buffer, "window", cm.Window(), ",")
  893. jsonEncodeString(buffer, "start", cm.Start().Format(time.RFC3339), ",")
  894. jsonEncodeString(buffer, "end", cm.End().Format(time.RFC3339), ",")
  895. jsonEncodeFloat64(buffer, "minutes", cm.Minutes(), ",")
  896. jsonEncodeFloat64(buffer, "totalCost", cm.TotalCost(), "")
  897. buffer.WriteString("}")
  898. return buffer.Bytes(), nil
  899. }
  900. // String implements fmt.Stringer
  901. func (cm *ClusterManagement) String() string {
  902. return toString(cm)
  903. }
  904. // Disk represents an in-cluster disk Asset
  905. type Disk struct {
  906. labels AssetLabels
  907. properties *AssetProperties
  908. start time.Time
  909. end time.Time
  910. window Window
  911. adjustment float64
  912. Cost float64
  913. ByteHours float64
  914. Local float64
  915. Breakdown *Breakdown
  916. }
  917. // NewDisk creates and returns a new Disk Asset
  918. func NewDisk(name, cluster, providerID string, start, end time.Time, window Window) *Disk {
  919. properties := &AssetProperties{
  920. Category: StorageCategory,
  921. Name: name,
  922. Cluster: cluster,
  923. ProviderID: providerID,
  924. Service: KubernetesService,
  925. }
  926. return &Disk{
  927. labels: AssetLabels{},
  928. properties: properties,
  929. start: start,
  930. end: end,
  931. window: window,
  932. Breakdown: &Breakdown{},
  933. }
  934. }
  935. // Type returns the AssetType of the Asset
  936. func (d *Disk) Type() AssetType {
  937. return DiskAssetType
  938. }
  939. // Properties returns the Asset's properties
  940. func (d *Disk) Properties() *AssetProperties {
  941. return d.properties
  942. }
  943. // SetProperties sets the Asset's properties
  944. func (d *Disk) SetProperties(props *AssetProperties) {
  945. d.properties = props
  946. }
  947. // Labels returns the Asset's labels
  948. func (d *Disk) Labels() AssetLabels {
  949. return d.labels
  950. }
  951. // SetLabels sets the Asset's labels
  952. func (d *Disk) SetLabels(labels AssetLabels) {
  953. d.labels = labels
  954. }
  955. // Adjustment returns the Asset's cost adjustment
  956. func (d *Disk) Adjustment() float64 {
  957. return d.adjustment
  958. }
  959. // SetAdjustment sets the Asset's cost adjustment
  960. func (d *Disk) SetAdjustment(adj float64) {
  961. d.adjustment = adj
  962. }
  963. // TotalCost returns the Asset's total cost
  964. func (d *Disk) TotalCost() float64 {
  965. return d.Cost + d.adjustment
  966. }
  967. // Start returns the precise start time of the Asset within the window
  968. func (d *Disk) Start() time.Time {
  969. return d.start
  970. }
  971. // End returns the precise start time of the Asset within the window
  972. func (d *Disk) End() time.Time {
  973. return d.end
  974. }
  975. // Minutes returns the number of minutes the Asset ran
  976. func (d *Disk) Minutes() float64 {
  977. diskMins := d.end.Sub(d.start).Minutes()
  978. windowMins := d.window.Minutes()
  979. if diskMins > windowMins {
  980. log.Warningf("Asset ETL: Disk.Minutes exceeds window: %.2f > %.2f", diskMins, windowMins)
  981. diskMins = windowMins
  982. }
  983. if diskMins < 0 {
  984. diskMins = 0
  985. }
  986. return diskMins
  987. }
  988. // Window returns the window within which the Asset
  989. func (d *Disk) Window() Window {
  990. return d.window
  991. }
  992. // ExpandWindow expands the Asset's window by the given window
  993. func (d *Disk) ExpandWindow(window Window) {
  994. d.window = d.window.Expand(window)
  995. }
  996. // SetStartEnd sets the Asset's Start and End fields
  997. func (d *Disk) SetStartEnd(start, end time.Time) {
  998. if d.Window().Contains(start) {
  999. d.start = start
  1000. } else {
  1001. log.Warningf("Disk.SetStartEnd: start %s not in %s", start, d.Window())
  1002. }
  1003. if d.Window().Contains(end) {
  1004. d.end = end
  1005. } else {
  1006. log.Warningf("Disk.SetStartEnd: end %s not in %s", end, d.Window())
  1007. }
  1008. }
  1009. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  1010. // as much relevant information as possible (i.e. type, properties, labels).
  1011. func (d *Disk) Add(a Asset) Asset {
  1012. // Disk + Disk = Disk
  1013. if that, ok := a.(*Disk); ok {
  1014. this := d.Clone().(*Disk)
  1015. this.add(that)
  1016. return this
  1017. }
  1018. props := d.Properties().Merge(a.Properties())
  1019. labels := d.Labels().Merge(a.Labels())
  1020. start := d.Start()
  1021. if a.Start().Before(start) {
  1022. start = a.Start()
  1023. }
  1024. end := d.End()
  1025. if a.End().After(end) {
  1026. end = a.End()
  1027. }
  1028. window := d.Window().Expand(a.Window())
  1029. // Disk + !Disk = Any
  1030. any := NewAsset(start, end, window)
  1031. any.SetProperties(props)
  1032. any.SetLabels(labels)
  1033. any.adjustment = d.Adjustment() + a.Adjustment()
  1034. any.Cost = (d.TotalCost() - d.Adjustment()) + (a.TotalCost() - a.Adjustment())
  1035. return any
  1036. }
  1037. func (d *Disk) add(that *Disk) {
  1038. if d == nil {
  1039. d = that
  1040. return
  1041. }
  1042. props := d.Properties().Merge(that.Properties())
  1043. labels := d.Labels().Merge(that.Labels())
  1044. d.SetProperties(props)
  1045. d.SetLabels(labels)
  1046. start := d.Start()
  1047. if that.Start().Before(start) {
  1048. start = that.Start()
  1049. }
  1050. end := d.End()
  1051. if that.End().After(end) {
  1052. end = that.End()
  1053. }
  1054. window := d.Window().Expand(that.Window())
  1055. d.start = start
  1056. d.end = end
  1057. d.window = window
  1058. totalCost := d.Cost + that.Cost
  1059. if totalCost > 0.0 {
  1060. d.Breakdown.Idle = (d.Breakdown.Idle*d.Cost + that.Breakdown.Idle*that.Cost) / totalCost
  1061. d.Breakdown.Other = (d.Breakdown.Other*d.Cost + that.Breakdown.Other*that.Cost) / totalCost
  1062. d.Breakdown.System = (d.Breakdown.System*d.Cost + that.Breakdown.System*that.Cost) / totalCost
  1063. d.Breakdown.User = (d.Breakdown.User*d.Cost + that.Breakdown.User*that.Cost) / totalCost
  1064. d.Local = (d.TotalCost()*d.Local + that.TotalCost()*that.Local) / (d.TotalCost() + that.TotalCost())
  1065. } else {
  1066. d.Local = (d.Local + that.Local) / 2.0
  1067. }
  1068. d.adjustment += that.adjustment
  1069. d.Cost += that.Cost
  1070. d.ByteHours += that.ByteHours
  1071. }
  1072. // Clone returns a cloned instance of the Asset
  1073. func (d *Disk) Clone() Asset {
  1074. return &Disk{
  1075. properties: d.properties.Clone(),
  1076. labels: d.labels.Clone(),
  1077. start: d.start,
  1078. end: d.end,
  1079. window: d.window.Clone(),
  1080. adjustment: d.adjustment,
  1081. Cost: d.Cost,
  1082. ByteHours: d.ByteHours,
  1083. Local: d.Local,
  1084. Breakdown: d.Breakdown.Clone(),
  1085. }
  1086. }
  1087. // Equal returns true if the two Assets match exactly
  1088. func (d *Disk) Equal(a Asset) bool {
  1089. that, ok := a.(*Disk)
  1090. if !ok {
  1091. return false
  1092. }
  1093. if !d.Labels().Equal(that.Labels()) {
  1094. return false
  1095. }
  1096. if !d.Properties().Equal(that.Properties()) {
  1097. return false
  1098. }
  1099. if !d.Start().Equal(that.Start()) {
  1100. return false
  1101. }
  1102. if !d.End().Equal(that.End()) {
  1103. return false
  1104. }
  1105. if !d.window.Equal(that.window) {
  1106. return false
  1107. }
  1108. if d.adjustment != that.adjustment {
  1109. return false
  1110. }
  1111. if d.Cost != that.Cost {
  1112. return false
  1113. }
  1114. if d.ByteHours != that.ByteHours {
  1115. return false
  1116. }
  1117. if d.Local != that.Local {
  1118. return false
  1119. }
  1120. if !d.Breakdown.Equal(that.Breakdown) {
  1121. return false
  1122. }
  1123. return true
  1124. }
  1125. // MarshalJSON implements the json.Marshaler interface
  1126. func (d *Disk) MarshalJSON() ([]byte, error) {
  1127. buffer := bytes.NewBufferString("{")
  1128. jsonEncodeString(buffer, "type", d.Type().String(), ",")
  1129. jsonEncode(buffer, "properties", d.Properties(), ",")
  1130. jsonEncode(buffer, "labels", d.Labels(), ",")
  1131. jsonEncode(buffer, "window", d.Window(), ",")
  1132. jsonEncodeString(buffer, "start", d.Start().Format(time.RFC3339), ",")
  1133. jsonEncodeString(buffer, "end", d.End().Format(time.RFC3339), ",")
  1134. jsonEncodeFloat64(buffer, "minutes", d.Minutes(), ",")
  1135. jsonEncodeFloat64(buffer, "byteHours", d.ByteHours, ",")
  1136. jsonEncodeFloat64(buffer, "bytes", d.Bytes(), ",")
  1137. jsonEncode(buffer, "breakdown", d.Breakdown, ",")
  1138. jsonEncodeFloat64(buffer, "adjustment", d.Adjustment(), ",")
  1139. jsonEncodeFloat64(buffer, "totalCost", d.TotalCost(), "")
  1140. buffer.WriteString("}")
  1141. return buffer.Bytes(), nil
  1142. }
  1143. // String implements fmt.Stringer
  1144. func (d *Disk) String() string {
  1145. return toString(d)
  1146. }
  1147. // Bytes returns the number of bytes belonging to the disk. This could be
  1148. // fractional because it's the number of byte*hours divided by the number of
  1149. // hours running; e.g. the sum of a 100GiB disk running for the first 10 hours
  1150. // and a 30GiB disk running for the last 20 hours of the same 24-hour window
  1151. // would produce:
  1152. // (100*10 + 30*20) / 24 = 66.667GiB
  1153. // However, any number of disks running for the full span of a window will
  1154. // report the actual number of bytes of the static disk; e.g. the above
  1155. // scenario for one entire 24-hour window:
  1156. // (100*24 + 30*24) / 24 = (100 + 30) = 130GiB
  1157. func (d *Disk) Bytes() float64 {
  1158. // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
  1159. return d.ByteHours * (60.0 / d.Minutes())
  1160. }
  1161. // Breakdown describes a resource's use as a percentage of various usage types
  1162. type Breakdown struct {
  1163. Idle float64 `json:"idle"`
  1164. Other float64 `json:"other"`
  1165. System float64 `json:"system"`
  1166. User float64 `json:"user"`
  1167. }
  1168. // Clone returns a cloned instance of the Breakdown
  1169. func (b *Breakdown) Clone() *Breakdown {
  1170. if b == nil {
  1171. return nil
  1172. }
  1173. return &Breakdown{
  1174. Idle: b.Idle,
  1175. Other: b.Other,
  1176. System: b.System,
  1177. User: b.User,
  1178. }
  1179. }
  1180. // Equal returns true if the two Breakdowns are exact matches
  1181. func (b *Breakdown) Equal(that *Breakdown) bool {
  1182. if b == nil || that == nil {
  1183. return false
  1184. }
  1185. if b.Idle != that.Idle {
  1186. return false
  1187. }
  1188. if b.Other != that.Other {
  1189. return false
  1190. }
  1191. if b.System != that.System {
  1192. return false
  1193. }
  1194. if b.User != that.User {
  1195. return false
  1196. }
  1197. return true
  1198. }
  1199. // Network is an Asset representing a single node's network costs
  1200. type Network struct {
  1201. properties *AssetProperties
  1202. labels AssetLabels
  1203. start time.Time
  1204. end time.Time
  1205. window Window
  1206. adjustment float64
  1207. Cost float64
  1208. }
  1209. // NewNetwork creates and returns a new Network Asset
  1210. func NewNetwork(name, cluster, providerID string, start, end time.Time, window Window) *Network {
  1211. properties := &AssetProperties{
  1212. Category: NetworkCategory,
  1213. Name: name,
  1214. Cluster: cluster,
  1215. ProviderID: providerID,
  1216. Service: KubernetesService,
  1217. }
  1218. return &Network{
  1219. properties: properties,
  1220. labels: AssetLabels{},
  1221. start: start,
  1222. end: end,
  1223. window: window.Clone(),
  1224. }
  1225. }
  1226. // Type returns the AssetType of the Asset
  1227. func (n *Network) Type() AssetType {
  1228. return NetworkAssetType
  1229. }
  1230. // Properties returns the Asset's properties
  1231. func (n *Network) Properties() *AssetProperties {
  1232. return n.properties
  1233. }
  1234. // SetProperties sets the Asset's properties
  1235. func (n *Network) SetProperties(props *AssetProperties) {
  1236. n.properties = props
  1237. }
  1238. // Labels returns the Asset's labels
  1239. func (n *Network) Labels() AssetLabels {
  1240. return n.labels
  1241. }
  1242. // SetLabels sets the Asset's labels
  1243. func (n *Network) SetLabels(labels AssetLabels) {
  1244. n.labels = labels
  1245. }
  1246. // Adjustment returns the Asset's cost adjustment
  1247. func (n *Network) Adjustment() float64 {
  1248. return n.adjustment
  1249. }
  1250. // SetAdjustment sets the Asset's cost adjustment
  1251. func (n *Network) SetAdjustment(adj float64) {
  1252. n.adjustment = adj
  1253. }
  1254. // TotalCost returns the Asset's total cost
  1255. func (n *Network) TotalCost() float64 {
  1256. return n.Cost + n.adjustment
  1257. }
  1258. // Start returns the precise start time of the Asset within the window
  1259. func (n *Network) Start() time.Time {
  1260. return n.start
  1261. }
  1262. // End returns the precise end time of the Asset within the window
  1263. func (n *Network) End() time.Time {
  1264. return n.end
  1265. }
  1266. // Minutes returns the number of minutes the Asset ran within the window
  1267. func (n *Network) Minutes() float64 {
  1268. netMins := n.end.Sub(n.start).Minutes()
  1269. windowMins := n.window.Minutes()
  1270. if netMins > windowMins {
  1271. log.Warningf("Asset ETL: Network.Minutes exceeds window: %.2f > %.2f", netMins, windowMins)
  1272. netMins = windowMins
  1273. }
  1274. if netMins < 0 {
  1275. netMins = 0
  1276. }
  1277. return netMins
  1278. }
  1279. // Window returns the window within which the Asset ran
  1280. func (n *Network) Window() Window {
  1281. return n.window
  1282. }
  1283. // ExpandWindow expands the Asset's window by the given window
  1284. func (n *Network) ExpandWindow(window Window) {
  1285. n.window = n.window.Expand(window)
  1286. }
  1287. // SetStartEnd sets the Asset's Start and End fields
  1288. func (n *Network) SetStartEnd(start, end time.Time) {
  1289. if n.Window().Contains(start) {
  1290. n.start = start
  1291. } else {
  1292. log.Warningf("Disk.SetStartEnd: start %s not in %s", start, n.Window())
  1293. }
  1294. if n.Window().Contains(end) {
  1295. n.end = end
  1296. } else {
  1297. log.Warningf("Disk.SetStartEnd: end %s not in %s", end, n.Window())
  1298. }
  1299. }
  1300. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  1301. // as much relevant information as possible (i.e. type, properties, labels).
  1302. func (n *Network) Add(a Asset) Asset {
  1303. // Network + Network = Network
  1304. if that, ok := a.(*Network); ok {
  1305. this := n.Clone().(*Network)
  1306. this.add(that)
  1307. return this
  1308. }
  1309. props := n.Properties().Merge(a.Properties())
  1310. labels := n.Labels().Merge(a.Labels())
  1311. start := n.Start()
  1312. if a.Start().Before(start) {
  1313. start = a.Start()
  1314. }
  1315. end := n.End()
  1316. if a.End().After(end) {
  1317. end = a.End()
  1318. }
  1319. window := n.Window().Expand(a.Window())
  1320. // Network + !Network = Any
  1321. any := NewAsset(start, end, window)
  1322. any.SetProperties(props)
  1323. any.SetLabels(labels)
  1324. any.adjustment = n.Adjustment() + a.Adjustment()
  1325. any.Cost = (n.TotalCost() - n.Adjustment()) + (a.TotalCost() - a.Adjustment())
  1326. return any
  1327. }
  1328. func (n *Network) add(that *Network) {
  1329. if n == nil {
  1330. n = that
  1331. return
  1332. }
  1333. props := n.Properties().Merge(that.Properties())
  1334. labels := n.Labels().Merge(that.Labels())
  1335. n.SetProperties(props)
  1336. n.SetLabels(labels)
  1337. start := n.Start()
  1338. if that.Start().Before(start) {
  1339. start = that.Start()
  1340. }
  1341. end := n.End()
  1342. if that.End().After(end) {
  1343. end = that.End()
  1344. }
  1345. window := n.Window().Expand(that.Window())
  1346. n.start = start
  1347. n.end = end
  1348. n.window = window
  1349. n.Cost += that.Cost
  1350. n.adjustment += that.adjustment
  1351. }
  1352. // Clone returns a deep copy of the given Network
  1353. func (n *Network) Clone() Asset {
  1354. if n == nil {
  1355. return nil
  1356. }
  1357. return &Network{
  1358. properties: n.properties.Clone(),
  1359. labels: n.labels.Clone(),
  1360. start: n.start,
  1361. end: n.end,
  1362. window: n.window.Clone(),
  1363. adjustment: n.adjustment,
  1364. Cost: n.Cost,
  1365. }
  1366. }
  1367. // Equal returns true if the tow Assets match exactly
  1368. func (n *Network) Equal(a Asset) bool {
  1369. that, ok := a.(*Network)
  1370. if !ok {
  1371. return false
  1372. }
  1373. if !n.Labels().Equal(that.Labels()) {
  1374. return false
  1375. }
  1376. if !n.Properties().Equal(that.Properties()) {
  1377. return false
  1378. }
  1379. if !n.Start().Equal(that.Start()) {
  1380. return false
  1381. }
  1382. if !n.End().Equal(that.End()) {
  1383. return false
  1384. }
  1385. if !n.window.Equal(that.window) {
  1386. return false
  1387. }
  1388. if n.adjustment != that.adjustment {
  1389. return false
  1390. }
  1391. if n.Cost != that.Cost {
  1392. return false
  1393. }
  1394. return true
  1395. }
  1396. // MarshalJSON implements json.Marshal interface
  1397. func (n *Network) MarshalJSON() ([]byte, error) {
  1398. buffer := bytes.NewBufferString("{")
  1399. jsonEncodeString(buffer, "type", n.Type().String(), ",")
  1400. jsonEncode(buffer, "properties", n.Properties(), ",")
  1401. jsonEncode(buffer, "labels", n.Labels(), ",")
  1402. jsonEncode(buffer, "window", n.Window(), ",")
  1403. jsonEncodeString(buffer, "start", n.Start().Format(time.RFC3339), ",")
  1404. jsonEncodeString(buffer, "end", n.End().Format(time.RFC3339), ",")
  1405. jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
  1406. jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
  1407. jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
  1408. buffer.WriteString("}")
  1409. return buffer.Bytes(), nil
  1410. }
  1411. // String implements fmt.Stringer
  1412. func (n *Network) String() string {
  1413. return toString(n)
  1414. }
  1415. // Node is an Asset representing a single node in a cluster
  1416. type Node struct {
  1417. properties *AssetProperties
  1418. labels AssetLabels
  1419. start time.Time
  1420. end time.Time
  1421. window Window
  1422. adjustment float64
  1423. NodeType string
  1424. CPUCoreHours float64
  1425. RAMByteHours float64
  1426. GPUHours float64
  1427. CPUBreakdown *Breakdown
  1428. RAMBreakdown *Breakdown
  1429. CPUCost float64
  1430. GPUCost float64
  1431. GPUCount float64
  1432. RAMCost float64
  1433. Discount float64
  1434. Preemptible float64
  1435. }
  1436. // NewNode creates and returns a new Node Asset
  1437. func NewNode(name, cluster, providerID string, start, end time.Time, window Window) *Node {
  1438. properties := &AssetProperties{
  1439. Category: ComputeCategory,
  1440. Name: name,
  1441. Cluster: cluster,
  1442. ProviderID: providerID,
  1443. Service: KubernetesService,
  1444. }
  1445. return &Node{
  1446. properties: properties,
  1447. labels: AssetLabels{},
  1448. start: start,
  1449. end: end,
  1450. window: window.Clone(),
  1451. CPUBreakdown: &Breakdown{},
  1452. RAMBreakdown: &Breakdown{},
  1453. }
  1454. }
  1455. // Type returns the AssetType of the Asset
  1456. func (n *Node) Type() AssetType {
  1457. return NodeAssetType
  1458. }
  1459. // Properties returns the Asset's properties
  1460. func (n *Node) Properties() *AssetProperties {
  1461. return n.properties
  1462. }
  1463. // SetProperties sets the Asset's properties
  1464. func (n *Node) SetProperties(props *AssetProperties) {
  1465. n.properties = props
  1466. }
  1467. // Labels returns the Asset's labels
  1468. func (n *Node) Labels() AssetLabels {
  1469. return n.labels
  1470. }
  1471. // SetLabels sets the Asset's labels
  1472. func (n *Node) SetLabels(labels AssetLabels) {
  1473. n.labels = labels
  1474. }
  1475. // Adjustment returns the Asset's cost adjustment
  1476. func (n *Node) Adjustment() float64 {
  1477. return n.adjustment
  1478. }
  1479. // SetAdjustment sets the Asset's cost adjustment
  1480. func (n *Node) SetAdjustment(adj float64) {
  1481. n.adjustment = adj
  1482. }
  1483. // TotalCost returns the Asset's total cost
  1484. func (n *Node) TotalCost() float64 {
  1485. return ((n.CPUCost + n.RAMCost) * (1.0 - n.Discount)) + n.GPUCost + n.adjustment
  1486. }
  1487. // Start returns the precise start time of the Asset within the window
  1488. func (n *Node) Start() time.Time {
  1489. return n.start
  1490. }
  1491. // End returns the precise end time of the Asset within the window
  1492. func (n *Node) End() time.Time {
  1493. return n.end
  1494. }
  1495. // Minutes returns the number of minutes the Asset ran within the window
  1496. func (n *Node) Minutes() float64 {
  1497. nodeMins := n.end.Sub(n.start).Minutes()
  1498. windowMins := n.window.Minutes()
  1499. if nodeMins > windowMins {
  1500. log.Warningf("Asset ETL: Node.Minutes exceeds window: %.2f > %.2f", nodeMins, windowMins)
  1501. nodeMins = windowMins
  1502. }
  1503. if nodeMins < 0 {
  1504. nodeMins = 0
  1505. }
  1506. return nodeMins
  1507. }
  1508. // Window returns the window within which the Asset ran
  1509. func (n *Node) Window() Window {
  1510. return n.window
  1511. }
  1512. // ExpandWindow expands the Asset's window by the given window
  1513. func (n *Node) ExpandWindow(window Window) {
  1514. n.window = n.window.Expand(window)
  1515. }
  1516. // SetStartEnd sets the Asset's Start and End fields
  1517. func (n *Node) SetStartEnd(start, end time.Time) {
  1518. if n.Window().Contains(start) {
  1519. n.start = start
  1520. } else {
  1521. log.Warningf("Disk.SetStartEnd: start %s not in %s", start, n.Window())
  1522. }
  1523. if n.Window().Contains(end) {
  1524. n.end = end
  1525. } else {
  1526. log.Warningf("Disk.SetStartEnd: end %s not in %s", end, n.Window())
  1527. }
  1528. }
  1529. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  1530. // as much relevant information as possible (i.e. type, properties, labels).
  1531. func (n *Node) Add(a Asset) Asset {
  1532. // Node + Node = Node
  1533. if that, ok := a.(*Node); ok {
  1534. this := n.Clone().(*Node)
  1535. this.add(that)
  1536. return this
  1537. }
  1538. props := n.Properties().Merge(a.Properties())
  1539. labels := n.Labels().Merge(a.Labels())
  1540. start := n.Start()
  1541. if a.Start().Before(start) {
  1542. start = a.Start()
  1543. }
  1544. end := n.End()
  1545. if a.End().After(end) {
  1546. end = a.End()
  1547. }
  1548. window := n.Window().Expand(a.Window())
  1549. // Node + !Node = Any
  1550. any := NewAsset(start, end, window)
  1551. any.SetProperties(props)
  1552. any.SetLabels(labels)
  1553. any.adjustment = n.Adjustment() + a.Adjustment()
  1554. any.Cost = (n.TotalCost() - n.Adjustment()) + (a.TotalCost() - a.Adjustment())
  1555. return any
  1556. }
  1557. func (n *Node) add(that *Node) {
  1558. if n == nil {
  1559. n = that
  1560. return
  1561. }
  1562. props := n.Properties().Merge(that.Properties())
  1563. labels := n.Labels().Merge(that.Labels())
  1564. n.SetProperties(props)
  1565. n.SetLabels(labels)
  1566. if n.NodeType != that.NodeType {
  1567. n.NodeType = ""
  1568. }
  1569. start := n.Start()
  1570. if that.Start().Before(start) {
  1571. start = that.Start()
  1572. }
  1573. end := n.End()
  1574. if that.End().After(end) {
  1575. end = that.End()
  1576. }
  1577. window := n.Window().Expand(that.Window())
  1578. n.start = start
  1579. n.end = end
  1580. n.window = window
  1581. // Order of operations for node costs is:
  1582. // Discount(CPU + RAM) + GPU + Adjustment
  1583. // Combining discounts, then involves weighting each discount by each
  1584. // respective (CPU + RAM) cost. Combining preemptible, on the other
  1585. // hand, is done with all three (but not Adjustment, which can change
  1586. // without triggering a re-computation of Preemtible).
  1587. disc := (n.CPUCost+n.RAMCost)*(1.0-n.Discount) + (that.CPUCost+that.RAMCost)*(1.0-that.Discount)
  1588. nonDisc := (n.CPUCost + n.RAMCost) + (that.CPUCost + that.RAMCost)
  1589. if nonDisc > 0 {
  1590. n.Discount = 1.0 - (disc / nonDisc)
  1591. } else {
  1592. n.Discount = (n.Discount + that.Discount) / 2.0
  1593. }
  1594. nNoAdj := n.TotalCost() - n.Adjustment()
  1595. thatNoAdj := that.TotalCost() - that.Adjustment()
  1596. if (nNoAdj + thatNoAdj) > 0 {
  1597. n.Preemptible = (nNoAdj*n.Preemptible + thatNoAdj*that.Preemptible) / (nNoAdj + thatNoAdj)
  1598. } else {
  1599. n.Preemptible = (n.Preemptible + that.Preemptible) / 2.0
  1600. }
  1601. totalCPUCost := n.CPUCost + that.CPUCost
  1602. if totalCPUCost > 0.0 {
  1603. n.CPUBreakdown.Idle = (n.CPUBreakdown.Idle*n.CPUCost + that.CPUBreakdown.Idle*that.CPUCost) / totalCPUCost
  1604. n.CPUBreakdown.Other = (n.CPUBreakdown.Other*n.CPUCost + that.CPUBreakdown.Other*that.CPUCost) / totalCPUCost
  1605. n.CPUBreakdown.System = (n.CPUBreakdown.System*n.CPUCost + that.CPUBreakdown.System*that.CPUCost) / totalCPUCost
  1606. n.CPUBreakdown.User = (n.CPUBreakdown.User*n.CPUCost + that.CPUBreakdown.User*that.CPUCost) / totalCPUCost
  1607. }
  1608. totalRAMCost := n.RAMCost + that.RAMCost
  1609. if totalRAMCost > 0.0 {
  1610. n.RAMBreakdown.Idle = (n.RAMBreakdown.Idle*n.RAMCost + that.RAMBreakdown.Idle*that.RAMCost) / totalRAMCost
  1611. n.RAMBreakdown.Other = (n.RAMBreakdown.Other*n.RAMCost + that.RAMBreakdown.Other*that.RAMCost) / totalRAMCost
  1612. n.RAMBreakdown.System = (n.RAMBreakdown.System*n.RAMCost + that.RAMBreakdown.System*that.RAMCost) / totalRAMCost
  1613. n.RAMBreakdown.User = (n.RAMBreakdown.User*n.RAMCost + that.RAMBreakdown.User*that.RAMCost) / totalRAMCost
  1614. }
  1615. n.CPUCoreHours += that.CPUCoreHours
  1616. n.RAMByteHours += that.RAMByteHours
  1617. n.GPUHours += that.GPUHours
  1618. n.CPUCost += that.CPUCost
  1619. n.GPUCost += that.GPUCost
  1620. n.RAMCost += that.RAMCost
  1621. n.adjustment += that.adjustment
  1622. }
  1623. // Clone returns a deep copy of the given Node
  1624. func (n *Node) Clone() Asset {
  1625. if n == nil {
  1626. return nil
  1627. }
  1628. return &Node{
  1629. properties: n.properties.Clone(),
  1630. labels: n.labels.Clone(),
  1631. start: n.start,
  1632. end: n.end,
  1633. window: n.window.Clone(),
  1634. adjustment: n.adjustment,
  1635. NodeType: n.NodeType,
  1636. CPUCoreHours: n.CPUCoreHours,
  1637. RAMByteHours: n.RAMByteHours,
  1638. GPUHours: n.GPUHours,
  1639. CPUBreakdown: n.CPUBreakdown.Clone(),
  1640. RAMBreakdown: n.RAMBreakdown.Clone(),
  1641. CPUCost: n.CPUCost,
  1642. GPUCost: n.GPUCost,
  1643. GPUCount: n.GPUCount,
  1644. RAMCost: n.RAMCost,
  1645. Preemptible: n.Preemptible,
  1646. Discount: n.Discount,
  1647. }
  1648. }
  1649. // Equal returns true if the tow Assets match exactly
  1650. func (n *Node) Equal(a Asset) bool {
  1651. that, ok := a.(*Node)
  1652. if !ok {
  1653. return false
  1654. }
  1655. if !n.Labels().Equal(that.Labels()) {
  1656. return false
  1657. }
  1658. if !n.Properties().Equal(that.Properties()) {
  1659. return false
  1660. }
  1661. if !n.Start().Equal(that.Start()) {
  1662. return false
  1663. }
  1664. if !n.End().Equal(that.End()) {
  1665. return false
  1666. }
  1667. if !n.window.Equal(that.window) {
  1668. return false
  1669. }
  1670. if n.adjustment != that.adjustment {
  1671. return false
  1672. }
  1673. if n.NodeType != that.NodeType {
  1674. return false
  1675. }
  1676. if n.CPUCoreHours != that.CPUCoreHours {
  1677. return false
  1678. }
  1679. if n.RAMByteHours != that.RAMByteHours {
  1680. return false
  1681. }
  1682. if n.GPUHours != that.GPUHours {
  1683. return false
  1684. }
  1685. if !n.CPUBreakdown.Equal(that.CPUBreakdown) {
  1686. return false
  1687. }
  1688. if !n.RAMBreakdown.Equal(that.RAMBreakdown) {
  1689. return false
  1690. }
  1691. if n.CPUCost != that.CPUCost {
  1692. return false
  1693. }
  1694. if n.GPUCost != that.GPUCost {
  1695. return false
  1696. }
  1697. if n.RAMCost != that.RAMCost {
  1698. return false
  1699. }
  1700. if n.Discount != that.Discount {
  1701. return false
  1702. }
  1703. if n.Preemptible != that.Preemptible {
  1704. return false
  1705. }
  1706. return true
  1707. }
  1708. // MarshalJSON implements json.Marshal interface
  1709. func (n *Node) MarshalJSON() ([]byte, error) {
  1710. buffer := bytes.NewBufferString("{")
  1711. jsonEncodeString(buffer, "type", n.Type().String(), ",")
  1712. jsonEncode(buffer, "properties", n.Properties(), ",")
  1713. jsonEncode(buffer, "labels", n.Labels(), ",")
  1714. jsonEncode(buffer, "window", n.Window(), ",")
  1715. jsonEncodeString(buffer, "start", n.Start().Format(time.RFC3339), ",")
  1716. jsonEncodeString(buffer, "end", n.End().Format(time.RFC3339), ",")
  1717. jsonEncodeFloat64(buffer, "minutes", n.Minutes(), ",")
  1718. jsonEncodeString(buffer, "nodeType", n.NodeType, ",")
  1719. jsonEncodeFloat64(buffer, "cpuCores", n.CPUCores(), ",")
  1720. jsonEncodeFloat64(buffer, "ramBytes", n.RAMBytes(), ",")
  1721. jsonEncodeFloat64(buffer, "cpuCoreHours", n.CPUCoreHours, ",")
  1722. jsonEncodeFloat64(buffer, "ramByteHours", n.RAMByteHours, ",")
  1723. jsonEncodeFloat64(buffer, "GPUHours", n.GPUHours, ",")
  1724. jsonEncode(buffer, "cpuBreakdown", n.CPUBreakdown, ",")
  1725. jsonEncode(buffer, "ramBreakdown", n.RAMBreakdown, ",")
  1726. jsonEncodeFloat64(buffer, "preemptible", n.Preemptible, ",")
  1727. jsonEncodeFloat64(buffer, "discount", n.Discount, ",")
  1728. jsonEncodeFloat64(buffer, "cpuCost", n.CPUCost, ",")
  1729. jsonEncodeFloat64(buffer, "gpuCost", n.GPUCost, ",")
  1730. jsonEncodeFloat64(buffer, "gpuCount", n.GPUs(), ",")
  1731. jsonEncodeFloat64(buffer, "ramCost", n.RAMCost, ",")
  1732. jsonEncodeFloat64(buffer, "adjustment", n.Adjustment(), ",")
  1733. jsonEncodeFloat64(buffer, "totalCost", n.TotalCost(), "")
  1734. buffer.WriteString("}")
  1735. return buffer.Bytes(), nil
  1736. }
  1737. // String implements fmt.Stringer
  1738. func (n *Node) String() string {
  1739. return toString(n)
  1740. }
  1741. // IsPreemptible returns true if the node is 100% preemptible. It's possible
  1742. // to be "partially preemptible" by adding a preemptible node with a
  1743. // non-preemptible node.
  1744. func (n *Node) IsPreemptible() bool {
  1745. return n.Preemptible == 1.0
  1746. }
  1747. // CPUCores returns the number of cores belonging to the node. This could be
  1748. // fractional because it's the number of core*hours divided by the number of
  1749. // hours running; e.g. the sum of a 4-core node running for the first 10 hours
  1750. // and a 3-core node running for the last 20 hours of the same 24-hour window
  1751. // would produce:
  1752. // (4*10 + 3*20) / 24 = 4.167 cores
  1753. // However, any number of cores running for the full span of a window will
  1754. // report the actual number of cores of the static node; e.g. the above
  1755. // scenario for one entire 24-hour window:
  1756. // (4*24 + 3*24) / 24 = (4 + 3) = 7 cores
  1757. func (n *Node) CPUCores() float64 {
  1758. // [core*hr]*([min/hr]*[1/min]) = [core*hr]/[hr] = core
  1759. return n.CPUCoreHours * (60.0 / n.Minutes())
  1760. }
  1761. // RAMBytes returns the amount of RAM belonging to the node. This could be
  1762. // fractional because it's the number of byte*hours divided by the number of
  1763. // hours running; e.g. the sum of a 12GiB-RAM node running for the first 10 hours
  1764. // and a 16GiB-RAM node running for the last 20 hours of the same 24-hour window
  1765. // would produce:
  1766. // (12*10 + 16*20) / 24 = 18.333GiB RAM
  1767. // However, any number of bytes running for the full span of a window will
  1768. // report the actual number of bytes of the static node; e.g. the above
  1769. // scenario for one entire 24-hour window:
  1770. // (12*24 + 16*24) / 24 = (12 + 16) = 28GiB RAM
  1771. func (n *Node) RAMBytes() float64 {
  1772. // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
  1773. return n.RAMByteHours * (60.0 / n.Minutes())
  1774. }
  1775. // GPUs returns the amount of GPUs belonging to the node. This could be
  1776. // fractional because it's the number of gpu*hours divided by the number of
  1777. // hours running; e.g. the sum of a 2 gpu node running for the first 10 hours
  1778. // and a 1 gpu node running for the last 20 hours of the same 24-hour window
  1779. // would produce:
  1780. // (2*10 + 1*20) / 24 = 1.667 GPUs
  1781. // However, any number of GPUs running for the full span of a window will
  1782. // report the actual number of GPUs of the static node; e.g. the above
  1783. // scenario for one entire 24-hour window:
  1784. // (2*24 + 1*24) / 24 = (2 + 1) = 3 GPUs
  1785. func (n *Node) GPUs() float64 {
  1786. // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
  1787. return n.GPUHours * (60.0 / n.Minutes())
  1788. }
  1789. // LoadBalancer is an Asset representing a single load balancer in a cluster
  1790. // TODO: add GB of ingress processed, numForwardingRules once we start recording those to prometheus metric
  1791. type LoadBalancer struct {
  1792. properties *AssetProperties
  1793. labels AssetLabels
  1794. start time.Time
  1795. end time.Time
  1796. window Window
  1797. adjustment float64
  1798. Cost float64
  1799. }
  1800. // NewLoadBalancer instantiates and returns a new LoadBalancer
  1801. func NewLoadBalancer(name, cluster, providerID string, start, end time.Time, window Window) *LoadBalancer {
  1802. properties := &AssetProperties{
  1803. Category: NetworkCategory,
  1804. Name: name,
  1805. Cluster: cluster,
  1806. ProviderID: providerID,
  1807. Service: KubernetesService,
  1808. }
  1809. return &LoadBalancer{
  1810. properties: properties,
  1811. labels: AssetLabels{},
  1812. start: start,
  1813. end: end,
  1814. window: window,
  1815. }
  1816. }
  1817. // Type returns the AssetType of the Asset
  1818. func (lb *LoadBalancer) Type() AssetType {
  1819. return LoadBalancerAssetType
  1820. }
  1821. // Properties returns the Asset's properties
  1822. func (lb *LoadBalancer) Properties() *AssetProperties {
  1823. return lb.properties
  1824. }
  1825. // SetProperties sets the Asset's properties
  1826. func (lb *LoadBalancer) SetProperties(props *AssetProperties) {
  1827. lb.properties = props
  1828. }
  1829. // Labels returns the Asset's labels
  1830. func (lb *LoadBalancer) Labels() AssetLabels {
  1831. return lb.labels
  1832. }
  1833. // SetLabels sets the Asset's labels
  1834. func (lb *LoadBalancer) SetLabels(labels AssetLabels) {
  1835. lb.labels = labels
  1836. }
  1837. // Adjustment returns the Asset's cost adjustment
  1838. func (lb *LoadBalancer) Adjustment() float64 {
  1839. return lb.adjustment
  1840. }
  1841. // SetAdjustment sets the Asset's cost adjustment
  1842. func (lb *LoadBalancer) SetAdjustment(adj float64) {
  1843. lb.adjustment = adj
  1844. }
  1845. // TotalCost returns the total cost of the Asset
  1846. func (lb *LoadBalancer) TotalCost() float64 {
  1847. return lb.Cost + lb.adjustment
  1848. }
  1849. // Start returns the preceise start point of the Asset within the window
  1850. func (lb *LoadBalancer) Start() time.Time {
  1851. return lb.start
  1852. }
  1853. // End returns the preceise end point of the Asset within the window
  1854. func (lb *LoadBalancer) End() time.Time {
  1855. return lb.end
  1856. }
  1857. // Minutes returns the number of minutes the Asset ran within the window
  1858. func (lb *LoadBalancer) Minutes() float64 {
  1859. return lb.end.Sub(lb.start).Minutes()
  1860. }
  1861. // Window returns the window within which the Asset ran
  1862. func (lb *LoadBalancer) Window() Window {
  1863. return lb.window
  1864. }
  1865. // ExpandWindow expands the Asset's window by the given window
  1866. func (lb *LoadBalancer) ExpandWindow(w Window) {
  1867. lb.window = lb.window.Expand(w)
  1868. }
  1869. // SetStartEnd sets the Asset's Start and End fields
  1870. func (lb *LoadBalancer) SetStartEnd(start, end time.Time) {
  1871. if lb.Window().Contains(start) {
  1872. lb.start = start
  1873. } else {
  1874. log.Warningf("Disk.SetStartEnd: start %s not in %s", start, lb.Window())
  1875. }
  1876. if lb.Window().Contains(end) {
  1877. lb.end = end
  1878. } else {
  1879. log.Warningf("Disk.SetStartEnd: end %s not in %s", end, lb.Window())
  1880. }
  1881. }
  1882. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  1883. // as much relevant information as possible (i.e. type, properties, labels).
  1884. func (lb *LoadBalancer) Add(a Asset) Asset {
  1885. // LoadBalancer + LoadBalancer = LoadBalancer
  1886. if that, ok := a.(*LoadBalancer); ok {
  1887. this := lb.Clone().(*LoadBalancer)
  1888. this.add(that)
  1889. return this
  1890. }
  1891. props := lb.Properties().Merge(a.Properties())
  1892. labels := lb.Labels().Merge(a.Labels())
  1893. start := lb.Start()
  1894. if a.Start().Before(start) {
  1895. start = a.Start()
  1896. }
  1897. end := lb.End()
  1898. if a.End().After(end) {
  1899. end = a.End()
  1900. }
  1901. window := lb.Window().Expand(a.Window())
  1902. // LoadBalancer + !LoadBalancer = Any
  1903. any := NewAsset(start, end, window)
  1904. any.SetProperties(props)
  1905. any.SetLabels(labels)
  1906. any.adjustment = lb.Adjustment() + a.Adjustment()
  1907. any.Cost = (lb.TotalCost() - lb.Adjustment()) + (a.TotalCost() - a.Adjustment())
  1908. return any
  1909. }
  1910. func (lb *LoadBalancer) add(that *LoadBalancer) {
  1911. if lb == nil {
  1912. lb = that
  1913. return
  1914. }
  1915. props := lb.Properties().Merge(that.Properties())
  1916. labels := lb.Labels().Merge(that.Labels())
  1917. lb.SetProperties(props)
  1918. lb.SetLabels(labels)
  1919. start := lb.Start()
  1920. if that.Start().Before(start) {
  1921. start = that.Start()
  1922. }
  1923. end := lb.End()
  1924. if that.End().After(end) {
  1925. end = that.End()
  1926. }
  1927. window := lb.Window().Expand(that.Window())
  1928. lb.start = start
  1929. lb.end = end
  1930. lb.window = window
  1931. lb.Cost += that.Cost
  1932. lb.adjustment += that.adjustment
  1933. }
  1934. // Clone returns a cloned instance of the given Asset
  1935. func (lb *LoadBalancer) Clone() Asset {
  1936. return &LoadBalancer{
  1937. properties: lb.properties.Clone(),
  1938. labels: lb.labels.Clone(),
  1939. start: lb.start,
  1940. end: lb.end,
  1941. window: lb.window.Clone(),
  1942. adjustment: lb.adjustment,
  1943. Cost: lb.Cost,
  1944. }
  1945. }
  1946. // Equal returns true if the tow Assets match precisely
  1947. func (lb *LoadBalancer) Equal(a Asset) bool {
  1948. that, ok := a.(*LoadBalancer)
  1949. if !ok {
  1950. return false
  1951. }
  1952. if !lb.Labels().Equal(that.Labels()) {
  1953. return false
  1954. }
  1955. if !lb.Properties().Equal(that.Properties()) {
  1956. return false
  1957. }
  1958. if !lb.Start().Equal(that.Start()) {
  1959. return false
  1960. }
  1961. if !lb.End().Equal(that.End()) {
  1962. return false
  1963. }
  1964. if !lb.window.Equal(that.window) {
  1965. return false
  1966. }
  1967. if lb.adjustment != that.adjustment {
  1968. return false
  1969. }
  1970. if lb.Cost != that.Cost {
  1971. return false
  1972. }
  1973. return true
  1974. }
  1975. // MarshalJSON implements json.Marshal
  1976. func (lb *LoadBalancer) MarshalJSON() ([]byte, error) {
  1977. buffer := bytes.NewBufferString("{")
  1978. jsonEncodeString(buffer, "type", lb.Type().String(), ",")
  1979. jsonEncode(buffer, "properties", lb.Properties(), ",")
  1980. jsonEncode(buffer, "labels", lb.Labels(), ",")
  1981. jsonEncode(buffer, "window", lb.Window(), ",")
  1982. jsonEncodeString(buffer, "start", lb.Start().Format(time.RFC3339), ",")
  1983. jsonEncodeString(buffer, "end", lb.End().Format(time.RFC3339), ",")
  1984. jsonEncodeFloat64(buffer, "minutes", lb.Minutes(), ",")
  1985. jsonEncodeFloat64(buffer, "adjustment", lb.Adjustment(), ",")
  1986. jsonEncodeFloat64(buffer, "totalCost", lb.TotalCost(), "")
  1987. buffer.WriteString("}")
  1988. return buffer.Bytes(), nil
  1989. }
  1990. // String implements fmt.Stringer
  1991. func (lb *LoadBalancer) String() string {
  1992. return toString(lb)
  1993. }
  1994. // SharedAsset is an Asset representing a shared cost
  1995. type SharedAsset struct {
  1996. properties *AssetProperties
  1997. labels AssetLabels
  1998. window Window
  1999. Cost float64
  2000. }
  2001. // NewSharedAsset creates and returns a new SharedAsset
  2002. func NewSharedAsset(name string, window Window) *SharedAsset {
  2003. properties := &AssetProperties{
  2004. Name: name,
  2005. Category: SharedCategory,
  2006. Service: OtherCategory,
  2007. }
  2008. return &SharedAsset{
  2009. properties: properties,
  2010. labels: AssetLabels{},
  2011. window: window.Clone(),
  2012. }
  2013. }
  2014. // Type returns the AssetType of the Asset
  2015. func (sa *SharedAsset) Type() AssetType {
  2016. return SharedAssetType
  2017. }
  2018. // Properties returns the Asset's properties
  2019. func (sa *SharedAsset) Properties() *AssetProperties {
  2020. return sa.properties
  2021. }
  2022. // SetProperties sets the Asset's properties
  2023. func (sa *SharedAsset) SetProperties(props *AssetProperties) {
  2024. sa.properties = props
  2025. }
  2026. // Labels returns the Asset's labels
  2027. func (sa *SharedAsset) Labels() AssetLabels {
  2028. return sa.labels
  2029. }
  2030. // SetLabels sets the Asset's labels
  2031. func (sa *SharedAsset) SetLabels(labels AssetLabels) {
  2032. sa.labels = labels
  2033. }
  2034. // Adjustment is not relevant to SharedAsset, but required to implement Asset
  2035. func (sa *SharedAsset) Adjustment() float64 {
  2036. return 0.0
  2037. }
  2038. // SetAdjustment is not relevant to SharedAsset, but required to implement Asset
  2039. func (sa *SharedAsset) SetAdjustment(float64) {
  2040. return
  2041. }
  2042. // TotalCost returns the Asset's total cost
  2043. func (sa *SharedAsset) TotalCost() float64 {
  2044. return sa.Cost
  2045. }
  2046. // Start returns the start time of the Asset
  2047. func (sa *SharedAsset) Start() time.Time {
  2048. return *sa.window.start
  2049. }
  2050. // End returns the end time of the Asset
  2051. func (sa *SharedAsset) End() time.Time {
  2052. return *sa.window.end
  2053. }
  2054. // Minutes returns the number of minutes the SharedAsset ran within the window
  2055. func (sa *SharedAsset) Minutes() float64 {
  2056. return sa.window.Minutes()
  2057. }
  2058. // Window returns the window within the SharedAsset ran
  2059. func (sa *SharedAsset) Window() Window {
  2060. return sa.window
  2061. }
  2062. // ExpandWindow expands the Asset's window
  2063. func (sa *SharedAsset) ExpandWindow(w Window) {
  2064. sa.window = sa.window.Expand(w)
  2065. }
  2066. // SetStartEnd sets the Asset's Start and End fields (not applicable here)
  2067. func (sa *SharedAsset) SetStartEnd(start, end time.Time) {
  2068. return
  2069. }
  2070. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  2071. // as much relevant information as possible (i.e. type, properties, labels).
  2072. func (sa *SharedAsset) Add(a Asset) Asset {
  2073. // SharedAsset + SharedAsset = SharedAsset
  2074. if that, ok := a.(*SharedAsset); ok {
  2075. this := sa.Clone().(*SharedAsset)
  2076. this.add(that)
  2077. return this
  2078. }
  2079. props := sa.Properties().Merge(a.Properties())
  2080. labels := sa.Labels().Merge(a.Labels())
  2081. start := sa.Start()
  2082. if a.Start().Before(start) {
  2083. start = a.Start()
  2084. }
  2085. end := sa.End()
  2086. if a.End().After(end) {
  2087. end = a.End()
  2088. }
  2089. window := sa.Window().Expand(a.Window())
  2090. // SharedAsset + !SharedAsset = Any
  2091. any := NewAsset(start, end, window)
  2092. any.SetProperties(props)
  2093. any.SetLabels(labels)
  2094. any.adjustment = sa.Adjustment() + a.Adjustment()
  2095. any.Cost = (sa.TotalCost() - sa.Adjustment()) + (a.TotalCost() - a.Adjustment())
  2096. return any
  2097. }
  2098. func (sa *SharedAsset) add(that *SharedAsset) {
  2099. if sa == nil {
  2100. sa = that
  2101. return
  2102. }
  2103. props := sa.Properties().Merge(that.Properties())
  2104. labels := sa.Labels().Merge(that.Labels())
  2105. sa.SetProperties(props)
  2106. sa.SetLabels(labels)
  2107. window := sa.Window().Expand(that.Window())
  2108. sa.window = window
  2109. sa.Cost += that.Cost
  2110. }
  2111. // Clone returns a deep copy of the given SharedAsset
  2112. func (sa *SharedAsset) Clone() Asset {
  2113. if sa == nil {
  2114. return nil
  2115. }
  2116. return &SharedAsset{
  2117. properties: sa.properties.Clone(),
  2118. labels: sa.labels.Clone(),
  2119. window: sa.window.Clone(),
  2120. Cost: sa.Cost,
  2121. }
  2122. }
  2123. // Equal returns true if the two Assets are exact matches
  2124. func (sa *SharedAsset) Equal(a Asset) bool {
  2125. that, ok := a.(*SharedAsset)
  2126. if !ok {
  2127. return false
  2128. }
  2129. if !sa.Labels().Equal(that.Labels()) {
  2130. return false
  2131. }
  2132. if !sa.Properties().Equal(that.Properties()) {
  2133. return false
  2134. }
  2135. if !sa.Start().Equal(that.Start()) {
  2136. return false
  2137. }
  2138. if !sa.End().Equal(that.End()) {
  2139. return false
  2140. }
  2141. if !sa.window.Equal(that.window) {
  2142. return false
  2143. }
  2144. if sa.Cost != that.Cost {
  2145. return false
  2146. }
  2147. return true
  2148. }
  2149. // MarshalJSON implements json.Marshaler
  2150. func (sa *SharedAsset) MarshalJSON() ([]byte, error) {
  2151. buffer := bytes.NewBufferString("{")
  2152. jsonEncodeString(buffer, "type", sa.Type().String(), ",")
  2153. jsonEncode(buffer, "properties", sa.Properties(), ",")
  2154. jsonEncode(buffer, "labels", sa.Labels(), ",")
  2155. jsonEncode(buffer, "properties", sa.Properties(), ",")
  2156. jsonEncode(buffer, "labels", sa.Labels(), ",")
  2157. jsonEncode(buffer, "window", sa.Window(), ",")
  2158. jsonEncodeString(buffer, "start", sa.Start().Format(time.RFC3339), ",")
  2159. jsonEncodeString(buffer, "end", sa.End().Format(time.RFC3339), ",")
  2160. jsonEncodeFloat64(buffer, "minutes", sa.Minutes(), ",")
  2161. jsonEncodeFloat64(buffer, "totalCost", sa.TotalCost(), "")
  2162. buffer.WriteString("}")
  2163. return buffer.Bytes(), nil
  2164. }
  2165. // String implements fmt.Stringer
  2166. func (sa *SharedAsset) String() string {
  2167. return toString(sa)
  2168. }
  2169. // AssetSet stores a set of Assets, each with a unique name, that share
  2170. // a window. An AssetSet is mutable, so treat it like a threadsafe map.
  2171. type AssetSet struct {
  2172. sync.RWMutex
  2173. aggregateBy []string
  2174. assets map[string]Asset
  2175. FromSource string // stores the name of the source used to compute the data
  2176. Window Window
  2177. Warnings []string
  2178. Errors []string
  2179. }
  2180. // NewAssetSet instantiates a new AssetSet and, optionally, inserts
  2181. // the given list of Assets
  2182. func NewAssetSet(start, end time.Time, assets ...Asset) *AssetSet {
  2183. as := &AssetSet{
  2184. assets: map[string]Asset{},
  2185. Window: NewWindow(&start, &end),
  2186. }
  2187. for _, a := range assets {
  2188. as.Insert(a)
  2189. }
  2190. return as
  2191. }
  2192. // AggregateBy aggregates the Assets in the AssetSet by the given list of
  2193. // AssetProperties, such that each asset is binned by a key determined by its
  2194. // relevant property values.
  2195. func (as *AssetSet) AggregateBy(aggregateBy []string, opts *AssetAggregationOptions) error {
  2196. if opts == nil {
  2197. opts = &AssetAggregationOptions{}
  2198. }
  2199. if as.IsEmpty() && len(opts.SharedHourlyCosts) == 0 {
  2200. return nil
  2201. }
  2202. as.Lock()
  2203. defer as.Unlock()
  2204. aggSet := NewAssetSet(as.Start(), as.End())
  2205. aggSet.aggregateBy = aggregateBy
  2206. // Compute hours of the given AssetSet, and if it ends in the future,
  2207. // adjust the hours accordingly
  2208. hours := as.Window.Minutes() / 60.0
  2209. diff := time.Since(as.End())
  2210. if diff < 0.0 {
  2211. hours += diff.Hours()
  2212. }
  2213. // Insert a shared asset for each shared cost
  2214. for name, hourlyCost := range opts.SharedHourlyCosts {
  2215. sa := NewSharedAsset(name, as.Window.Clone())
  2216. sa.Cost = hourlyCost * hours
  2217. err := aggSet.Insert(sa)
  2218. if err != nil {
  2219. return err
  2220. }
  2221. }
  2222. // Delete the Assets that don't pass each filter
  2223. for _, ff := range opts.FilterFuncs {
  2224. for key, asset := range as.assets {
  2225. if !ff(asset) {
  2226. delete(as.assets, key)
  2227. }
  2228. }
  2229. }
  2230. // Insert each asset into the new set, which will be keyed by the `aggregateBy`
  2231. // on aggSet, resulting in aggregation.
  2232. for _, asset := range as.assets {
  2233. err := aggSet.Insert(asset)
  2234. if err != nil {
  2235. return err
  2236. }
  2237. }
  2238. // Assign the aggregated values back to the original set
  2239. as.assets = aggSet.assets
  2240. as.aggregateBy = aggregateBy
  2241. return nil
  2242. }
  2243. // Clone returns a new AssetSet with a deep copy of the given
  2244. // AssetSet's assets.
  2245. func (as *AssetSet) Clone() *AssetSet {
  2246. if as == nil {
  2247. return nil
  2248. }
  2249. as.RLock()
  2250. defer as.RUnlock()
  2251. var aggregateBy []string
  2252. if as.aggregateBy != nil {
  2253. aggregateBy = append([]string{}, as.aggregateBy...)
  2254. }
  2255. assets := map[string]Asset{}
  2256. for k, v := range as.assets {
  2257. assets[k] = v.Clone()
  2258. }
  2259. s := as.Start()
  2260. e := as.End()
  2261. return &AssetSet{
  2262. Window: NewWindow(&s, &e),
  2263. aggregateBy: aggregateBy,
  2264. assets: assets,
  2265. }
  2266. }
  2267. // Each invokes the given function for each Asset in the set
  2268. func (as *AssetSet) Each(f func(string, Asset)) {
  2269. if as == nil {
  2270. return
  2271. }
  2272. for k, a := range as.assets {
  2273. f(k, a)
  2274. }
  2275. }
  2276. // End returns the end time of the AssetSet's window
  2277. func (as *AssetSet) End() time.Time {
  2278. return *as.Window.End()
  2279. }
  2280. // FindMatch attempts to find a match in the AssetSet for the given Asset on
  2281. // the provided properties and labels. If a match is not found, FindMatch
  2282. // returns nil and a Not Found error.
  2283. func (as *AssetSet) FindMatch(query Asset, aggregateBy []string) (Asset, error) {
  2284. as.RLock()
  2285. defer as.RUnlock()
  2286. matchKey, err := key(query, aggregateBy)
  2287. if err != nil {
  2288. return nil, err
  2289. }
  2290. for _, asset := range as.assets {
  2291. if k, err := key(asset, aggregateBy); err != nil {
  2292. return nil, err
  2293. } else if k == matchKey {
  2294. return asset, nil
  2295. }
  2296. }
  2297. return nil, fmt.Errorf("Asset not found to match %s on %v", query, aggregateBy)
  2298. }
  2299. // ReconciliationMatch attempts to find an exact match in the AssetSet on
  2300. // (Category, ProviderID). If a match is found, it returns the Asset with the
  2301. // intent to adjust it. If no match exists, it attempts to find one on only
  2302. // (ProviderID). If that match is found, it returns the Asset with the intent
  2303. // to insert the associated Cloud cost.
  2304. func (as *AssetSet) ReconciliationMatch(query Asset) (Asset, bool, error) {
  2305. as.RLock()
  2306. defer as.RUnlock()
  2307. // Full match means matching on (Category, ProviderID)
  2308. fullMatchProps := []string{string(AssetCategoryProp), string(AssetProviderIDProp)}
  2309. fullMatchKey, err := key(query, fullMatchProps)
  2310. // This should never happen because we are using enumerated properties,
  2311. // but the check is here in case that changes
  2312. if err != nil {
  2313. return nil, false, err
  2314. }
  2315. // Partial match means matching only on (ProviderID)
  2316. providerIDMatchProps := []string{string(AssetProviderIDProp)}
  2317. providerIDMatchKey, err := key(query, providerIDMatchProps)
  2318. // This should never happen because we are using enumerated properties,
  2319. // but the check is here in case that changes
  2320. if err != nil {
  2321. return nil, false, err
  2322. }
  2323. var providerIDMatch Asset
  2324. for _, asset := range as.assets {
  2325. if k, err := key(asset, fullMatchProps); err != nil {
  2326. return nil, false, err
  2327. } else if k == fullMatchKey {
  2328. log.DedupedInfof(10, "Asset ETL: Reconciliation[rcnw]: ReconcileRange Match: %s", fullMatchKey)
  2329. return asset, true, nil
  2330. }
  2331. if k, err := key(asset, providerIDMatchProps); err != nil {
  2332. return nil, false, err
  2333. } else if k == providerIDMatchKey {
  2334. // Found a partial match. Save it until after all other options
  2335. // have been checked for full matches.
  2336. providerIDMatch = asset
  2337. }
  2338. }
  2339. // No full match was found, so return partial match, if found.
  2340. if providerIDMatch != nil {
  2341. return providerIDMatch, false, nil
  2342. }
  2343. return nil, false, fmt.Errorf("Asset not found to match %s", query)
  2344. }
  2345. // Get returns the Asset in the AssetSet at the given key, or nil and false
  2346. // if no Asset exists for the given key
  2347. func (as *AssetSet) Get(key string) (Asset, bool) {
  2348. as.RLock()
  2349. defer as.RUnlock()
  2350. if a, ok := as.assets[key]; ok {
  2351. return a, true
  2352. }
  2353. return nil, false
  2354. }
  2355. // Insert inserts the given Asset into the AssetSet, using the AssetSet's
  2356. // configured properties to determine the key under which the Asset will
  2357. // be inserted.
  2358. func (as *AssetSet) Insert(asset Asset) error {
  2359. if as == nil {
  2360. return fmt.Errorf("cannot Insert into nil AssetSet")
  2361. }
  2362. as.Lock()
  2363. defer as.Unlock()
  2364. if as.assets == nil {
  2365. as.assets = map[string]Asset{}
  2366. }
  2367. // Determine key into which to Insert the Asset.
  2368. k, err := key(asset, as.aggregateBy)
  2369. if err != nil {
  2370. return err
  2371. }
  2372. // Add the given Asset to the existing entry, if there is one;
  2373. // otherwise just set directly into assets
  2374. if _, ok := as.assets[k]; !ok {
  2375. as.assets[k] = asset
  2376. } else {
  2377. as.assets[k] = as.assets[k].Add(asset)
  2378. }
  2379. // Expand the window, just to be safe. It's possible that the asset will
  2380. // be set into the map without expanding it to the AssetSet's window.
  2381. as.assets[k].ExpandWindow(as.Window)
  2382. return nil
  2383. }
  2384. // IsEmpty returns true if the AssetSet is nil, or if it contains
  2385. // zero assets.
  2386. func (as *AssetSet) IsEmpty() bool {
  2387. if as == nil || len(as.assets) == 0 {
  2388. return true
  2389. }
  2390. as.RLock()
  2391. defer as.RUnlock()
  2392. return as.assets == nil || len(as.assets) == 0
  2393. }
  2394. func (as *AssetSet) Length() int {
  2395. if as == nil {
  2396. return 0
  2397. }
  2398. as.RLock()
  2399. defer as.RUnlock()
  2400. return len(as.assets)
  2401. }
  2402. // Map clones and returns a map of the AssetSet's Assets
  2403. func (as *AssetSet) Map() map[string]Asset {
  2404. if as.IsEmpty() {
  2405. return map[string]Asset{}
  2406. }
  2407. return as.Clone().assets
  2408. }
  2409. // MarshalJSON JSON-encodes the AssetSet
  2410. func (as *AssetSet) MarshalJSON() ([]byte, error) {
  2411. as.RLock()
  2412. defer as.RUnlock()
  2413. return json.Marshal(as.assets)
  2414. }
  2415. func (as *AssetSet) Set(asset Asset, aggregateBy []string) error {
  2416. if as.IsEmpty() {
  2417. as.Lock()
  2418. as.assets = map[string]Asset{}
  2419. as.Unlock()
  2420. }
  2421. as.Lock()
  2422. defer as.Unlock()
  2423. // Expand the window to match the AssetSet, then set it
  2424. asset.ExpandWindow(as.Window)
  2425. k, err := key(asset, aggregateBy)
  2426. if err != nil {
  2427. return err
  2428. }
  2429. as.assets[k] = asset
  2430. return nil
  2431. }
  2432. func (as *AssetSet) Start() time.Time {
  2433. return *as.Window.Start()
  2434. }
  2435. func (as *AssetSet) TotalCost() float64 {
  2436. tc := 0.0
  2437. as.Lock()
  2438. defer as.Unlock()
  2439. for _, a := range as.assets {
  2440. tc += a.TotalCost()
  2441. }
  2442. return tc
  2443. }
  2444. func (as *AssetSet) UTCOffset() time.Duration {
  2445. _, zone := as.Start().Zone()
  2446. return time.Duration(zone) * time.Second
  2447. }
  2448. func (as *AssetSet) accumulate(that *AssetSet) (*AssetSet, error) {
  2449. if as == nil {
  2450. return that, nil
  2451. }
  2452. if that == nil {
  2453. return as, nil
  2454. }
  2455. // In the case of an AssetSetRange with empty entries, we may end up with
  2456. // an incoming `as` without an `aggregateBy`, even though we are tring to
  2457. // aggregate here. This handles that case by assigning the correct `aggregateBy`.
  2458. if !sameContents(as.aggregateBy, that.aggregateBy) {
  2459. if len(as.aggregateBy) == 0 {
  2460. as.aggregateBy = that.aggregateBy
  2461. }
  2462. }
  2463. // Set start, end to min(start), max(end)
  2464. start := as.Start()
  2465. end := as.End()
  2466. if that.Start().Before(start) {
  2467. start = that.Start()
  2468. }
  2469. if that.End().After(end) {
  2470. end = that.End()
  2471. }
  2472. if as.IsEmpty() && that.IsEmpty() {
  2473. return NewAssetSet(start, end), nil
  2474. }
  2475. acc := NewAssetSet(start, end)
  2476. acc.aggregateBy = as.aggregateBy
  2477. as.RLock()
  2478. defer as.RUnlock()
  2479. that.RLock()
  2480. defer that.RUnlock()
  2481. for _, asset := range as.assets {
  2482. err := acc.Insert(asset)
  2483. if err != nil {
  2484. return nil, err
  2485. }
  2486. }
  2487. for _, asset := range that.assets {
  2488. err := acc.Insert(asset)
  2489. if err != nil {
  2490. return nil, err
  2491. }
  2492. }
  2493. return acc, nil
  2494. }
  2495. // AssetSetRange is a thread-safe slice of AssetSets. It is meant to
  2496. // be used such that the AssetSets held are consecutive and coherent with
  2497. // respect to using the same aggregation properties, UTC offset, and
  2498. // resolution. However these rules are not necessarily enforced, so use wisely.
  2499. type AssetSetRange struct {
  2500. sync.RWMutex
  2501. assets []*AssetSet
  2502. FromStore string // stores the name of the store used to retrieve the data
  2503. }
  2504. // NewAssetSetRange instantiates a new range composed of the given
  2505. // AssetSets in the order provided.
  2506. func NewAssetSetRange(assets ...*AssetSet) *AssetSetRange {
  2507. return &AssetSetRange{
  2508. assets: assets,
  2509. }
  2510. }
  2511. // Accumulate sums each AssetSet in the given range, returning a single cumulative
  2512. // AssetSet for the entire range.
  2513. func (asr *AssetSetRange) Accumulate() (*AssetSet, error) {
  2514. var assetSet *AssetSet
  2515. var err error
  2516. asr.RLock()
  2517. defer asr.RUnlock()
  2518. for _, as := range asr.assets {
  2519. assetSet, err = assetSet.accumulate(as)
  2520. if err != nil {
  2521. return nil, err
  2522. }
  2523. }
  2524. return assetSet, nil
  2525. }
  2526. type AssetAggregationOptions struct {
  2527. SharedHourlyCosts map[string]float64
  2528. FilterFuncs []AssetMatchFunc
  2529. }
  2530. func (asr *AssetSetRange) AggregateBy(aggregateBy []string, opts *AssetAggregationOptions) error {
  2531. aggRange := &AssetSetRange{assets: []*AssetSet{}}
  2532. asr.Lock()
  2533. defer asr.Unlock()
  2534. for _, as := range asr.assets {
  2535. err := as.AggregateBy(aggregateBy, opts)
  2536. if err != nil {
  2537. return err
  2538. }
  2539. aggRange.assets = append(aggRange.assets, as)
  2540. }
  2541. asr.assets = aggRange.assets
  2542. return nil
  2543. }
  2544. func (asr *AssetSetRange) Append(that *AssetSet) {
  2545. asr.Lock()
  2546. defer asr.Unlock()
  2547. asr.assets = append(asr.assets, that)
  2548. }
  2549. // Each invokes the given function for each AssetSet in the range
  2550. func (asr *AssetSetRange) Each(f func(int, *AssetSet)) {
  2551. if asr == nil {
  2552. return
  2553. }
  2554. for i, as := range asr.assets {
  2555. f(i, as)
  2556. }
  2557. }
  2558. func (asr *AssetSetRange) Get(i int) (*AssetSet, error) {
  2559. if i < 0 || i >= len(asr.assets) {
  2560. return nil, fmt.Errorf("AssetSetRange: index out of range: %d", i)
  2561. }
  2562. asr.RLock()
  2563. defer asr.RUnlock()
  2564. return asr.assets[i], nil
  2565. }
  2566. func (asr *AssetSetRange) Length() int {
  2567. if asr == nil || asr.assets == nil {
  2568. return 0
  2569. }
  2570. asr.RLock()
  2571. defer asr.RUnlock()
  2572. return len(asr.assets)
  2573. }
  2574. func (asr *AssetSetRange) MarshalJSON() ([]byte, error) {
  2575. asr.RLock()
  2576. defer asr.RUnlock()
  2577. return json.Marshal(asr.assets)
  2578. }
  2579. func (asr *AssetSetRange) UTCOffset() time.Duration {
  2580. if asr.Length() == 0 {
  2581. return 0
  2582. }
  2583. as, err := asr.Get(0)
  2584. if err != nil {
  2585. return 0
  2586. }
  2587. return as.UTCOffset()
  2588. }
  2589. // Window returns the full window that the AssetSetRange spans, from the
  2590. // start of the first AssetSet to the end of the last one.
  2591. func (asr *AssetSetRange) Window() Window {
  2592. if asr == nil || asr.Length() == 0 {
  2593. return NewWindow(nil, nil)
  2594. }
  2595. start := asr.assets[0].Start()
  2596. end := asr.assets[asr.Length()-1].End()
  2597. return NewWindow(&start, &end)
  2598. }
  2599. // Returns true if string slices a and b contain all of the same strings, in any order.
  2600. func sameContents(a, b []string) bool {
  2601. if len(a) != len(b) {
  2602. return false
  2603. }
  2604. for i := range a {
  2605. if !contains(b, a[i]) {
  2606. return false
  2607. }
  2608. }
  2609. return true
  2610. }
  2611. func contains(slice []string, item string) bool {
  2612. for _, element := range slice {
  2613. if element == item {
  2614. return true
  2615. }
  2616. }
  2617. return false
  2618. }