asset.go 77 KB

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