asset.go 68 KB

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