asset.go 105 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081
  1. package kubecost
  2. import (
  3. "encoding"
  4. "fmt"
  5. "math"
  6. "regexp"
  7. "strings"
  8. "time"
  9. filter21 "github.com/opencost/opencost/pkg/filter21"
  10. "github.com/opencost/opencost/pkg/filter21/ast"
  11. "github.com/opencost/opencost/pkg/filter21/matcher"
  12. "github.com/opencost/opencost/pkg/log"
  13. "github.com/opencost/opencost/pkg/util/json"
  14. "github.com/opencost/opencost/pkg/util/timeutil"
  15. )
  16. // UndefinedKey is used in composing Asset group keys if the group does not have that property defined.
  17. // E.g. if aggregating on Cluster, Assets in the AssetSet where Asset has no cluster will be grouped under key "__undefined__"
  18. const UndefinedKey = "__undefined__"
  19. // LocalStorageClass is used to assign storage class of local disks.
  20. const LocalStorageClass = "__local__"
  21. // UnknownStorageClass is used to assign storage class of persistent volume whose information is unable to be traced.
  22. const UnknownStorageClass = "__unknown__"
  23. // Asset defines an entity within a cluster that has a defined cost over a
  24. // given period of time.
  25. type Asset interface {
  26. // Type identifies the kind of Asset, which must always exist and should
  27. // be defined by the underlying type implementing the interface.
  28. Type() AssetType
  29. // GetProperties are a map of predefined traits, which may or may not exist,
  30. // but must conform to the AssetProperty schema
  31. GetProperties() *AssetProperties
  32. SetProperties(*AssetProperties)
  33. // GetLabels are a map of undefined string-to-string values
  34. GetLabels() AssetLabels
  35. SetLabels(AssetLabels)
  36. // Monetary values
  37. GetAdjustment() float64
  38. SetAdjustment(float64)
  39. TotalCost() float64
  40. // Temporal values
  41. GetStart() time.Time
  42. GetEnd() time.Time
  43. SetStartEnd(time.Time, time.Time)
  44. GetWindow() Window
  45. SetWindow(Window)
  46. ExpandWindow(Window)
  47. Minutes() float64
  48. // Operations and comparisons
  49. Add(Asset) Asset
  50. Clone() Asset
  51. Equal(Asset) bool
  52. SanitizeNaN()
  53. // Representations
  54. encoding.BinaryMarshaler
  55. encoding.BinaryUnmarshaler
  56. json.Marshaler
  57. fmt.Stringer
  58. }
  59. // AssetToExternalAllocation converts the given asset to an Allocation, given
  60. // the Properties to use to aggregate, and the mapping from Allocation property
  61. // to Asset label. For example, consider this asset:
  62. //
  63. // CURRENT: Asset ETL stores its data ALREADY MAPPED from label to k8s concept. This isn't ideal-- see the TODO.
  64. //
  65. // Cloud {
  66. // TotalCost: 10.00,
  67. // Labels{
  68. // "kubernetes_namespace":"monitoring",
  69. // "env":"prod"
  70. // }
  71. // }
  72. //
  73. // Given the following parameters, we expect to return:
  74. //
  75. // 1. single-prop full match
  76. // aggregateBy = ["namespace"]
  77. // => Allocation{Name: "monitoring", ExternalCost: 10.00, TotalCost: 10.00}, nil
  78. //
  79. // 2. multi-prop full match
  80. // aggregateBy = ["namespace", "label:env"]
  81. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  82. // => Allocation{Name: "monitoring/env=prod", ExternalCost: 10.00, TotalCost: 10.00}, nil
  83. //
  84. // 3. multi-prop partial match
  85. // aggregateBy = ["namespace", "label:foo"]
  86. // => Allocation{Name: "monitoring/__unallocated__", ExternalCost: 10.00, TotalCost: 10.00}, nil
  87. //
  88. // 4. no match
  89. // aggregateBy = ["cluster"]
  90. // => nil, err
  91. //
  92. // TODO:
  93. //
  94. // Cloud {
  95. // TotalCost: 10.00,
  96. // Labels{
  97. // "kubernetes_namespace":"monitoring",
  98. // "env":"prod"
  99. // }
  100. // }
  101. //
  102. // Given the following parameters, we expect to return:
  103. //
  104. // 1. single-prop full match
  105. // aggregateBy = ["namespace"]
  106. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  107. // => Allocation{Name: "monitoring", ExternalCost: 10.00, TotalCost: 10.00}, nil
  108. //
  109. // 2. multi-prop full match
  110. // aggregateBy = ["namespace", "label:env"]
  111. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  112. // => Allocation{Name: "monitoring/env=prod", ExternalCost: 10.00, TotalCost: 10.00}, nil
  113. //
  114. // 3. multi-prop partial match
  115. // aggregateBy = ["namespace", "label:foo"]
  116. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  117. // => Allocation{Name: "monitoring/__unallocated__", ExternalCost: 10.00, TotalCost: 10.00}, nil
  118. //
  119. // 4. no match
  120. // aggregateBy = ["cluster"]
  121. // allocationPropertyLabels = {"namespace":"kubernetes_namespace"}
  122. // => nil, err
  123. //
  124. // (See asset_test.go for assertions of these examples and more.)
  125. func AssetToExternalAllocation(asset Asset, aggregateBy []string, labelConfig *LabelConfig) (*Allocation, error) {
  126. if asset == nil {
  127. return nil, fmt.Errorf("asset is nil")
  128. }
  129. // Use default label config if one is not provided.
  130. if labelConfig == nil {
  131. labelConfig = NewLabelConfig()
  132. }
  133. // names will collect the slash-separated names accrued by iterating over
  134. // aggregateBy and checking the relevant labels.
  135. names := []string{}
  136. // match records whether or not a match was found in the Asset labels,
  137. // such that is can genuinely be turned into an external Allocation.
  138. match := false
  139. // props records the relevant Properties to set on the resultant Allocation
  140. props := AllocationProperties{}
  141. // For each aggregation parameter, try to find a match in the asset's
  142. // labels, using the labelConfig to translate. For an aggregation parameter
  143. // defined by a label (e.g. "label:app") this is simple: look for the label
  144. // and use it (e.g. if "app" is a defined label on the asset, then use its
  145. // value). For an aggregation parameter defined by a non-label property
  146. // (e.g. "namespace") this requires using the labelConfig to look up the
  147. // label name associated with that property and to use the value under that
  148. // label, if set (e.g. if the aggregation property is "namespace" and the
  149. // labelConfig is configured with "namespace_external_label" => "kubens"
  150. // and the asset has label "kubens":"kubecost", then file the asset as an
  151. // external cost under "kubecost").
  152. for _, aggBy := range aggregateBy {
  153. name := labelConfig.GetExternalAllocationName(asset.GetLabels(), aggBy)
  154. if name == "" {
  155. // No matching label has been defined in the cost-analyzer label config
  156. // relating to the given aggregateBy property.
  157. names = append(names, UnallocatedSuffix)
  158. continue
  159. } else {
  160. names = append(names, name)
  161. match = true
  162. // Default labels to an empty map, if necessary
  163. if props.Labels == nil {
  164. props.Labels = map[string]string{}
  165. }
  166. // Set the corresponding property on props
  167. switch aggBy {
  168. case AllocationClusterProp:
  169. props.Cluster = name
  170. case AllocationNodeProp:
  171. props.Node = name
  172. case AllocationNamespaceProp:
  173. props.Namespace = name
  174. case AllocationControllerKindProp:
  175. props.ControllerKind = name
  176. case AllocationControllerProp:
  177. props.Controller = name
  178. case AllocationPodProp:
  179. props.Pod = name
  180. case AllocationContainerProp:
  181. props.Container = name
  182. case AllocationServiceProp:
  183. props.Services = []string{name}
  184. case AllocationDeploymentProp:
  185. props.Controller = name
  186. props.ControllerKind = "deployment"
  187. case AllocationStatefulSetProp:
  188. props.Controller = name
  189. props.ControllerKind = "statefulset"
  190. case AllocationDaemonSetProp:
  191. props.Controller = name
  192. props.ControllerKind = "daemonset"
  193. case AllocationDepartmentProp:
  194. props.Labels[labelConfig.DepartmentLabel] = name
  195. case AllocationEnvironmentProp:
  196. props.Labels[labelConfig.EnvironmentLabel] = name
  197. case AllocationOwnerProp:
  198. props.Labels[labelConfig.OwnerLabel] = name
  199. case AllocationProductProp:
  200. props.Labels[labelConfig.ProductLabel] = name
  201. case AllocationTeamProp:
  202. props.Labels[labelConfig.TeamLabel] = name
  203. default:
  204. if strings.HasPrefix(aggBy, "label:") {
  205. // Set the corresponding label in props
  206. labelName := strings.TrimPrefix(aggBy, "label:")
  207. labelValue := strings.TrimPrefix(name, labelName+"=")
  208. props.Labels[labelName] = labelValue
  209. }
  210. }
  211. }
  212. }
  213. // If not a signle aggregation property generated a matching label property,
  214. // then consider the asset ineligible to be treated as an external allocation.
  215. if !match {
  216. return nil, fmt.Errorf("asset does not qualify as an external allocation")
  217. }
  218. // Use naming to label as an external allocation. See IsExternal() for more.
  219. names = append(names, ExternalSuffix)
  220. // TODO: external allocation: efficiency?
  221. // TODO: external allocation: resource totals?
  222. return &Allocation{
  223. Name: strings.Join(names, "/"),
  224. Properties: &props,
  225. Window: asset.GetWindow().Clone(),
  226. Start: asset.GetStart(),
  227. End: asset.GetEnd(),
  228. ExternalCost: asset.TotalCost(),
  229. }, nil
  230. }
  231. // key is used to determine uniqueness of an Asset, for instance during Insert
  232. // to determine if two Assets should be combined. Passing `nil` `aggregateBy` indicates
  233. // that all available `AssetProperty` keys should be used. Passing empty `aggregateBy` indicates that
  234. // no key should be used (e.g. to aggregate all assets). Passing one or more `aggregateBy`
  235. // values will key by only those values.
  236. // Valid values of `aggregateBy` elements are strings which are an `AssetProperty`, and strings prefixed
  237. // with `"label:"`.
  238. func key(a Asset, aggregateBy []string, labelConfig *LabelConfig) (string, error) {
  239. var buffer strings.Builder
  240. if labelConfig == nil {
  241. labelConfig = NewLabelConfig()
  242. }
  243. if aggregateBy == nil {
  244. aggregateBy = []string{
  245. string(AssetProviderProp),
  246. string(AssetAccountProp),
  247. string(AssetProjectProp),
  248. string(AssetCategoryProp),
  249. string(AssetClusterProp),
  250. string(AssetTypeProp),
  251. string(AssetServiceProp),
  252. string(AssetProviderIDProp),
  253. string(AssetNameProp),
  254. }
  255. }
  256. for i, s := range aggregateBy {
  257. key := ""
  258. switch true {
  259. case s == string(AssetProviderProp):
  260. key = a.GetProperties().Provider
  261. case s == string(AssetAccountProp):
  262. key = a.GetProperties().Account
  263. case s == string(AssetProjectProp):
  264. key = a.GetProperties().Project
  265. case s == string(AssetClusterProp):
  266. key = a.GetProperties().Cluster
  267. case s == string(AssetCategoryProp):
  268. key = a.GetProperties().Category
  269. case s == string(AssetTypeProp):
  270. key = a.Type().String()
  271. case s == string(AssetServiceProp):
  272. key = a.GetProperties().Service
  273. case s == string(AssetProviderIDProp):
  274. key = a.GetProperties().ProviderID
  275. case s == string(AssetNameProp):
  276. key = a.GetProperties().Name
  277. case s == string(AssetDepartmentProp):
  278. key = getKeyFromLabelConfig(a, labelConfig, labelConfig.DepartmentExternalLabel)
  279. case s == string(AssetEnvironmentProp):
  280. key = getKeyFromLabelConfig(a, labelConfig, labelConfig.EnvironmentExternalLabel)
  281. case s == string(AssetOwnerProp):
  282. key = getKeyFromLabelConfig(a, labelConfig, labelConfig.OwnerExternalLabel)
  283. case s == string(AssetProductProp):
  284. key = getKeyFromLabelConfig(a, labelConfig, labelConfig.ProductExternalLabel)
  285. case s == string(AssetTeamProp):
  286. key = getKeyFromLabelConfig(a, labelConfig, labelConfig.TeamExternalLabel)
  287. case strings.HasPrefix(s, "label:"):
  288. if labelKey := strings.TrimPrefix(s, "label:"); labelKey != "" {
  289. labelVal := a.GetLabels()[labelKey]
  290. if labelVal == "" {
  291. key = "__undefined__"
  292. } else {
  293. key = fmt.Sprintf("%s=%s", labelKey, labelVal)
  294. }
  295. } else {
  296. // Don't allow aggregating on label ""
  297. return "", fmt.Errorf("attempted to aggregate on invalid key: %s", s)
  298. }
  299. default:
  300. return "", fmt.Errorf("attempted to aggregate on invalid key: %s", s)
  301. }
  302. if key != "" {
  303. buffer.WriteString(key)
  304. } else {
  305. buffer.WriteString(UndefinedKey)
  306. }
  307. if i != (len(aggregateBy) - 1) {
  308. buffer.WriteString("/")
  309. }
  310. }
  311. return buffer.String(), nil
  312. }
  313. func getKeyFromLabelConfig(a Asset, labelConfig *LabelConfig, label string) string {
  314. labels := a.GetLabels()
  315. if labels == nil {
  316. return UnallocatedSuffix
  317. } else {
  318. key := UnallocatedSuffix
  319. labelNames := strings.Split(label, ",")
  320. for _, labelName := range labelNames {
  321. name := labelConfig.Sanitize(labelName)
  322. if labelValue, ok := labels[name]; ok {
  323. key = labelValue
  324. break
  325. }
  326. }
  327. return key
  328. }
  329. }
  330. func GetAssetKey(a Asset, aggregateBy []string) (string, error) {
  331. return key(a, aggregateBy, nil)
  332. }
  333. func toString(a Asset) string {
  334. return fmt.Sprintf("%s{%s}%s=%.2f", a.Type().String(), a.GetProperties(), a.GetWindow(), a.TotalCost())
  335. }
  336. // AssetLabels is a schema-free mapping of key/value pairs that can be
  337. // attributed to an Asset as a flexible a
  338. type AssetLabels map[string]string
  339. // Clone returns a cloned map of labels
  340. func (al AssetLabels) Clone() AssetLabels {
  341. clone := make(AssetLabels, len(al))
  342. for label, value := range al {
  343. clone[label] = value
  344. }
  345. return clone
  346. }
  347. // Equal returns true only if the two set of labels are exact matches
  348. func (al AssetLabels) Equal(that AssetLabels) bool {
  349. if len(al) != len(that) {
  350. return false
  351. }
  352. for label, value := range al {
  353. if thatValue, ok := that[label]; !ok || thatValue != value {
  354. return false
  355. }
  356. }
  357. return true
  358. }
  359. // Merge retains only the labels shared with the given AssetLabels
  360. func (al AssetLabels) Merge(that AssetLabels) AssetLabels {
  361. result := AssetLabels{}
  362. for label, value := range al {
  363. if thatValue, ok := that[label]; ok && thatValue == value {
  364. result[label] = value
  365. }
  366. }
  367. return result
  368. }
  369. // Append joins AssetLabels with a given map of labels
  370. func (al AssetLabels) Append(newLabels map[string]string, overwrite bool) {
  371. if len(newLabels) == 0 {
  372. return
  373. }
  374. for label, value := range newLabels {
  375. if _, ok := al[label]; ok {
  376. if overwrite {
  377. al[label] = value
  378. }
  379. } else {
  380. al[label] = value
  381. }
  382. }
  383. }
  384. // AssetType identifies a type of Asset
  385. type AssetType int
  386. const (
  387. // AnyAssetType describes the Any AssetType
  388. AnyAssetType AssetType = iota
  389. // CloudAssetType describes the Cloud AssetType
  390. CloudAssetType
  391. // ClusterManagementAssetType describes the ClusterManagement AssetType
  392. ClusterManagementAssetType
  393. // DiskAssetType describes the Disk AssetType
  394. DiskAssetType
  395. // LoadBalancerAssetType describes the LoadBalancer AssetType
  396. LoadBalancerAssetType
  397. // NetworkAssetType describes the Network AssetType
  398. NetworkAssetType
  399. // NodeAssetType describes the Node AssetType
  400. NodeAssetType
  401. // SharedAssetType describes the Shared AssetType
  402. SharedAssetType
  403. )
  404. // ParseAssetType attempts to parse the given string into an AssetType
  405. func ParseAssetType(text string) (AssetType, error) {
  406. switch strings.TrimSpace(strings.ToLower(text)) {
  407. case "cloud":
  408. return CloudAssetType, nil
  409. case "clustermanagement":
  410. return ClusterManagementAssetType, nil
  411. case "disk":
  412. return DiskAssetType, nil
  413. case "loadbalancer":
  414. return LoadBalancerAssetType, nil
  415. case "network":
  416. return NetworkAssetType, nil
  417. case "node":
  418. return NodeAssetType, nil
  419. case "shared":
  420. return SharedAssetType, nil
  421. }
  422. return AnyAssetType, fmt.Errorf("invalid asset type: %s", text)
  423. }
  424. // String converts the given AssetType to a string
  425. func (at AssetType) String() string {
  426. return [...]string{
  427. "Asset",
  428. "Cloud",
  429. "ClusterManagement",
  430. "Disk",
  431. "LoadBalancer",
  432. "Network",
  433. "Node",
  434. "Shared",
  435. }[at]
  436. }
  437. // Any is the most general Asset, which is usually created as a result of
  438. // adding two Assets of different types.
  439. type Any struct {
  440. Labels AssetLabels
  441. Properties *AssetProperties
  442. Start time.Time
  443. End time.Time
  444. Window Window
  445. Adjustment float64
  446. Cost float64
  447. }
  448. // NewAsset creates a new Any-type Asset for the given period of time
  449. func NewAsset(start, end time.Time, window Window) *Any {
  450. return &Any{
  451. Labels: AssetLabels{},
  452. Properties: &AssetProperties{},
  453. Start: start,
  454. End: end,
  455. Window: window.Clone(),
  456. }
  457. }
  458. // Type returns the Asset's type
  459. func (a *Any) Type() AssetType {
  460. return AnyAssetType
  461. }
  462. // Properties returns the Asset's Properties
  463. func (a *Any) GetProperties() *AssetProperties {
  464. return a.Properties
  465. }
  466. // SetProperties sets the Asset's Properties
  467. func (a *Any) SetProperties(props *AssetProperties) {
  468. a.Properties = props
  469. }
  470. // Labels returns the Asset's labels
  471. func (a *Any) GetLabels() AssetLabels {
  472. return a.Labels
  473. }
  474. // SetLabels sets the Asset's labels
  475. func (a *Any) SetLabels(labels AssetLabels) {
  476. a.Labels = labels
  477. }
  478. // Adjustment returns the Asset's cost adjustment
  479. func (a *Any) GetAdjustment() float64 {
  480. return a.Adjustment
  481. }
  482. // SetAdjustment sets the Asset's cost adjustment
  483. func (a *Any) SetAdjustment(adj float64) {
  484. a.Adjustment = adj
  485. }
  486. // TotalCost returns the Asset's TotalCost
  487. func (a *Any) TotalCost() float64 {
  488. return a.Cost + a.Adjustment
  489. }
  490. // Start returns the Asset's start time within the window
  491. func (a *Any) GetStart() time.Time {
  492. return a.Start
  493. }
  494. // End returns the Asset's end time within the window
  495. func (a *Any) GetEnd() time.Time {
  496. return a.End
  497. }
  498. // Minutes returns the number of minutes the Asset was active within the window
  499. func (a *Any) Minutes() float64 {
  500. return a.End.Sub(a.Start).Minutes()
  501. }
  502. // Window returns the Asset's window
  503. func (a *Any) GetWindow() Window {
  504. return a.Window
  505. }
  506. func (a *Any) SetWindow(window Window) {
  507. a.Window = window
  508. }
  509. // ExpandWindow expands the Asset's window by the given window
  510. func (a *Any) ExpandWindow(window Window) {
  511. a.Window = a.Window.Expand(window)
  512. }
  513. // SetStartEnd sets the Asset's Start and End fields
  514. func (a *Any) SetStartEnd(start, end time.Time) {
  515. if a.Window.Contains(start) {
  516. a.Start = start
  517. } else {
  518. log.Warnf("Any.SetStartEnd: start %s not in %s", start, a.Window)
  519. }
  520. if a.Window.Contains(end) {
  521. a.End = end
  522. } else {
  523. log.Warnf("Any.SetStartEnd: end %s not in %s", end, a.Window)
  524. }
  525. }
  526. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  527. // as much relevant information as possible (i.e. type, Properties, labels).
  528. func (a *Any) Add(that Asset) Asset {
  529. this := a.Clone().(*Any)
  530. props := a.Properties.Merge(that.GetProperties())
  531. labels := a.Labels.Merge(that.GetLabels())
  532. start := a.Start
  533. if that.GetStart().Before(start) {
  534. start = that.GetStart()
  535. }
  536. end := a.End
  537. if that.GetEnd().After(end) {
  538. end = that.GetEnd()
  539. }
  540. window := a.Window.Expand(that.GetWindow())
  541. this.Start = start
  542. this.End = end
  543. this.Window = window
  544. this.SetProperties(props)
  545. this.SetLabels(labels)
  546. this.Adjustment += that.GetAdjustment()
  547. this.Cost += (that.TotalCost() - that.GetAdjustment())
  548. return this
  549. }
  550. // Clone returns a cloned instance of the Asset
  551. func (a *Any) Clone() Asset {
  552. return &Any{
  553. Labels: a.Labels.Clone(),
  554. Properties: a.Properties.Clone(),
  555. Start: a.Start,
  556. End: a.End,
  557. Window: a.Window.Clone(),
  558. Adjustment: a.Adjustment,
  559. Cost: a.Cost,
  560. }
  561. }
  562. // Equal returns true if the given Asset is an exact match of the receiver
  563. func (a *Any) Equal(that Asset) bool {
  564. t, ok := that.(*Any)
  565. if !ok {
  566. return false
  567. }
  568. if !a.Labels.Equal(t.Labels) {
  569. return false
  570. }
  571. if !a.Properties.Equal(t.Properties) {
  572. return false
  573. }
  574. if !a.Start.Equal(t.Start) {
  575. return false
  576. }
  577. if !a.End.Equal(t.End) {
  578. return false
  579. }
  580. if !a.Window.Equal(t.Window) {
  581. return false
  582. }
  583. if a.Cost != t.Cost {
  584. return false
  585. }
  586. return true
  587. }
  588. // String implements fmt.Stringer
  589. func (a *Any) String() string {
  590. return toString(a)
  591. }
  592. func (a *Any) SanitizeNaN() {
  593. if a == nil {
  594. return
  595. }
  596. if math.IsNaN(a.Adjustment) {
  597. log.DedupedWarningf(5, "Any: Unexpected NaN found for Adjustment: labels:%v, window:%s, properties:%s", a.Labels, a.Window.String(), a.Properties.String())
  598. a.Adjustment = 0
  599. }
  600. if math.IsNaN(a.Cost) {
  601. log.DedupedWarningf(5, "Any: Unexpected NaN found for Cost: name:%v, window:%s, properties:%s", a.Labels, a.Window.String(), a.Properties.String())
  602. a.Cost = 0
  603. }
  604. }
  605. // Cloud describes a cloud asset
  606. type Cloud struct {
  607. Labels AssetLabels
  608. Properties *AssetProperties
  609. Start time.Time
  610. End time.Time
  611. Window Window
  612. Adjustment float64
  613. Cost float64
  614. Credit float64 // Credit is a negative value representing dollars credited back to a given line-item
  615. }
  616. // NewCloud returns a new Cloud Asset
  617. func NewCloud(category, providerID string, start, end time.Time, window Window) *Cloud {
  618. properties := &AssetProperties{
  619. Category: category,
  620. ProviderID: providerID,
  621. }
  622. return &Cloud{
  623. Labels: AssetLabels{},
  624. Properties: properties,
  625. Start: start,
  626. End: end,
  627. Window: window.Clone(),
  628. }
  629. }
  630. // Type returns the AssetType
  631. func (ca *Cloud) Type() AssetType {
  632. return CloudAssetType
  633. }
  634. // Properties returns the AssetProperties
  635. func (ca *Cloud) GetProperties() *AssetProperties {
  636. return ca.Properties
  637. }
  638. // SetProperties sets the Asset's Properties
  639. func (ca *Cloud) SetProperties(props *AssetProperties) {
  640. ca.Properties = props
  641. }
  642. // Labels returns the AssetLabels
  643. func (ca *Cloud) GetLabels() AssetLabels {
  644. return ca.Labels
  645. }
  646. // SetLabels sets the Asset's labels
  647. func (ca *Cloud) SetLabels(labels AssetLabels) {
  648. ca.Labels = labels
  649. }
  650. // Adjustment returns the Asset's adjustment value
  651. func (ca *Cloud) GetAdjustment() float64 {
  652. return ca.Adjustment
  653. }
  654. // SetAdjustment sets the Asset's adjustment value
  655. func (ca *Cloud) SetAdjustment(adj float64) {
  656. ca.Adjustment = adj
  657. }
  658. // TotalCost returns the Asset's total cost
  659. func (ca *Cloud) TotalCost() float64 {
  660. return ca.Cost + ca.Adjustment + ca.Credit
  661. }
  662. // Start returns the Asset's precise start time within the window
  663. func (ca *Cloud) GetStart() time.Time {
  664. return ca.Start
  665. }
  666. // End returns the Asset's precise end time within the window
  667. func (ca *Cloud) GetEnd() time.Time {
  668. return ca.End
  669. }
  670. // Minutes returns the number of Minutes the Asset ran
  671. func (ca *Cloud) Minutes() float64 {
  672. return ca.End.Sub(ca.Start).Minutes()
  673. }
  674. // Window returns the window within which the Asset ran
  675. func (ca *Cloud) GetWindow() Window {
  676. return ca.Window
  677. }
  678. func (ca *Cloud) SetWindow(window Window) {
  679. ca.Window = window
  680. }
  681. // ExpandWindow expands the Asset's window by the given window
  682. func (ca *Cloud) ExpandWindow(window Window) {
  683. ca.Window = ca.Window.Expand(window)
  684. }
  685. // SetStartEnd sets the Asset's Start and End fields
  686. func (ca *Cloud) SetStartEnd(start, end time.Time) {
  687. if ca.Window.Contains(start) {
  688. ca.Start = start
  689. } else {
  690. log.Warnf("Cloud.SetStartEnd: start %s not in %s", start, ca.Window)
  691. }
  692. if ca.Window.Contains(end) {
  693. ca.End = end
  694. } else {
  695. log.Warnf("Cloud.SetStartEnd: end %s not in %s", end, ca.Window)
  696. }
  697. }
  698. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  699. // as much relevant information as possible (i.e. type, Properties, labels).
  700. func (ca *Cloud) Add(a Asset) Asset {
  701. // Cloud + Cloud = Cloud
  702. if that, ok := a.(*Cloud); ok {
  703. this := ca.Clone().(*Cloud)
  704. this.add(that)
  705. return this
  706. }
  707. props := ca.Properties.Merge(a.GetProperties())
  708. labels := ca.Labels.Merge(a.GetLabels())
  709. start := ca.Start
  710. if a.GetStart().Before(start) {
  711. start = a.GetStart()
  712. }
  713. end := ca.End
  714. if a.GetEnd().After(end) {
  715. end = a.GetEnd()
  716. }
  717. window := ca.Window.Expand(a.GetWindow())
  718. // Cloud + !Cloud = Any
  719. any := NewAsset(start, end, window)
  720. any.SetProperties(props)
  721. any.SetLabels(labels)
  722. any.Adjustment = ca.Adjustment + a.GetAdjustment()
  723. any.Cost = (ca.TotalCost() - ca.Adjustment) + (a.TotalCost() - a.GetAdjustment())
  724. return any
  725. }
  726. func (ca *Cloud) add(that *Cloud) {
  727. if ca == nil {
  728. ca = that
  729. return
  730. }
  731. props := ca.Properties.Merge(that.Properties)
  732. labels := ca.Labels.Merge(that.Labels)
  733. start := ca.Start
  734. if that.Start.Before(start) {
  735. start = that.Start
  736. }
  737. end := ca.End
  738. if that.End.After(end) {
  739. end = that.End
  740. }
  741. window := ca.Window.Expand(that.Window)
  742. ca.Start = start
  743. ca.End = end
  744. ca.Window = window
  745. ca.SetProperties(props)
  746. ca.SetLabels(labels)
  747. ca.Adjustment += that.Adjustment
  748. ca.Cost += that.Cost
  749. ca.Credit += that.Credit
  750. }
  751. // Clone returns a cloned instance of the Asset
  752. func (ca *Cloud) Clone() Asset {
  753. return &Cloud{
  754. Labels: ca.Labels.Clone(),
  755. Properties: ca.Properties.Clone(),
  756. Start: ca.Start,
  757. End: ca.End,
  758. Window: ca.Window.Clone(),
  759. Adjustment: ca.Adjustment,
  760. Cost: ca.Cost,
  761. Credit: ca.Credit,
  762. }
  763. }
  764. // Equal returns true if the given Asset precisely equals the Asset
  765. func (ca *Cloud) Equal(a Asset) bool {
  766. that, ok := a.(*Cloud)
  767. if !ok {
  768. return false
  769. }
  770. if !ca.Labels.Equal(that.Labels) {
  771. return false
  772. }
  773. if !ca.Properties.Equal(that.Properties) {
  774. return false
  775. }
  776. if !ca.Start.Equal(that.Start) {
  777. return false
  778. }
  779. if !ca.End.Equal(that.End) {
  780. return false
  781. }
  782. if !ca.Window.Equal(that.Window) {
  783. return false
  784. }
  785. if ca.Adjustment != that.Adjustment {
  786. return false
  787. }
  788. if ca.Cost != that.Cost {
  789. return false
  790. }
  791. if ca.Credit != that.Credit {
  792. return false
  793. }
  794. return true
  795. }
  796. // String implements fmt.Stringer
  797. func (ca *Cloud) String() string {
  798. return toString(ca)
  799. }
  800. func (ca *Cloud) SanitizeNaN() {
  801. if ca == nil {
  802. return
  803. }
  804. if math.IsNaN(ca.Adjustment) {
  805. log.DedupedWarningf(5, "Cloud: Unexpected NaN found for Adjustment: labels:%v, window:%s, properties:%s", ca.Labels, ca.Window.String(), ca.Properties.String())
  806. ca.Adjustment = 0
  807. }
  808. if math.IsNaN(ca.Cost) {
  809. log.DedupedWarningf(5, "Cloud: Unexpected NaN found for Cost: name:%v, window:%s, properties:%s", ca.Labels, ca.Window.String(), ca.Properties.String())
  810. ca.Cost = 0
  811. }
  812. if math.IsNaN(ca.Credit) {
  813. log.DedupedWarningf(5, "Cloud: Unexpected NaN found for Credit: name:%v, window:%s, properties:%s", ca.Labels, ca.Window.String(), ca.Properties.String())
  814. ca.Credit = 0
  815. }
  816. }
  817. // ClusterManagement describes a provider's cluster management fee
  818. type ClusterManagement struct {
  819. Labels AssetLabels
  820. Properties *AssetProperties
  821. Window Window
  822. Cost float64
  823. Adjustment float64 // @bingen:field[version=16]
  824. }
  825. // NewClusterManagement creates and returns a new ClusterManagement instance
  826. func NewClusterManagement(provider, cluster string, window Window) *ClusterManagement {
  827. properties := &AssetProperties{
  828. Category: ManagementCategory,
  829. Provider: ParseProvider(provider),
  830. Cluster: cluster,
  831. Service: KubernetesService,
  832. }
  833. return &ClusterManagement{
  834. Labels: AssetLabels{},
  835. Properties: properties,
  836. Window: window.Clone(),
  837. }
  838. }
  839. // Type returns the Asset's type
  840. func (cm *ClusterManagement) Type() AssetType {
  841. return ClusterManagementAssetType
  842. }
  843. // Properties returns the Asset's Properties
  844. func (cm *ClusterManagement) GetProperties() *AssetProperties {
  845. return cm.Properties
  846. }
  847. // SetProperties sets the Asset's Properties
  848. func (cm *ClusterManagement) SetProperties(props *AssetProperties) {
  849. cm.Properties = props
  850. }
  851. // Labels returns the Asset's labels
  852. func (cm *ClusterManagement) GetLabels() AssetLabels {
  853. return cm.Labels
  854. }
  855. // SetLabels sets the Asset's Properties
  856. func (cm *ClusterManagement) SetLabels(props AssetLabels) {
  857. cm.Labels = props
  858. }
  859. // Adjustment does not apply to ClusterManagement
  860. func (cm *ClusterManagement) GetAdjustment() float64 {
  861. return cm.Adjustment
  862. }
  863. // SetAdjustment does not apply to ClusterManagement
  864. func (cm *ClusterManagement) SetAdjustment(adj float64) {
  865. cm.Adjustment = adj
  866. }
  867. // TotalCost returns the Asset's total cost
  868. func (cm *ClusterManagement) TotalCost() float64 {
  869. return cm.Cost + cm.Adjustment
  870. }
  871. // Start returns the Asset's precise start time within the window
  872. func (cm *ClusterManagement) GetStart() time.Time {
  873. return *cm.Window.Start()
  874. }
  875. // End returns the Asset's precise end time within the window
  876. func (cm *ClusterManagement) GetEnd() time.Time {
  877. return *cm.Window.End()
  878. }
  879. // Minutes returns the number of minutes the Asset ran
  880. func (cm *ClusterManagement) Minutes() float64 {
  881. return cm.Window.Minutes()
  882. }
  883. // Window return the Asset's window
  884. func (cm *ClusterManagement) GetWindow() Window {
  885. return cm.Window
  886. }
  887. func (cm *ClusterManagement) SetWindow(window Window) {
  888. cm.Window = window
  889. }
  890. // ExpandWindow expands the Asset's window by the given window
  891. func (cm *ClusterManagement) ExpandWindow(window Window) {
  892. cm.Window = cm.Window.Expand(window)
  893. }
  894. // SetStartEnd sets the Asset's Start and End fields (not applicable here)
  895. func (cm *ClusterManagement) SetStartEnd(start, end time.Time) {
  896. return
  897. }
  898. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  899. // as much relevant information as possible (i.e. type, Properties, labels).
  900. func (cm *ClusterManagement) Add(a Asset) Asset {
  901. // ClusterManagement + ClusterManagement = ClusterManagement
  902. if that, ok := a.(*ClusterManagement); ok {
  903. this := cm.Clone().(*ClusterManagement)
  904. this.add(that)
  905. return this
  906. }
  907. props := cm.Properties.Merge(a.GetProperties())
  908. labels := cm.Labels.Merge(a.GetLabels())
  909. start := cm.GetStart()
  910. if a.GetStart().Before(start) {
  911. start = a.GetStart()
  912. }
  913. end := cm.GetEnd()
  914. if a.GetEnd().After(end) {
  915. end = a.GetEnd()
  916. }
  917. window := cm.Window.Expand(a.GetWindow())
  918. // ClusterManagement + !ClusterManagement = Any
  919. any := NewAsset(start, end, window)
  920. any.SetProperties(props)
  921. any.SetLabels(labels)
  922. any.Adjustment = cm.Adjustment + a.GetAdjustment()
  923. any.Cost = (cm.TotalCost() - cm.Adjustment) + (a.TotalCost() - a.GetAdjustment())
  924. return any
  925. }
  926. func (cm *ClusterManagement) add(that *ClusterManagement) {
  927. if cm == nil {
  928. cm = that
  929. return
  930. }
  931. props := cm.Properties.Merge(that.Properties)
  932. labels := cm.Labels.Merge(that.Labels)
  933. window := cm.Window.Expand(that.Window)
  934. cm.Window = window
  935. cm.SetProperties(props)
  936. cm.SetLabels(labels)
  937. cm.Adjustment += that.Adjustment
  938. cm.Cost += that.Cost
  939. }
  940. // Clone returns a cloned instance of the Asset
  941. func (cm *ClusterManagement) Clone() Asset {
  942. return &ClusterManagement{
  943. Labels: cm.Labels.Clone(),
  944. Properties: cm.Properties.Clone(),
  945. Window: cm.Window.Clone(),
  946. Adjustment: cm.Adjustment,
  947. Cost: cm.Cost,
  948. }
  949. }
  950. // Equal returns true if the given Asset exactly matches the Asset
  951. func (cm *ClusterManagement) Equal(a Asset) bool {
  952. that, ok := a.(*ClusterManagement)
  953. if !ok {
  954. return false
  955. }
  956. if !cm.Labels.Equal(that.Labels) {
  957. return false
  958. }
  959. if !cm.Properties.Equal(that.Properties) {
  960. return false
  961. }
  962. if !cm.Window.Equal(that.Window) {
  963. return false
  964. }
  965. if cm.Adjustment != that.Adjustment {
  966. return false
  967. }
  968. if cm.Cost != that.Cost {
  969. return false
  970. }
  971. return true
  972. }
  973. // String implements fmt.Stringer
  974. func (cm *ClusterManagement) String() string {
  975. return toString(cm)
  976. }
  977. func (cm *ClusterManagement) SanitizeNaN() {
  978. if cm == nil {
  979. return
  980. }
  981. if math.IsNaN(cm.Adjustment) {
  982. log.DedupedWarningf(5, "ClusterManagement: Unexpected NaN found for Adjustment: labels:%v, window:%s, properties:%s", cm.Labels, cm.Window.String(), cm.Properties.String())
  983. cm.Adjustment = 0
  984. }
  985. if math.IsNaN(cm.Cost) {
  986. log.DedupedWarningf(5, "ClusterManagement: Unexpected NaN found for Cost: name:%v, window:%s, properties:%s", cm.Labels, cm.Window.String(), cm.Properties.String())
  987. cm.Cost = 0
  988. }
  989. }
  990. // Disk represents an in-cluster disk Asset
  991. type Disk struct {
  992. Labels AssetLabels
  993. Properties *AssetProperties
  994. Start time.Time
  995. End time.Time
  996. Window Window
  997. Adjustment float64
  998. Cost float64
  999. ByteHours float64
  1000. Local float64
  1001. Breakdown *Breakdown
  1002. StorageClass string // @bingen:field[version=17]
  1003. ByteHoursUsed *float64 // @bingen:field[version=18]
  1004. ByteUsageMax *float64 // @bingen:field[version=18]
  1005. VolumeName string // @bingen:field[version=18]
  1006. ClaimName string // @bingen:field[version=18]
  1007. ClaimNamespace string // @bingen:field[version=18]
  1008. }
  1009. // NewDisk creates and returns a new Disk Asset
  1010. func NewDisk(name, cluster, providerID string, start, end time.Time, window Window) *Disk {
  1011. properties := &AssetProperties{
  1012. Category: StorageCategory,
  1013. Name: name,
  1014. Cluster: cluster,
  1015. ProviderID: providerID,
  1016. Service: KubernetesService,
  1017. }
  1018. return &Disk{
  1019. Labels: AssetLabels{},
  1020. Properties: properties,
  1021. Start: start,
  1022. End: end,
  1023. Window: window,
  1024. Breakdown: &Breakdown{},
  1025. }
  1026. }
  1027. // Type returns the AssetType of the Asset
  1028. func (d *Disk) Type() AssetType {
  1029. return DiskAssetType
  1030. }
  1031. // Properties returns the Asset's Properties
  1032. func (d *Disk) GetProperties() *AssetProperties {
  1033. return d.Properties
  1034. }
  1035. // SetProperties sets the Asset's Properties
  1036. func (d *Disk) SetProperties(props *AssetProperties) {
  1037. d.Properties = props
  1038. }
  1039. // Labels returns the Asset's labels
  1040. func (d *Disk) GetLabels() AssetLabels {
  1041. return d.Labels
  1042. }
  1043. // SetLabels sets the Asset's labels
  1044. func (d *Disk) SetLabels(labels AssetLabels) {
  1045. d.Labels = labels
  1046. }
  1047. // Adjustment returns the Asset's cost adjustment
  1048. func (d *Disk) GetAdjustment() float64 {
  1049. return d.Adjustment
  1050. }
  1051. // SetAdjustment sets the Asset's cost adjustment
  1052. func (d *Disk) SetAdjustment(adj float64) {
  1053. d.Adjustment = adj
  1054. }
  1055. // TotalCost returns the Asset's total cost
  1056. func (d *Disk) TotalCost() float64 {
  1057. return d.Cost + d.Adjustment
  1058. }
  1059. // Start returns the precise start time of the Asset within the window
  1060. func (d *Disk) GetStart() time.Time {
  1061. return d.Start
  1062. }
  1063. // End returns the precise start time of the Asset within the window
  1064. func (d *Disk) GetEnd() time.Time {
  1065. return d.End
  1066. }
  1067. // Minutes returns the number of minutes the Asset ran
  1068. func (d *Disk) Minutes() float64 {
  1069. diskMins := d.End.Sub(d.Start).Minutes()
  1070. windowMins := d.Window.Minutes()
  1071. if diskMins > windowMins {
  1072. log.Warnf("Asset ETL: Disk.Minutes exceeds window: %.2f > %.2f", diskMins, windowMins)
  1073. diskMins = windowMins
  1074. }
  1075. if diskMins < 0 {
  1076. diskMins = 0
  1077. }
  1078. return diskMins
  1079. }
  1080. // Window returns the window within which the Asset
  1081. func (d *Disk) GetWindow() Window {
  1082. return d.Window
  1083. }
  1084. func (d *Disk) SetWindow(window Window) {
  1085. d.Window = window
  1086. }
  1087. // ExpandWindow expands the Asset's window by the given window
  1088. func (d *Disk) ExpandWindow(window Window) {
  1089. d.Window = d.Window.Expand(window)
  1090. }
  1091. // SetStartEnd sets the Asset's Start and End fields
  1092. func (d *Disk) SetStartEnd(start, end time.Time) {
  1093. if d.Window.Contains(start) {
  1094. d.Start = start
  1095. } else {
  1096. log.Warnf("Disk.SetStartEnd: start %s not in %s", start, d.Window)
  1097. }
  1098. if d.Window.Contains(end) {
  1099. d.End = end
  1100. } else {
  1101. log.Warnf("Disk.SetStartEnd: end %s not in %s", end, d.Window)
  1102. }
  1103. }
  1104. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  1105. // as much relevant information as possible (i.e. type, Properties, labels).
  1106. func (d *Disk) Add(a Asset) Asset {
  1107. // Disk + Disk = Disk
  1108. if that, ok := a.(*Disk); ok {
  1109. this := d.Clone().(*Disk)
  1110. this.add(that)
  1111. return this
  1112. }
  1113. props := d.Properties.Merge(a.GetProperties())
  1114. labels := d.Labels.Merge(a.GetLabels())
  1115. start := d.Start
  1116. if a.GetStart().Before(start) {
  1117. start = a.GetStart()
  1118. }
  1119. end := d.End
  1120. if a.GetEnd().After(end) {
  1121. end = a.GetEnd()
  1122. }
  1123. window := d.Window.Expand(a.GetWindow())
  1124. // Disk + !Disk = Any
  1125. any := NewAsset(start, end, window)
  1126. any.SetProperties(props)
  1127. any.SetLabels(labels)
  1128. any.Adjustment = d.Adjustment + a.GetAdjustment()
  1129. any.Cost = (d.TotalCost() - d.Adjustment) + (a.TotalCost() - a.GetAdjustment())
  1130. return any
  1131. }
  1132. func (d *Disk) add(that *Disk) {
  1133. if d == nil {
  1134. d = that
  1135. return
  1136. }
  1137. props := d.Properties.Merge(that.Properties)
  1138. labels := d.Labels.Merge(that.Labels)
  1139. d.SetProperties(props)
  1140. d.SetLabels(labels)
  1141. start := d.Start
  1142. if that.Start.Before(start) {
  1143. start = that.Start
  1144. }
  1145. end := d.End
  1146. if that.End.After(end) {
  1147. end = that.End
  1148. }
  1149. window := d.Window.Expand(that.Window)
  1150. d.Start = start
  1151. d.End = end
  1152. d.Window = window
  1153. totalCost := d.Cost + that.Cost
  1154. if totalCost > 0.0 {
  1155. d.Breakdown.Idle = (d.Breakdown.Idle*d.Cost + that.Breakdown.Idle*that.Cost) / totalCost
  1156. d.Breakdown.Other = (d.Breakdown.Other*d.Cost + that.Breakdown.Other*that.Cost) / totalCost
  1157. d.Breakdown.System = (d.Breakdown.System*d.Cost + that.Breakdown.System*that.Cost) / totalCost
  1158. d.Breakdown.User = (d.Breakdown.User*d.Cost + that.Breakdown.User*that.Cost) / totalCost
  1159. d.Local = (d.TotalCost()*d.Local + that.TotalCost()*that.Local) / (d.TotalCost() + that.TotalCost())
  1160. } else {
  1161. d.Local = (d.Local + that.Local) / 2.0
  1162. }
  1163. d.Adjustment += that.Adjustment
  1164. d.Cost += that.Cost
  1165. d.ByteHours += that.ByteHours
  1166. if d.ByteHoursUsed == nil && that.ByteHoursUsed != nil {
  1167. copy := *that.ByteHoursUsed
  1168. d.ByteHoursUsed = &copy
  1169. } else if d.ByteHoursUsed != nil && that.ByteHoursUsed == nil {
  1170. // do nothing
  1171. } else if d.ByteHoursUsed != nil && that.ByteHoursUsed != nil {
  1172. sum := *d.ByteHoursUsed
  1173. sum += *that.ByteHoursUsed
  1174. d.ByteHoursUsed = &sum
  1175. }
  1176. // We have to nil out the max because we don't know if we're
  1177. // aggregating across time our properties. See RawAllocationOnly on
  1178. // Allocation for further reference.
  1179. d.ByteUsageMax = nil
  1180. // If storage class don't match default it to empty storage class
  1181. if d.StorageClass != that.StorageClass {
  1182. d.StorageClass = ""
  1183. }
  1184. if d.VolumeName != that.VolumeName {
  1185. d.VolumeName = ""
  1186. }
  1187. if d.ClaimName != that.ClaimName {
  1188. d.ClaimName = ""
  1189. }
  1190. if d.ClaimNamespace != that.ClaimNamespace {
  1191. d.ClaimNamespace = ""
  1192. }
  1193. }
  1194. // Clone returns a cloned instance of the Asset
  1195. func (d *Disk) Clone() Asset {
  1196. var max *float64
  1197. if d.ByteUsageMax != nil {
  1198. copied := *d.ByteUsageMax
  1199. max = &copied
  1200. }
  1201. var byteHoursUsed *float64
  1202. if d.ByteHoursUsed != nil {
  1203. copied := *d.ByteHoursUsed
  1204. byteHoursUsed = &copied
  1205. }
  1206. return &Disk{
  1207. Properties: d.Properties.Clone(),
  1208. Labels: d.Labels.Clone(),
  1209. Start: d.Start,
  1210. End: d.End,
  1211. Window: d.Window.Clone(),
  1212. Adjustment: d.Adjustment,
  1213. Cost: d.Cost,
  1214. ByteHours: d.ByteHours,
  1215. ByteHoursUsed: byteHoursUsed,
  1216. ByteUsageMax: max,
  1217. Local: d.Local,
  1218. Breakdown: d.Breakdown.Clone(),
  1219. StorageClass: d.StorageClass,
  1220. VolumeName: d.VolumeName,
  1221. ClaimName: d.ClaimName,
  1222. ClaimNamespace: d.ClaimNamespace,
  1223. }
  1224. }
  1225. // Equal returns true if the two Assets match exactly
  1226. func (d *Disk) Equal(a Asset) bool {
  1227. that, ok := a.(*Disk)
  1228. if !ok {
  1229. return false
  1230. }
  1231. if !d.Labels.Equal(that.Labels) {
  1232. return false
  1233. }
  1234. if !d.Properties.Equal(that.Properties) {
  1235. return false
  1236. }
  1237. if !d.Start.Equal(that.Start) {
  1238. return false
  1239. }
  1240. if !d.End.Equal(that.End) {
  1241. return false
  1242. }
  1243. if !d.Window.Equal(that.Window) {
  1244. return false
  1245. }
  1246. if d.Adjustment != that.Adjustment {
  1247. return false
  1248. }
  1249. if d.Cost != that.Cost {
  1250. return false
  1251. }
  1252. if d.ByteHours != that.ByteHours {
  1253. return false
  1254. }
  1255. if d.ByteHoursUsed != nil && that.ByteHoursUsed == nil {
  1256. return false
  1257. }
  1258. if d.ByteHoursUsed == nil && that.ByteHoursUsed != nil {
  1259. return false
  1260. }
  1261. if (d.ByteHoursUsed != nil && that.ByteHoursUsed != nil) && *d.ByteHoursUsed != *that.ByteHoursUsed {
  1262. return false
  1263. }
  1264. if d.ByteUsageMax != nil && that.ByteUsageMax == nil {
  1265. return false
  1266. }
  1267. if d.ByteUsageMax == nil && that.ByteUsageMax != nil {
  1268. return false
  1269. }
  1270. if (d.ByteUsageMax != nil && that.ByteUsageMax != nil) && *d.ByteUsageMax != *that.ByteUsageMax {
  1271. return false
  1272. }
  1273. if d.Local != that.Local {
  1274. return false
  1275. }
  1276. if !d.Breakdown.Equal(that.Breakdown) {
  1277. return false
  1278. }
  1279. if d.StorageClass != that.StorageClass {
  1280. return false
  1281. }
  1282. if d.VolumeName != that.VolumeName {
  1283. return false
  1284. }
  1285. if d.ClaimName != that.ClaimName {
  1286. return false
  1287. }
  1288. if d.ClaimNamespace != that.ClaimNamespace {
  1289. return false
  1290. }
  1291. return true
  1292. }
  1293. // String implements fmt.Stringer
  1294. func (d *Disk) String() string {
  1295. return toString(d)
  1296. }
  1297. // Bytes returns the number of bytes belonging to the disk. This could be
  1298. // fractional because it's the number of byte*hours divided by the number of
  1299. // hours running; e.g. the sum of a 100GiB disk running for the first 10 hours
  1300. // and a 30GiB disk running for the last 20 hours of the same 24-hour window
  1301. // would produce:
  1302. //
  1303. // (100*10 + 30*20) / 24 = 66.667GiB
  1304. //
  1305. // However, any number of disks running for the full span of a window will
  1306. // report the actual number of bytes of the static disk; e.g. the above
  1307. // scenario for one entire 24-hour window:
  1308. //
  1309. // (100*24 + 30*24) / 24 = (100 + 30) = 130GiB
  1310. func (d *Disk) Bytes() float64 {
  1311. // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
  1312. return d.ByteHours * (60.0 / d.Minutes())
  1313. }
  1314. func (d *Disk) SanitizeNaN() {
  1315. if d == nil {
  1316. return
  1317. }
  1318. if math.IsNaN(d.Adjustment) {
  1319. log.DedupedWarningf(5, "Disk: Unexpected NaN found for Adjustment: labels:%v, window:%s, properties:%s", d.Labels, d.Window.String(), d.Properties.String())
  1320. d.Adjustment = 0
  1321. }
  1322. if math.IsNaN(d.Cost) {
  1323. log.DedupedWarningf(5, "Disk: Unexpected NaN found for Cost: labels:%v, window:%s, properties:%s", d.Labels, d.Window.String(), d.Properties.String())
  1324. d.Cost = 0
  1325. }
  1326. if math.IsNaN(d.ByteHours) {
  1327. log.DedupedWarningf(5, "Disk: Unexpected NaN found for ByteHours: labels:%v, window:%s, properties:%s", d.Labels, d.Window.String(), d.Properties.String())
  1328. d.ByteHours = 0
  1329. }
  1330. if math.IsNaN(d.Local) {
  1331. log.DedupedWarningf(5, "Disk: Unexpected NaN found for Local: labels:%v, window:%s, properties:%s", d.Labels, d.Window.String(), d.Properties.String())
  1332. d.Local = 0
  1333. }
  1334. if d.ByteHoursUsed != nil && math.IsNaN(*d.ByteHoursUsed) {
  1335. log.DedupedWarningf(5, "Disk: Unexpected NaN found for ByteHoursUsed: labels:%v, window:%s, properties:%s", d.Labels, d.Window.String(), d.Properties.String())
  1336. f := 0.0
  1337. d.ByteHoursUsed = &f
  1338. }
  1339. if d.ByteUsageMax != nil && math.IsNaN(*d.ByteUsageMax) {
  1340. log.DedupedWarningf(5, "Disk: Unexpected NaN found for ByteUsageMax: labels:%v, window:%s, properties:%s", d.Labels, d.Window.String(), d.Properties.String())
  1341. f := 0.0
  1342. d.ByteUsageMax = &f
  1343. }
  1344. d.Breakdown.SanitizeNaN()
  1345. }
  1346. // Breakdown describes a resource's use as a percentage of various usage types
  1347. type Breakdown struct {
  1348. Idle float64 `json:"idle"`
  1349. Other float64 `json:"other"`
  1350. System float64 `json:"system"`
  1351. User float64 `json:"user"`
  1352. }
  1353. func (b *Breakdown) SanitizeNaN() {
  1354. if math.IsNaN(b.Idle) {
  1355. log.DedupedWarningf(5, "Breakdown: Unexpected NaN found for Idle")
  1356. b.Idle = 0
  1357. }
  1358. if math.IsNaN(b.Other) {
  1359. log.DedupedWarningf(5, "Breakdown: Unexpected NaN found for Other")
  1360. b.Other = 0
  1361. }
  1362. if math.IsNaN(b.System) {
  1363. log.DedupedWarningf(5, "Breakdown: Unexpected NaN found for System")
  1364. b.System = 0
  1365. }
  1366. if math.IsNaN(b.User) {
  1367. log.DedupedWarningf(5, "Breakdown: Unexpected NaN found for User")
  1368. b.User = 0
  1369. }
  1370. }
  1371. // Clone returns a cloned instance of the Breakdown
  1372. func (b *Breakdown) Clone() *Breakdown {
  1373. if b == nil {
  1374. return nil
  1375. }
  1376. return &Breakdown{
  1377. Idle: b.Idle,
  1378. Other: b.Other,
  1379. System: b.System,
  1380. User: b.User,
  1381. }
  1382. }
  1383. // Equal returns true if the two Breakdowns are exact matches
  1384. func (b *Breakdown) Equal(that *Breakdown) bool {
  1385. if b == nil || that == nil {
  1386. return false
  1387. }
  1388. if b.Idle != that.Idle {
  1389. return false
  1390. }
  1391. if b.Other != that.Other {
  1392. return false
  1393. }
  1394. if b.System != that.System {
  1395. return false
  1396. }
  1397. if b.User != that.User {
  1398. return false
  1399. }
  1400. return true
  1401. }
  1402. // Network is an Asset representing a single node's network costs
  1403. type Network struct {
  1404. Properties *AssetProperties
  1405. Labels AssetLabels
  1406. Start time.Time
  1407. End time.Time
  1408. Window Window
  1409. Adjustment float64
  1410. Cost float64
  1411. }
  1412. // NewNetwork creates and returns a new Network Asset
  1413. func NewNetwork(name, cluster, providerID string, start, end time.Time, window Window) *Network {
  1414. properties := &AssetProperties{
  1415. Category: NetworkCategory,
  1416. Name: name,
  1417. Cluster: cluster,
  1418. ProviderID: providerID,
  1419. Service: KubernetesService,
  1420. }
  1421. return &Network{
  1422. Properties: properties,
  1423. Labels: AssetLabels{},
  1424. Start: start,
  1425. End: end,
  1426. Window: window.Clone(),
  1427. }
  1428. }
  1429. // Type returns the AssetType of the Asset
  1430. func (n *Network) Type() AssetType {
  1431. return NetworkAssetType
  1432. }
  1433. // Properties returns the Asset's Properties
  1434. func (n *Network) GetProperties() *AssetProperties {
  1435. return n.Properties
  1436. }
  1437. // SetProperties sets the Asset's Properties
  1438. func (n *Network) SetProperties(props *AssetProperties) {
  1439. n.Properties = props
  1440. }
  1441. // Labels returns the Asset's labels
  1442. func (n *Network) GetLabels() AssetLabels {
  1443. return n.Labels
  1444. }
  1445. // SetLabels sets the Asset's labels
  1446. func (n *Network) SetLabels(labels AssetLabels) {
  1447. n.Labels = labels
  1448. }
  1449. // Adjustment returns the Asset's cost adjustment
  1450. func (n *Network) GetAdjustment() float64 {
  1451. return n.Adjustment
  1452. }
  1453. // SetAdjustment sets the Asset's cost adjustment
  1454. func (n *Network) SetAdjustment(adj float64) {
  1455. n.Adjustment = adj
  1456. }
  1457. // TotalCost returns the Asset's total cost
  1458. func (n *Network) TotalCost() float64 {
  1459. return n.Cost + n.Adjustment
  1460. }
  1461. // Start returns the precise start time of the Asset within the window
  1462. func (n *Network) GetStart() time.Time {
  1463. return n.Start
  1464. }
  1465. // End returns the precise end time of the Asset within the window
  1466. func (n *Network) GetEnd() time.Time {
  1467. return n.End
  1468. }
  1469. // Minutes returns the number of minutes the Asset ran within the window
  1470. func (n *Network) Minutes() float64 {
  1471. netMins := n.End.Sub(n.Start).Minutes()
  1472. windowMins := n.Window.Minutes()
  1473. if netMins > windowMins {
  1474. log.Warnf("Asset ETL: Network.Minutes exceeds window: %.2f > %.2f", netMins, windowMins)
  1475. netMins = windowMins
  1476. }
  1477. if netMins < 0 {
  1478. netMins = 0
  1479. }
  1480. return netMins
  1481. }
  1482. // Window returns the window within which the Asset ran
  1483. func (n *Network) GetWindow() Window {
  1484. return n.Window
  1485. }
  1486. func (n *Network) SetWindow(window Window) {
  1487. n.Window = window
  1488. }
  1489. // ExpandWindow expands the Asset's window by the given window
  1490. func (n *Network) ExpandWindow(window Window) {
  1491. n.Window = n.Window.Expand(window)
  1492. }
  1493. // SetStartEnd sets the Asset's Start and End fields
  1494. func (n *Network) SetStartEnd(start, end time.Time) {
  1495. if n.Window.Contains(start) {
  1496. n.Start = start
  1497. } else {
  1498. log.Warnf("Disk.SetStartEnd: start %s not in %s", start, n.Window)
  1499. }
  1500. if n.Window.Contains(end) {
  1501. n.End = end
  1502. } else {
  1503. log.Warnf("Disk.SetStartEnd: end %s not in %s", end, n.Window)
  1504. }
  1505. }
  1506. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  1507. // as much relevant information as possible (i.e. type, Properties, labels).
  1508. func (n *Network) Add(a Asset) Asset {
  1509. // Network + Network = Network
  1510. if that, ok := a.(*Network); ok {
  1511. this := n.Clone().(*Network)
  1512. this.add(that)
  1513. return this
  1514. }
  1515. props := n.Properties.Merge(a.GetProperties())
  1516. labels := n.Labels.Merge(a.GetLabels())
  1517. start := n.Start
  1518. if a.GetStart().Before(start) {
  1519. start = a.GetStart()
  1520. }
  1521. end := n.End
  1522. if a.GetEnd().After(end) {
  1523. end = a.GetEnd()
  1524. }
  1525. window := n.Window.Expand(a.GetWindow())
  1526. // Network + !Network = Any
  1527. any := NewAsset(start, end, window)
  1528. any.SetProperties(props)
  1529. any.SetLabels(labels)
  1530. any.Adjustment = n.Adjustment + a.GetAdjustment()
  1531. any.Cost = (n.TotalCost() - n.Adjustment) + (a.TotalCost() - a.GetAdjustment())
  1532. return any
  1533. }
  1534. func (n *Network) add(that *Network) {
  1535. if n == nil {
  1536. n = that
  1537. return
  1538. }
  1539. props := n.Properties.Merge(that.Properties)
  1540. labels := n.Labels.Merge(that.Labels)
  1541. n.SetProperties(props)
  1542. n.SetLabels(labels)
  1543. start := n.Start
  1544. if that.Start.Before(start) {
  1545. start = that.Start
  1546. }
  1547. end := n.End
  1548. if that.End.After(end) {
  1549. end = that.End
  1550. }
  1551. window := n.Window.Expand(that.Window)
  1552. n.Start = start
  1553. n.End = end
  1554. n.Window = window
  1555. n.Cost += that.Cost
  1556. n.Adjustment += that.Adjustment
  1557. }
  1558. // Clone returns a deep copy of the given Network
  1559. func (n *Network) Clone() Asset {
  1560. if n == nil {
  1561. return nil
  1562. }
  1563. return &Network{
  1564. Properties: n.Properties.Clone(),
  1565. Labels: n.Labels.Clone(),
  1566. Start: n.Start,
  1567. End: n.End,
  1568. Window: n.Window.Clone(),
  1569. Adjustment: n.Adjustment,
  1570. Cost: n.Cost,
  1571. }
  1572. }
  1573. // Equal returns true if the tow Assets match exactly
  1574. func (n *Network) Equal(a Asset) bool {
  1575. that, ok := a.(*Network)
  1576. if !ok {
  1577. return false
  1578. }
  1579. if !n.Labels.Equal(that.Labels) {
  1580. return false
  1581. }
  1582. if !n.Properties.Equal(that.Properties) {
  1583. return false
  1584. }
  1585. if !n.Start.Equal(that.Start) {
  1586. return false
  1587. }
  1588. if !n.End.Equal(that.End) {
  1589. return false
  1590. }
  1591. if !n.Window.Equal(that.Window) {
  1592. return false
  1593. }
  1594. if n.Adjustment != that.Adjustment {
  1595. return false
  1596. }
  1597. if n.Cost != that.Cost {
  1598. return false
  1599. }
  1600. return true
  1601. }
  1602. // String implements fmt.Stringer
  1603. func (n *Network) String() string {
  1604. return toString(n)
  1605. }
  1606. func (n *Network) SanitizeNaN() {
  1607. if n == nil {
  1608. return
  1609. }
  1610. if math.IsNaN(n.Adjustment) {
  1611. log.DedupedWarningf(5, "Network: Unexpected NaN found for Adjustment: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  1612. n.Adjustment = 0
  1613. }
  1614. if math.IsNaN(n.Cost) {
  1615. log.DedupedWarningf(5, "Network: Unexpected NaN found for Cost: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  1616. n.Cost = 0
  1617. }
  1618. }
  1619. // NodeOverhead represents the delta between the allocatable resources
  1620. // of the node and the node nameplate capacity
  1621. type NodeOverhead struct {
  1622. CpuOverheadFraction float64
  1623. RamOverheadFraction float64
  1624. OverheadCostFraction float64
  1625. }
  1626. func (n *NodeOverhead) SanitizeNaN() {
  1627. if math.IsNaN(n.CpuOverheadFraction) {
  1628. log.DedupedWarningf(5, "NodeOverhead: Unexpected NaN found for CpuOverheadFraction")
  1629. n.CpuOverheadFraction = 0
  1630. }
  1631. if math.IsNaN(n.RamOverheadFraction) {
  1632. log.DedupedWarningf(5, "NodeOverhead: Unexpected NaN found for RamOverheadFraction")
  1633. n.RamOverheadFraction = 0
  1634. }
  1635. if math.IsNaN(n.OverheadCostFraction) {
  1636. log.DedupedWarningf(5, "NodeOverhead: Unexpected NaN found for OverheadCostFraction")
  1637. n.OverheadCostFraction = 0
  1638. }
  1639. }
  1640. // Node is an Asset representing a single node in a cluster
  1641. type Node struct {
  1642. Properties *AssetProperties
  1643. Labels AssetLabels
  1644. Start time.Time
  1645. End time.Time
  1646. Window Window
  1647. Adjustment float64
  1648. NodeType string
  1649. CPUCoreHours float64
  1650. RAMByteHours float64
  1651. GPUHours float64
  1652. CPUBreakdown *Breakdown
  1653. RAMBreakdown *Breakdown
  1654. CPUCost float64
  1655. GPUCost float64
  1656. GPUCount float64
  1657. RAMCost float64
  1658. Discount float64
  1659. Preemptible float64
  1660. Overhead *NodeOverhead // @bingen:field[version=19]
  1661. }
  1662. // NewNode creates and returns a new Node Asset
  1663. func NewNode(name, cluster, providerID string, start, end time.Time, window Window) *Node {
  1664. properties := &AssetProperties{
  1665. Category: ComputeCategory,
  1666. Name: name,
  1667. Cluster: cluster,
  1668. ProviderID: providerID,
  1669. Service: KubernetesService,
  1670. }
  1671. return &Node{
  1672. Properties: properties,
  1673. Labels: AssetLabels{},
  1674. Start: start,
  1675. End: end,
  1676. Window: window.Clone(),
  1677. CPUBreakdown: &Breakdown{},
  1678. RAMBreakdown: &Breakdown{},
  1679. }
  1680. }
  1681. // Type returns the AssetType of the Asset
  1682. func (n *Node) Type() AssetType {
  1683. return NodeAssetType
  1684. }
  1685. // Properties returns the Asset's Properties
  1686. func (n *Node) GetProperties() *AssetProperties {
  1687. return n.Properties
  1688. }
  1689. // SetProperties sets the Asset's Properties
  1690. func (n *Node) SetProperties(props *AssetProperties) {
  1691. n.Properties = props
  1692. }
  1693. // Labels returns the Asset's labels
  1694. func (n *Node) GetLabels() AssetLabels {
  1695. return n.Labels
  1696. }
  1697. // SetLabels sets the Asset's labels
  1698. func (n *Node) SetLabels(labels AssetLabels) {
  1699. n.Labels = labels
  1700. }
  1701. // Adjustment returns the Asset's cost adjustment
  1702. func (n *Node) GetAdjustment() float64 {
  1703. return n.Adjustment
  1704. }
  1705. // SetAdjustment sets the Asset's cost adjustment
  1706. func (n *Node) SetAdjustment(adj float64) {
  1707. n.Adjustment = adj
  1708. }
  1709. // TotalCost returns the Asset's total cost
  1710. func (n *Node) TotalCost() float64 {
  1711. return ((n.CPUCost + n.RAMCost) * (1.0 - n.Discount)) + n.GPUCost + n.Adjustment
  1712. }
  1713. // Start returns the precise start time of the Asset within the window
  1714. func (n *Node) GetStart() time.Time {
  1715. return n.Start
  1716. }
  1717. // End returns the precise end time of the Asset within the window
  1718. func (n *Node) GetEnd() time.Time {
  1719. return n.End
  1720. }
  1721. // Minutes returns the number of minutes the Asset ran within the window
  1722. func (n *Node) Minutes() float64 {
  1723. nodeMins := n.End.Sub(n.Start).Minutes()
  1724. windowMins := n.Window.Minutes()
  1725. if nodeMins > windowMins {
  1726. log.Warnf("Asset ETL: Node.Minutes exceeds window: %.2f > %.2f", nodeMins, windowMins)
  1727. nodeMins = windowMins
  1728. }
  1729. if nodeMins < 0 {
  1730. nodeMins = 0
  1731. }
  1732. return nodeMins
  1733. }
  1734. // Window returns the window within which the Asset ran
  1735. func (n *Node) GetWindow() Window {
  1736. return n.Window
  1737. }
  1738. func (n *Node) SetWindow(window Window) {
  1739. n.Window = window
  1740. }
  1741. // ExpandWindow expands the Asset's window by the given window
  1742. func (n *Node) ExpandWindow(window Window) {
  1743. n.Window = n.Window.Expand(window)
  1744. }
  1745. // SetStartEnd sets the Asset's Start and End fields
  1746. func (n *Node) SetStartEnd(start, end time.Time) {
  1747. if n.Window.Contains(start) {
  1748. n.Start = start
  1749. } else {
  1750. log.Warnf("Disk.SetStartEnd: start %s not in %s", start, n.Window)
  1751. }
  1752. if n.Window.Contains(end) {
  1753. n.End = end
  1754. } else {
  1755. log.Warnf("Disk.SetStartEnd: end %s not in %s", end, n.Window)
  1756. }
  1757. }
  1758. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  1759. // as much relevant information as possible (i.e. type, Properties, labels).
  1760. func (n *Node) Add(a Asset) Asset {
  1761. // Node + Node = Node
  1762. if that, ok := a.(*Node); ok {
  1763. this := n.Clone().(*Node)
  1764. this.add(that)
  1765. return this
  1766. }
  1767. props := n.Properties.Merge(a.GetProperties())
  1768. labels := n.Labels.Merge(a.GetLabels())
  1769. start := n.Start
  1770. if a.GetStart().Before(start) {
  1771. start = a.GetStart()
  1772. }
  1773. end := n.End
  1774. if a.GetEnd().After(end) {
  1775. end = a.GetEnd()
  1776. }
  1777. window := n.Window.Expand(a.GetWindow())
  1778. // Node + !Node = Any
  1779. any := NewAsset(start, end, window)
  1780. any.SetProperties(props)
  1781. any.SetLabels(labels)
  1782. any.Adjustment = n.Adjustment + a.GetAdjustment()
  1783. any.Cost = (n.TotalCost() - n.Adjustment) + (a.TotalCost() - a.GetAdjustment())
  1784. return any
  1785. }
  1786. func (n *Node) add(that *Node) {
  1787. if n == nil {
  1788. n = that
  1789. return
  1790. }
  1791. props := n.Properties.Merge(that.Properties)
  1792. labels := n.Labels.Merge(that.Labels)
  1793. n.SetProperties(props)
  1794. n.SetLabels(labels)
  1795. if n.NodeType != that.NodeType {
  1796. n.NodeType = ""
  1797. }
  1798. start := n.Start
  1799. if that.Start.Before(start) {
  1800. start = that.Start
  1801. }
  1802. end := n.End
  1803. if that.End.After(end) {
  1804. end = that.End
  1805. }
  1806. window := n.Window.Expand(that.Window)
  1807. n.Start = start
  1808. n.End = end
  1809. n.Window = window
  1810. // Order of operations for node costs is:
  1811. // Discount(CPU + RAM) + GPU + Adjustment
  1812. // Combining discounts, then involves weighting each discount by each
  1813. // respective (CPU + RAM) cost. Combining preemptible, on the other
  1814. // hand, is done with all three (but not Adjustment, which can change
  1815. // without triggering a re-computation of Preemtible).
  1816. disc := (n.CPUCost+n.RAMCost)*(1.0-n.Discount) + (that.CPUCost+that.RAMCost)*(1.0-that.Discount)
  1817. nonDisc := (n.CPUCost + n.RAMCost) + (that.CPUCost + that.RAMCost)
  1818. if nonDisc > 0 {
  1819. n.Discount = 1.0 - (disc / nonDisc)
  1820. } else {
  1821. n.Discount = (n.Discount + that.Discount) / 2.0
  1822. }
  1823. nNoAdj := n.TotalCost() - n.Adjustment
  1824. thatNoAdj := that.TotalCost() - that.Adjustment
  1825. if (nNoAdj + thatNoAdj) > 0 {
  1826. n.Preemptible = (nNoAdj*n.Preemptible + thatNoAdj*that.Preemptible) / (nNoAdj + thatNoAdj)
  1827. } else {
  1828. n.Preemptible = (n.Preemptible + that.Preemptible) / 2.0
  1829. }
  1830. totalCPUCost := n.CPUCost + that.CPUCost
  1831. if totalCPUCost > 0.0 {
  1832. n.CPUBreakdown.Idle = (n.CPUBreakdown.Idle*n.CPUCost + that.CPUBreakdown.Idle*that.CPUCost) / totalCPUCost
  1833. n.CPUBreakdown.Other = (n.CPUBreakdown.Other*n.CPUCost + that.CPUBreakdown.Other*that.CPUCost) / totalCPUCost
  1834. n.CPUBreakdown.System = (n.CPUBreakdown.System*n.CPUCost + that.CPUBreakdown.System*that.CPUCost) / totalCPUCost
  1835. n.CPUBreakdown.User = (n.CPUBreakdown.User*n.CPUCost + that.CPUBreakdown.User*that.CPUCost) / totalCPUCost
  1836. }
  1837. totalRAMCost := n.RAMCost + that.RAMCost
  1838. if totalRAMCost > 0.0 {
  1839. n.RAMBreakdown.Idle = (n.RAMBreakdown.Idle*n.RAMCost + that.RAMBreakdown.Idle*that.RAMCost) / totalRAMCost
  1840. n.RAMBreakdown.Other = (n.RAMBreakdown.Other*n.RAMCost + that.RAMBreakdown.Other*that.RAMCost) / totalRAMCost
  1841. n.RAMBreakdown.System = (n.RAMBreakdown.System*n.RAMCost + that.RAMBreakdown.System*that.RAMCost) / totalRAMCost
  1842. n.RAMBreakdown.User = (n.RAMBreakdown.User*n.RAMCost + that.RAMBreakdown.User*that.RAMCost) / totalRAMCost
  1843. }
  1844. n.CPUCoreHours += that.CPUCoreHours
  1845. n.RAMByteHours += that.RAMByteHours
  1846. n.GPUHours += that.GPUHours
  1847. n.CPUCost += that.CPUCost
  1848. n.GPUCost += that.GPUCost
  1849. n.RAMCost += that.RAMCost
  1850. n.Adjustment += that.Adjustment
  1851. if n.Overhead != nil && that.Overhead != nil {
  1852. n.Overhead.RamOverheadFraction = (n.Overhead.RamOverheadFraction*n.RAMCost + that.Overhead.RamOverheadFraction*that.RAMCost) / totalRAMCost
  1853. n.Overhead.CpuOverheadFraction = (n.Overhead.CpuOverheadFraction*n.CPUCost + that.Overhead.CpuOverheadFraction*that.CPUCost) / totalCPUCost
  1854. n.Overhead.OverheadCostFraction = ((n.Overhead.CpuOverheadFraction * n.CPUCost) + (n.Overhead.RamOverheadFraction * n.RAMCost)) / n.TotalCost()
  1855. }
  1856. }
  1857. // Clone returns a deep copy of the given Node
  1858. func (n *Node) Clone() Asset {
  1859. if n == nil {
  1860. return nil
  1861. }
  1862. return &Node{
  1863. Properties: n.Properties.Clone(),
  1864. Labels: n.Labels.Clone(),
  1865. Start: n.Start,
  1866. End: n.End,
  1867. Window: n.Window.Clone(),
  1868. Adjustment: n.Adjustment,
  1869. NodeType: n.NodeType,
  1870. CPUCoreHours: n.CPUCoreHours,
  1871. RAMByteHours: n.RAMByteHours,
  1872. GPUHours: n.GPUHours,
  1873. CPUBreakdown: n.CPUBreakdown.Clone(),
  1874. RAMBreakdown: n.RAMBreakdown.Clone(),
  1875. CPUCost: n.CPUCost,
  1876. GPUCost: n.GPUCost,
  1877. GPUCount: n.GPUCount,
  1878. RAMCost: n.RAMCost,
  1879. Preemptible: n.Preemptible,
  1880. Discount: n.Discount,
  1881. }
  1882. }
  1883. // Equal returns true if the tow Assets match exactly
  1884. func (n *Node) Equal(a Asset) bool {
  1885. that, ok := a.(*Node)
  1886. if !ok {
  1887. return false
  1888. }
  1889. if !n.Labels.Equal(that.Labels) {
  1890. return false
  1891. }
  1892. if !n.Properties.Equal(that.Properties) {
  1893. return false
  1894. }
  1895. if !n.Start.Equal(that.Start) {
  1896. return false
  1897. }
  1898. if !n.End.Equal(that.End) {
  1899. return false
  1900. }
  1901. if !n.Window.Equal(that.Window) {
  1902. return false
  1903. }
  1904. if n.Adjustment != that.Adjustment {
  1905. return false
  1906. }
  1907. if n.NodeType != that.NodeType {
  1908. return false
  1909. }
  1910. if n.CPUCoreHours != that.CPUCoreHours {
  1911. return false
  1912. }
  1913. if n.RAMByteHours != that.RAMByteHours {
  1914. return false
  1915. }
  1916. if n.GPUHours != that.GPUHours {
  1917. return false
  1918. }
  1919. if !n.CPUBreakdown.Equal(that.CPUBreakdown) {
  1920. return false
  1921. }
  1922. if !n.RAMBreakdown.Equal(that.RAMBreakdown) {
  1923. return false
  1924. }
  1925. if n.CPUCost != that.CPUCost {
  1926. return false
  1927. }
  1928. if n.GPUCost != that.GPUCost {
  1929. return false
  1930. }
  1931. if n.RAMCost != that.RAMCost {
  1932. return false
  1933. }
  1934. if n.Discount != that.Discount {
  1935. return false
  1936. }
  1937. if n.Preemptible != that.Preemptible {
  1938. return false
  1939. }
  1940. return true
  1941. }
  1942. // String implements fmt.Stringer
  1943. func (n *Node) String() string {
  1944. return toString(n)
  1945. }
  1946. // IsPreemptible returns true if the node is 100% preemptible. It's possible
  1947. // to be "partially preemptible" by adding a preemptible node with a
  1948. // non-preemptible node.
  1949. func (n *Node) IsPreemptible() bool {
  1950. return n.Preemptible == 1.0
  1951. }
  1952. // CPUCores returns the number of cores belonging to the node. This could be
  1953. // fractional because it's the number of core*hours divided by the number of
  1954. // hours running; e.g. the sum of a 4-core node running for the first 10 hours
  1955. // and a 3-core node running for the last 20 hours of the same 24-hour window
  1956. // would produce:
  1957. //
  1958. // (4*10 + 3*20) / 24 = 4.167 cores
  1959. //
  1960. // However, any number of cores running for the full span of a window will
  1961. // report the actual number of cores of the static node; e.g. the above
  1962. // scenario for one entire 24-hour window:
  1963. //
  1964. // (4*24 + 3*24) / 24 = (4 + 3) = 7 cores
  1965. func (n *Node) CPUCores() float64 {
  1966. // [core*hr]*([min/hr]*[1/min]) = [core*hr]/[hr] = core
  1967. return n.CPUCoreHours * (60.0 / n.Minutes())
  1968. }
  1969. // RAMBytes returns the amount of RAM belonging to the node. This could be
  1970. // fractional because it's the number of byte*hours divided by the number of
  1971. // hours running; e.g. the sum of a 12GiB-RAM node running for the first 10 hours
  1972. // and a 16GiB-RAM node running for the last 20 hours of the same 24-hour window
  1973. // would produce:
  1974. //
  1975. // (12*10 + 16*20) / 24 = 18.333GiB RAM
  1976. //
  1977. // However, any number of bytes running for the full span of a window will
  1978. // report the actual number of bytes of the static node; e.g. the above
  1979. // scenario for one entire 24-hour window:
  1980. //
  1981. // (12*24 + 16*24) / 24 = (12 + 16) = 28GiB RAM
  1982. func (n *Node) RAMBytes() float64 {
  1983. // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
  1984. return n.RAMByteHours * (60.0 / n.Minutes())
  1985. }
  1986. // GPUs returns the amount of GPUs belonging to the node. This could be
  1987. // fractional because it's the number of gpu*hours divided by the number of
  1988. // hours running; e.g. the sum of a 2 gpu node running for the first 10 hours
  1989. // and a 1 gpu node running for the last 20 hours of the same 24-hour window
  1990. // would produce:
  1991. //
  1992. // (2*10 + 1*20) / 24 = 1.667 GPUs
  1993. //
  1994. // However, any number of GPUs running for the full span of a window will
  1995. // report the actual number of GPUs of the static node; e.g. the above
  1996. // scenario for one entire 24-hour window:
  1997. //
  1998. // (2*24 + 1*24) / 24 = (2 + 1) = 3 GPUs
  1999. func (n *Node) GPUs() float64 {
  2000. // [b*hr]*([min/hr]*[1/min]) = [b*hr]/[hr] = b
  2001. return n.GPUHours * (60.0 / n.Minutes())
  2002. }
  2003. func (n *Node) SanitizeNaN() {
  2004. if math.IsNaN(n.Adjustment) {
  2005. log.DedupedWarningf(5, "Node: Unexpected NaN found for Adjustment: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2006. n.Adjustment = 0
  2007. }
  2008. if math.IsNaN(n.CPUCoreHours) {
  2009. log.DedupedWarningf(5, "Node: Unexpected NaN found for CPUCoreHours: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2010. n.CPUCoreHours = 0
  2011. }
  2012. if math.IsNaN(n.RAMByteHours) {
  2013. log.DedupedWarningf(5, "Node: Unexpected NaN found for RAMByteHours: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2014. n.RAMByteHours = 0
  2015. }
  2016. if math.IsNaN(n.GPUHours) {
  2017. log.DedupedWarningf(5, "Node: Unexpected NaN found for GPUHours: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2018. n.GPUHours = 0
  2019. }
  2020. if math.IsNaN(n.CPUCost) {
  2021. log.DedupedWarningf(5, "Node: Unexpected NaN found for CPUCost: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2022. n.CPUCost = 0
  2023. }
  2024. if math.IsNaN(n.GPUCost) {
  2025. log.DedupedWarningf(5, "Node: Unexpected NaN found for GPUCost: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2026. n.GPUCost = 0
  2027. }
  2028. if math.IsNaN(n.GPUCount) {
  2029. log.DedupedWarningf(5, "Node: Unexpected NaN found for GPUCount: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2030. n.GPUCount = 0
  2031. }
  2032. if math.IsNaN(n.RAMCost) {
  2033. log.DedupedWarningf(5, "Node: Unexpected NaN found for RAMCost: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2034. n.RAMCost = 0
  2035. }
  2036. if math.IsNaN(n.Discount) {
  2037. log.DedupedWarningf(5, "Node: Unexpected NaN found for Discount: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2038. n.Discount = 0
  2039. }
  2040. if math.IsNaN(n.Preemptible) {
  2041. log.DedupedWarningf(5, "Node: Unexpected NaN found for Preemptible: labels:%v, window:%s, properties:%s", n.Labels, n.Window.String(), n.Properties.String())
  2042. n.Preemptible = 0
  2043. }
  2044. n.CPUBreakdown.SanitizeNaN()
  2045. n.RAMBreakdown.SanitizeNaN()
  2046. n.Overhead.SanitizeNaN()
  2047. }
  2048. // LoadBalancer is an Asset representing a single load balancer in a cluster
  2049. // TODO: add GB of ingress processed, numForwardingRules once we start recording those to prometheus metric
  2050. type LoadBalancer struct {
  2051. Properties *AssetProperties
  2052. Labels AssetLabels
  2053. Start time.Time
  2054. End time.Time
  2055. Window Window
  2056. Adjustment float64
  2057. Cost float64
  2058. Private bool // @bingen:field[version=20]
  2059. }
  2060. // NewLoadBalancer instantiates and returns a new LoadBalancer
  2061. func NewLoadBalancer(name, cluster, providerID string, start, end time.Time, window Window, private bool) *LoadBalancer {
  2062. properties := &AssetProperties{
  2063. Category: NetworkCategory,
  2064. Name: name,
  2065. Cluster: cluster,
  2066. ProviderID: providerID,
  2067. Service: KubernetesService,
  2068. }
  2069. return &LoadBalancer{
  2070. Properties: properties,
  2071. Labels: AssetLabels{},
  2072. Start: start,
  2073. End: end,
  2074. Window: window,
  2075. Private: private,
  2076. }
  2077. }
  2078. // Type returns the AssetType of the Asset
  2079. func (lb *LoadBalancer) Type() AssetType {
  2080. return LoadBalancerAssetType
  2081. }
  2082. // Properties returns the Asset's Properties
  2083. func (lb *LoadBalancer) GetProperties() *AssetProperties {
  2084. return lb.Properties
  2085. }
  2086. // SetProperties sets the Asset's Properties
  2087. func (lb *LoadBalancer) SetProperties(props *AssetProperties) {
  2088. lb.Properties = props
  2089. }
  2090. // Labels returns the Asset's labels
  2091. func (lb *LoadBalancer) GetLabels() AssetLabels {
  2092. return lb.Labels
  2093. }
  2094. // SetLabels sets the Asset's labels
  2095. func (lb *LoadBalancer) SetLabels(labels AssetLabels) {
  2096. lb.Labels = labels
  2097. }
  2098. // Adjustment returns the Asset's cost adjustment
  2099. func (lb *LoadBalancer) GetAdjustment() float64 {
  2100. return lb.Adjustment
  2101. }
  2102. // SetAdjustment sets the Asset's cost adjustment
  2103. func (lb *LoadBalancer) SetAdjustment(adj float64) {
  2104. lb.Adjustment = adj
  2105. }
  2106. // TotalCost returns the total cost of the Asset
  2107. func (lb *LoadBalancer) TotalCost() float64 {
  2108. return lb.Cost + lb.Adjustment
  2109. }
  2110. // Start returns the preceise start point of the Asset within the window
  2111. func (lb *LoadBalancer) GetStart() time.Time {
  2112. return lb.Start
  2113. }
  2114. // End returns the preceise end point of the Asset within the window
  2115. func (lb *LoadBalancer) GetEnd() time.Time {
  2116. return lb.End
  2117. }
  2118. // Minutes returns the number of minutes the Asset ran within the window
  2119. func (lb *LoadBalancer) Minutes() float64 {
  2120. return lb.End.Sub(lb.Start).Minutes()
  2121. }
  2122. // Window returns the window within which the Asset ran
  2123. func (lb *LoadBalancer) GetWindow() Window {
  2124. return lb.Window
  2125. }
  2126. func (lb *LoadBalancer) SetWindow(window Window) {
  2127. lb.Window = window
  2128. }
  2129. // ExpandWindow expands the Asset's window by the given window
  2130. func (lb *LoadBalancer) ExpandWindow(w Window) {
  2131. lb.Window = lb.Window.Expand(w)
  2132. }
  2133. // SetStartEnd sets the Asset's Start and End fields
  2134. func (lb *LoadBalancer) SetStartEnd(start, end time.Time) {
  2135. if lb.Window.Contains(start) {
  2136. lb.Start = start
  2137. } else {
  2138. log.Warnf("Disk.SetStartEnd: start %s not in %s", start, lb.Window)
  2139. }
  2140. if lb.Window.Contains(end) {
  2141. lb.End = end
  2142. } else {
  2143. log.Warnf("Disk.SetStartEnd: end %s not in %s", end, lb.Window)
  2144. }
  2145. }
  2146. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  2147. // as much relevant information as possible (i.e. type, Properties, labels).
  2148. func (lb *LoadBalancer) Add(a Asset) Asset {
  2149. // LoadBalancer + LoadBalancer = LoadBalancer
  2150. if that, ok := a.(*LoadBalancer); ok {
  2151. this := lb.Clone().(*LoadBalancer)
  2152. this.add(that)
  2153. return this
  2154. }
  2155. props := lb.GetProperties().Merge(a.GetProperties())
  2156. labels := lb.Labels.Merge(a.GetLabels())
  2157. start := lb.Start
  2158. if a.GetStart().Before(start) {
  2159. start = a.GetStart()
  2160. }
  2161. end := lb.End
  2162. if a.GetEnd().After(end) {
  2163. end = a.GetEnd()
  2164. }
  2165. window := lb.Window.Expand(a.GetWindow())
  2166. // LoadBalancer + !LoadBalancer = Any
  2167. any := NewAsset(start, end, window)
  2168. any.SetProperties(props)
  2169. any.SetLabels(labels)
  2170. any.Adjustment = lb.Adjustment + a.GetAdjustment()
  2171. any.Cost = (lb.TotalCost() - lb.Adjustment) + (a.TotalCost() - a.GetAdjustment())
  2172. return any
  2173. }
  2174. func (lb *LoadBalancer) add(that *LoadBalancer) {
  2175. if lb == nil {
  2176. lb = that
  2177. return
  2178. }
  2179. props := lb.Properties.Merge(that.GetProperties())
  2180. labels := lb.Labels.Merge(that.GetLabels())
  2181. lb.SetProperties(props)
  2182. lb.SetLabels(labels)
  2183. start := lb.Start
  2184. if that.Start.Before(start) {
  2185. start = that.Start
  2186. }
  2187. end := lb.End
  2188. if that.End.After(end) {
  2189. end = that.End
  2190. }
  2191. window := lb.Window.Expand(that.Window)
  2192. lb.Start = start
  2193. lb.End = end
  2194. lb.Window = window
  2195. lb.Cost += that.Cost
  2196. lb.Adjustment += that.Adjustment
  2197. }
  2198. // Clone returns a cloned instance of the given Asset
  2199. func (lb *LoadBalancer) Clone() Asset {
  2200. return &LoadBalancer{
  2201. Properties: lb.Properties.Clone(),
  2202. Labels: lb.Labels.Clone(),
  2203. Start: lb.Start,
  2204. End: lb.End,
  2205. Window: lb.Window.Clone(),
  2206. Adjustment: lb.Adjustment,
  2207. Cost: lb.Cost,
  2208. }
  2209. }
  2210. // Equal returns true if the tow Assets match precisely
  2211. func (lb *LoadBalancer) Equal(a Asset) bool {
  2212. that, ok := a.(*LoadBalancer)
  2213. if !ok {
  2214. return false
  2215. }
  2216. if !lb.Labels.Equal(that.Labels) {
  2217. return false
  2218. }
  2219. if !lb.Properties.Equal(that.Properties) {
  2220. return false
  2221. }
  2222. if !lb.Start.Equal(that.Start) {
  2223. return false
  2224. }
  2225. if !lb.End.Equal(that.End) {
  2226. return false
  2227. }
  2228. if !lb.Window.Equal(that.Window) {
  2229. return false
  2230. }
  2231. if lb.Adjustment != that.Adjustment {
  2232. return false
  2233. }
  2234. if lb.Cost != that.Cost {
  2235. return false
  2236. }
  2237. return true
  2238. }
  2239. // String implements fmt.Stringer
  2240. func (lb *LoadBalancer) String() string {
  2241. return toString(lb)
  2242. }
  2243. func (lb *LoadBalancer) SanitizeNaN() {
  2244. if lb == nil {
  2245. return
  2246. }
  2247. if math.IsNaN(lb.Adjustment) {
  2248. log.DedupedWarningf(5, "LoadBalancer: Unexpected NaN found for Adjustment: labels:%v, window:%s, properties:%s", lb.Labels, lb.Window.String(), lb.Properties.String())
  2249. lb.Adjustment = 0
  2250. }
  2251. if math.IsNaN(lb.Cost) {
  2252. log.DedupedWarningf(5, "LoadBalancer: Unexpected NaN found for Cost: labels:%v, window:%s, properties:%s", lb.Labels, lb.Window.String(), lb.Properties.String())
  2253. lb.Cost = 0
  2254. }
  2255. }
  2256. // SharedAsset is an Asset representing a shared cost
  2257. type SharedAsset struct {
  2258. Properties *AssetProperties
  2259. Labels AssetLabels
  2260. Window Window
  2261. Cost float64
  2262. }
  2263. // NewSharedAsset creates and returns a new SharedAsset
  2264. func NewSharedAsset(name string, window Window) *SharedAsset {
  2265. properties := &AssetProperties{
  2266. Name: name,
  2267. Category: SharedCategory,
  2268. Service: OtherCategory,
  2269. }
  2270. return &SharedAsset{
  2271. Properties: properties,
  2272. Labels: AssetLabels{},
  2273. Window: window.Clone(),
  2274. }
  2275. }
  2276. // Type returns the AssetType of the Asset
  2277. func (sa *SharedAsset) Type() AssetType {
  2278. return SharedAssetType
  2279. }
  2280. // Properties returns the Asset's Properties
  2281. func (sa *SharedAsset) GetProperties() *AssetProperties {
  2282. return sa.Properties
  2283. }
  2284. // SetProperties sets the Asset's Properties
  2285. func (sa *SharedAsset) SetProperties(props *AssetProperties) {
  2286. sa.Properties = props
  2287. }
  2288. // Labels returns the Asset's labels
  2289. func (sa *SharedAsset) GetLabels() AssetLabels {
  2290. return sa.Labels
  2291. }
  2292. // SetLabels sets the Asset's labels
  2293. func (sa *SharedAsset) SetLabels(labels AssetLabels) {
  2294. sa.Labels = labels
  2295. }
  2296. // Adjustment is not relevant to SharedAsset, but required to implement Asset
  2297. func (sa *SharedAsset) GetAdjustment() float64 {
  2298. return 0.0
  2299. }
  2300. // SetAdjustment is not relevant to SharedAsset, but required to implement Asset
  2301. func (sa *SharedAsset) SetAdjustment(float64) {
  2302. return
  2303. }
  2304. // TotalCost returns the Asset's total cost
  2305. func (sa *SharedAsset) TotalCost() float64 {
  2306. return sa.Cost
  2307. }
  2308. // Start returns the start time of the Asset
  2309. func (sa *SharedAsset) GetStart() time.Time {
  2310. return *sa.Window.start
  2311. }
  2312. // End returns the end time of the Asset
  2313. func (sa *SharedAsset) GetEnd() time.Time {
  2314. return *sa.Window.end
  2315. }
  2316. // Minutes returns the number of minutes the SharedAsset ran within the window
  2317. func (sa *SharedAsset) Minutes() float64 {
  2318. return sa.Window.Minutes()
  2319. }
  2320. // Window returns the window within the SharedAsset ran
  2321. func (sa *SharedAsset) GetWindow() Window {
  2322. return sa.Window
  2323. }
  2324. func (sa *SharedAsset) SetWindow(window Window) {
  2325. sa.Window = window
  2326. }
  2327. // ExpandWindow expands the Asset's window
  2328. func (sa *SharedAsset) ExpandWindow(w Window) {
  2329. sa.Window = sa.Window.Expand(w)
  2330. }
  2331. // SetStartEnd sets the Asset's Start and End fields (not applicable here)
  2332. func (sa *SharedAsset) SetStartEnd(start, end time.Time) {
  2333. return
  2334. }
  2335. // Add sums the Asset with the given Asset to produce a new Asset, maintaining
  2336. // as much relevant information as possible (i.e. type, Properties, labels).
  2337. func (sa *SharedAsset) Add(a Asset) Asset {
  2338. // SharedAsset + SharedAsset = SharedAsset
  2339. if that, ok := a.(*SharedAsset); ok {
  2340. this := sa.Clone().(*SharedAsset)
  2341. this.add(that)
  2342. return this
  2343. }
  2344. props := sa.Properties.Merge(a.GetProperties())
  2345. labels := sa.Labels.Merge(a.GetLabels())
  2346. start := sa.GetStart()
  2347. if a.GetStart().Before(start) {
  2348. start = a.GetStart()
  2349. }
  2350. end := sa.GetEnd()
  2351. if a.GetEnd().After(end) {
  2352. end = a.GetEnd()
  2353. }
  2354. window := sa.Window.Expand(a.GetWindow())
  2355. // SharedAsset + !SharedAsset = Any
  2356. any := NewAsset(start, end, window)
  2357. any.SetProperties(props)
  2358. any.SetLabels(labels)
  2359. any.Adjustment = sa.GetAdjustment() + a.GetAdjustment()
  2360. any.Cost = (sa.TotalCost() - sa.GetAdjustment()) + (a.TotalCost() - a.GetAdjustment())
  2361. return any
  2362. }
  2363. func (sa *SharedAsset) add(that *SharedAsset) {
  2364. if sa == nil {
  2365. sa = that
  2366. return
  2367. }
  2368. props := sa.Properties.Merge(that.GetProperties())
  2369. labels := sa.Labels.Merge(that.GetLabels())
  2370. sa.SetProperties(props)
  2371. sa.SetLabels(labels)
  2372. window := sa.Window.Expand(that.Window)
  2373. sa.Window = window
  2374. sa.Cost += that.Cost
  2375. }
  2376. // Clone returns a deep copy of the given SharedAsset
  2377. func (sa *SharedAsset) Clone() Asset {
  2378. if sa == nil {
  2379. return nil
  2380. }
  2381. return &SharedAsset{
  2382. Properties: sa.Properties.Clone(),
  2383. Labels: sa.Labels.Clone(),
  2384. Window: sa.Window.Clone(),
  2385. Cost: sa.Cost,
  2386. }
  2387. }
  2388. // Equal returns true if the two Assets are exact matches
  2389. func (sa *SharedAsset) Equal(a Asset) bool {
  2390. that, ok := a.(*SharedAsset)
  2391. if !ok {
  2392. return false
  2393. }
  2394. if !sa.Labels.Equal(that.Labels) {
  2395. return false
  2396. }
  2397. if !sa.Properties.Equal(that.Properties) {
  2398. return false
  2399. }
  2400. if !sa.Window.Equal(that.Window) {
  2401. return false
  2402. }
  2403. if sa.Cost != that.Cost {
  2404. return false
  2405. }
  2406. return true
  2407. }
  2408. // String implements fmt.Stringer
  2409. func (sa *SharedAsset) String() string {
  2410. return toString(sa)
  2411. }
  2412. func (sa *SharedAsset) SanitizeNaN() {
  2413. if sa == nil {
  2414. return
  2415. }
  2416. if math.IsNaN(sa.Cost) {
  2417. log.DedupedWarningf(5, "SharedAsset: Unexpected NaN found for Cost: labels:%v, window:%s, properties:%s", sa.Labels, sa.Window.String(), sa.Properties.String())
  2418. sa.Cost = 0
  2419. }
  2420. }
  2421. // This type exists because only the assets map of AssetSet is marshaled to
  2422. // json, which makes it impossible to recreate an AssetSet struct. Thus,
  2423. // the type when unmarshaling a marshaled AssetSet,is AssetSetResponse
  2424. type AssetSetResponse struct {
  2425. Assets map[string]Asset
  2426. }
  2427. // AssetSet stores a set of Assets, each with a unique name, that share
  2428. // a window. An AssetSet is mutable, so treat it like a threadsafe map.
  2429. type AssetSet struct {
  2430. AggregationKeys []string
  2431. Assets map[string]Asset
  2432. Any map[string]*Any //@bingen:field[ignore]
  2433. Cloud map[string]*Cloud //@bingen:field[ignore]
  2434. ClusterManagement map[string]*ClusterManagement //@bingen:field[ignore]
  2435. Disks map[string]*Disk //@bingen:field[ignore]
  2436. Network map[string]*Network //@bingen:field[ignore]
  2437. Nodes map[string]*Node //@bingen:field[ignore]
  2438. LoadBalancers map[string]*LoadBalancer //@bingen:field[ignore]
  2439. SharedAssets map[string]*SharedAsset //@bingen:field[ignore]
  2440. FromSource string // stores the name of the source used to compute the data
  2441. Window Window
  2442. Warnings []string
  2443. Errors []string
  2444. }
  2445. // This methid is executed before marshalling the AssetSet binary.
  2446. func preProcessAssetSet(assetSet *AssetSet) {
  2447. length := len(assetSet.Any) + len(assetSet.Cloud) + len(assetSet.ClusterManagement) + len(assetSet.Disks) +
  2448. len(assetSet.Network) + len(assetSet.Nodes) + len(assetSet.LoadBalancers) + len(assetSet.SharedAssets)
  2449. if length != len(assetSet.Assets) {
  2450. log.Warnf("AssetSet concrete Asset maps are out of sync with AssetSet.Assets map.")
  2451. }
  2452. }
  2453. // This method is executed after unmarshalling the AssetSet binary.
  2454. func postProcessAssetSet(assetSet *AssetSet) {
  2455. for key, as := range assetSet.Assets {
  2456. addToConcreteMap(assetSet, key, as)
  2457. }
  2458. }
  2459. // addToConcreteMap adds the Asset to the correct concrete map in the AssetSet. This is used
  2460. // in the postProcessAssetSet method as well as AssetSet.Insert().
  2461. func addToConcreteMap(assetSet *AssetSet, key string, as Asset) {
  2462. switch asset := as.(type) {
  2463. case *Any:
  2464. if assetSet.Any == nil {
  2465. assetSet.Any = make(map[string]*Any)
  2466. }
  2467. assetSet.Any[key] = asset
  2468. case *Cloud:
  2469. if assetSet.Cloud == nil {
  2470. assetSet.Cloud = make(map[string]*Cloud)
  2471. }
  2472. assetSet.Cloud[key] = asset
  2473. case *ClusterManagement:
  2474. if assetSet.ClusterManagement == nil {
  2475. assetSet.ClusterManagement = make(map[string]*ClusterManagement)
  2476. }
  2477. assetSet.ClusterManagement[key] = asset
  2478. case *Disk:
  2479. if assetSet.Disks == nil {
  2480. assetSet.Disks = make(map[string]*Disk)
  2481. }
  2482. assetSet.Disks[key] = asset
  2483. case *Network:
  2484. if assetSet.Network == nil {
  2485. assetSet.Network = make(map[string]*Network)
  2486. }
  2487. assetSet.Network[key] = asset
  2488. case *Node:
  2489. if assetSet.Nodes == nil {
  2490. assetSet.Nodes = make(map[string]*Node)
  2491. }
  2492. assetSet.Nodes[key] = asset
  2493. case *LoadBalancer:
  2494. if assetSet.LoadBalancers == nil {
  2495. assetSet.LoadBalancers = make(map[string]*LoadBalancer)
  2496. }
  2497. assetSet.LoadBalancers[key] = asset
  2498. case *SharedAsset:
  2499. if assetSet.SharedAssets == nil {
  2500. assetSet.SharedAssets = make(map[string]*SharedAsset)
  2501. }
  2502. assetSet.SharedAssets[key] = asset
  2503. }
  2504. }
  2505. // removeFromConcreteMap will remove an Asset from the AssetSet's concrete type mapping for the Asset and key.
  2506. func removeFromConcreteMap(assetSet *AssetSet, key string, as Asset) {
  2507. switch as.(type) {
  2508. case *Any:
  2509. delete(assetSet.Any, key)
  2510. case *Cloud:
  2511. delete(assetSet.Cloud, key)
  2512. case *ClusterManagement:
  2513. delete(assetSet.ClusterManagement, key)
  2514. case *Disk:
  2515. delete(assetSet.Disks, key)
  2516. case *Network:
  2517. delete(assetSet.Network, key)
  2518. case *Node:
  2519. delete(assetSet.Nodes, key)
  2520. case *LoadBalancer:
  2521. delete(assetSet.LoadBalancers, key)
  2522. case *SharedAsset:
  2523. delete(assetSet.SharedAssets, key)
  2524. }
  2525. }
  2526. // NewAssetSet instantiates a new AssetSet and, optionally, inserts
  2527. // the given list of Assets
  2528. func NewAssetSet(start, end time.Time, assets ...Asset) *AssetSet {
  2529. as := &AssetSet{
  2530. Assets: map[string]Asset{},
  2531. Window: NewWindow(&start, &end),
  2532. }
  2533. for _, a := range assets {
  2534. as.Insert(a, nil)
  2535. }
  2536. return as
  2537. }
  2538. // AggregateBy aggregates the Assets in the AssetSet by the given list of
  2539. // AssetProperties, such that each asset is binned by a key determined by its
  2540. // relevant property values.
  2541. func (as *AssetSet) AggregateBy(aggregateBy []string, opts *AssetAggregationOptions) error {
  2542. if opts == nil {
  2543. opts = &AssetAggregationOptions{}
  2544. }
  2545. if as.IsEmpty() && len(opts.SharedHourlyCosts) == 0 {
  2546. return nil
  2547. }
  2548. var filter AssetMatcher
  2549. if opts.Filter == nil {
  2550. filter = &matcher.AllPass[Asset]{}
  2551. } else {
  2552. compiler := NewAssetMatchCompiler()
  2553. var err error
  2554. filter, err = compiler.Compile(opts.Filter)
  2555. if err != nil {
  2556. return fmt.Errorf("compiling filter '%s': %w", ast.ToPreOrderShortString(opts.Filter), err)
  2557. }
  2558. }
  2559. if filter == nil {
  2560. return fmt.Errorf("unexpected nil filter")
  2561. }
  2562. aggSet := NewAssetSet(as.Start(), as.End())
  2563. aggSet.AggregationKeys = aggregateBy
  2564. // Compute hours of the given AssetSet, and if it ends in the future,
  2565. // adjust the hours accordingly
  2566. hours := as.Window.Minutes() / 60.0
  2567. diff := time.Since(as.End())
  2568. if diff < 0.0 {
  2569. hours += diff.Hours()
  2570. }
  2571. // Insert a shared asset for each shared cost
  2572. for name, hourlyCost := range opts.SharedHourlyCosts {
  2573. sa := NewSharedAsset(name, as.Window.Clone())
  2574. sa.Cost = hourlyCost * hours
  2575. // Insert shared asset if it passes filter
  2576. if filter.Matches(sa) {
  2577. err := aggSet.Insert(sa, opts.LabelConfig)
  2578. if err != nil {
  2579. return err
  2580. }
  2581. }
  2582. }
  2583. // Delete the Assets that don't pass each filter
  2584. for key, asset := range as.Assets {
  2585. if !filter.Matches(asset) {
  2586. delete(as.Assets, key)
  2587. }
  2588. }
  2589. // Insert each asset into the new set, which will be keyed by the `aggregateBy`
  2590. // on aggSet, resulting in aggregation.
  2591. for _, asset := range as.Assets {
  2592. err := aggSet.Insert(asset, opts.LabelConfig)
  2593. if err != nil {
  2594. return err
  2595. }
  2596. }
  2597. // Assign the aggregated values back to the original set
  2598. as.Assets = aggSet.Assets
  2599. as.AggregationKeys = aggregateBy
  2600. return nil
  2601. }
  2602. // Clone returns a new AssetSet with a deep copy of the given
  2603. // AssetSet's assets.
  2604. func (as *AssetSet) Clone() *AssetSet {
  2605. if as == nil {
  2606. return nil
  2607. }
  2608. var aggregateBy []string
  2609. if as.AggregationKeys != nil {
  2610. aggregateBy = append(aggregateBy, as.AggregationKeys...)
  2611. }
  2612. s := as.Start()
  2613. e := as.End()
  2614. var errors []string
  2615. var warnings []string
  2616. if as.Errors != nil {
  2617. errors = make([]string, len(as.Errors))
  2618. copy(errors, as.Errors)
  2619. } else {
  2620. errors = nil
  2621. }
  2622. if as.Warnings != nil {
  2623. warnings := make([]string, len(as.Warnings))
  2624. copy(warnings, as.Warnings)
  2625. } else {
  2626. warnings = nil
  2627. }
  2628. var anyMap map[string]*Any
  2629. if as.Any != nil {
  2630. anyMap = make(map[string]*Any, len(as.Any))
  2631. }
  2632. var cloudMap map[string]*Cloud
  2633. if as.Cloud != nil {
  2634. cloudMap = make(map[string]*Cloud, len(as.Cloud))
  2635. }
  2636. var clusterManagementMap map[string]*ClusterManagement
  2637. if as.ClusterManagement != nil {
  2638. clusterManagementMap = make(map[string]*ClusterManagement, len(as.ClusterManagement))
  2639. }
  2640. var disksMap map[string]*Disk
  2641. if as.Disks != nil {
  2642. disksMap = make(map[string]*Disk, len(as.Disks))
  2643. }
  2644. var networkMap map[string]*Network
  2645. if as.Network != nil {
  2646. networkMap = make(map[string]*Network, len(as.Network))
  2647. }
  2648. var nodesMap map[string]*Node
  2649. if as.Nodes != nil {
  2650. nodesMap = make(map[string]*Node, len(as.Nodes))
  2651. }
  2652. var loadBalancersMap map[string]*LoadBalancer
  2653. if as.LoadBalancers != nil {
  2654. loadBalancersMap = make(map[string]*LoadBalancer, len(as.LoadBalancers))
  2655. }
  2656. var sharedAssetsMap map[string]*SharedAsset
  2657. if as.SharedAssets != nil {
  2658. sharedAssetsMap = make(map[string]*SharedAsset, len(as.SharedAssets))
  2659. }
  2660. assetSet := &AssetSet{
  2661. Window: NewWindow(&s, &e),
  2662. AggregationKeys: aggregateBy,
  2663. Assets: make(map[string]Asset, len(as.Assets)),
  2664. Any: anyMap,
  2665. Cloud: cloudMap,
  2666. ClusterManagement: clusterManagementMap,
  2667. Disks: disksMap,
  2668. Network: networkMap,
  2669. Nodes: nodesMap,
  2670. LoadBalancers: loadBalancersMap,
  2671. SharedAssets: sharedAssetsMap,
  2672. Errors: errors,
  2673. Warnings: warnings,
  2674. }
  2675. for k, v := range as.Assets {
  2676. newAsset := v.Clone()
  2677. assetSet.Assets[k] = newAsset
  2678. addToConcreteMap(assetSet, k, newAsset)
  2679. }
  2680. return assetSet
  2681. }
  2682. // End returns the end time of the AssetSet's window
  2683. func (as *AssetSet) End() time.Time {
  2684. return *as.Window.End()
  2685. }
  2686. // FindMatch attempts to find a match in the AssetSet for the given Asset on
  2687. // the provided Properties and labels. If a match is not found, FindMatch
  2688. // returns nil and a Not Found error.
  2689. func (as *AssetSet) FindMatch(query Asset, aggregateBy []string, labelConfig *LabelConfig) (Asset, error) {
  2690. matchKey, err := key(query, aggregateBy, labelConfig)
  2691. if err != nil {
  2692. return nil, err
  2693. }
  2694. for _, asset := range as.Assets {
  2695. if k, err := key(asset, aggregateBy, labelConfig); err != nil {
  2696. return nil, err
  2697. } else if k == matchKey {
  2698. return asset, nil
  2699. }
  2700. }
  2701. return nil, fmt.Errorf("Asset not found to match %s on %v", query, aggregateBy)
  2702. }
  2703. // ReconciliationMatch attempts to find an exact match in the AssetSet on
  2704. // (Category, ProviderID). If a match is found, it returns the Asset with the
  2705. // intent to adjust it. If no match exists, it attempts to find one on only
  2706. // (ProviderID). If that match is found, it returns the Asset with the intent
  2707. // to insert the associated Cloud cost.
  2708. func (as *AssetSet) ReconciliationMatch(query Asset) (Asset, bool, error) {
  2709. // Full match means matching on (Category, ProviderID)
  2710. fullMatchProps := []string{string(AssetCategoryProp), string(AssetProviderIDProp)}
  2711. fullMatchKey, err := key(query, fullMatchProps, nil)
  2712. // This should never happen because we are using enumerated Properties,
  2713. // but the check is here in case that changes
  2714. if err != nil {
  2715. return nil, false, err
  2716. }
  2717. // Partial match means matching only on (ProviderID)
  2718. providerIDMatchProps := []string{string(AssetProviderIDProp)}
  2719. providerIDMatchKey, err := key(query, providerIDMatchProps, nil)
  2720. // This should never happen because we are using enumerated Properties,
  2721. // but the check is here in case that changes
  2722. if err != nil {
  2723. return nil, false, err
  2724. }
  2725. var providerIDMatch Asset
  2726. for _, asset := range as.Assets {
  2727. // Ignore cloud assets when looking for reconciliation matches
  2728. if asset.Type() == CloudAssetType {
  2729. continue
  2730. }
  2731. if k, err := key(asset, fullMatchProps, nil); err != nil {
  2732. return nil, false, err
  2733. } else if k == fullMatchKey {
  2734. log.DedupedInfof(10, "Asset ETL: Reconciliation[rcnw]: ReconcileRange Match: %s", fullMatchKey)
  2735. return asset, true, nil
  2736. }
  2737. if k, err := key(asset, providerIDMatchProps, nil); err != nil {
  2738. return nil, false, err
  2739. } else if k == providerIDMatchKey {
  2740. // Found a partial match. Save it until after all other options
  2741. // have been checked for full matches.
  2742. providerIDMatch = asset
  2743. }
  2744. }
  2745. // No full match was found, so return partial match, if found.
  2746. if providerIDMatch != nil {
  2747. return providerIDMatch, false, nil
  2748. }
  2749. return nil, false, fmt.Errorf("Asset not found to match %s", query)
  2750. }
  2751. // ReconciliationMatchMap returns a map of the calling AssetSet's Assets, by provider id and category. This data structure
  2752. // allows for reconciliation matching to be done in constant time and prevents duplicate reconciliation.
  2753. func (as *AssetSet) ReconciliationMatchMap() map[string]map[string]Asset {
  2754. matchMap := make(map[string]map[string]Asset)
  2755. if as == nil {
  2756. return matchMap
  2757. }
  2758. for _, asset := range as.Assets {
  2759. if asset == nil {
  2760. continue
  2761. }
  2762. props := asset.GetProperties()
  2763. // Ignore assets that cannot be matched when looking for reconciliation matches
  2764. if props == nil || props.ProviderID == "" {
  2765. continue
  2766. }
  2767. if _, ok := matchMap[props.ProviderID]; !ok {
  2768. matchMap[props.ProviderID] = make(map[string]Asset)
  2769. }
  2770. // Check if a match is already in the map
  2771. if duplicateAsset, ok := matchMap[props.ProviderID][props.Category]; ok {
  2772. log.DedupedWarningf(5, "duplicate asset found when reconciling for %s", props.ProviderID)
  2773. // if one asset already has adjustment use that one
  2774. if duplicateAsset.GetAdjustment() == 0 && asset.GetAdjustment() != 0 {
  2775. matchMap[props.ProviderID][props.Category] = asset
  2776. } else if duplicateAsset.GetAdjustment() != 0 && asset.GetAdjustment() == 0 {
  2777. matchMap[props.ProviderID][props.Category] = duplicateAsset
  2778. // otherwise use the one with the higher cost
  2779. } else if duplicateAsset.TotalCost() < asset.TotalCost() {
  2780. matchMap[props.ProviderID][props.Category] = asset
  2781. }
  2782. } else {
  2783. matchMap[props.ProviderID][props.Category] = asset
  2784. }
  2785. }
  2786. return matchMap
  2787. }
  2788. // Get returns the Asset in the AssetSet at the given key, or nil and false
  2789. // if no Asset exists for the given key
  2790. func (as *AssetSet) Get(key string) (Asset, bool) {
  2791. if a, ok := as.Assets[key]; ok {
  2792. return a, true
  2793. }
  2794. return nil, false
  2795. }
  2796. // Insert inserts the given Asset into the AssetSet, using the AssetSet's
  2797. // configured Properties to determine the key under which the Asset will
  2798. // be inserted.
  2799. func (as *AssetSet) Insert(asset Asset, labelConfig *LabelConfig) error {
  2800. if as == nil {
  2801. return fmt.Errorf("cannot Insert into nil AssetSet")
  2802. }
  2803. if as.Assets == nil {
  2804. as.Assets = map[string]Asset{}
  2805. }
  2806. // need a label config
  2807. // Determine key into which to Insert the Asset.
  2808. k, err := key(asset, as.AggregationKeys, labelConfig)
  2809. if err != nil {
  2810. return err
  2811. }
  2812. // Add the given Asset to the existing entry, if there is one;
  2813. // otherwise just set directly into assets
  2814. if _, ok := as.Assets[k]; !ok {
  2815. as.Assets[k] = asset
  2816. // insert the asset into it's type specific map as well
  2817. addToConcreteMap(as, k, asset)
  2818. } else {
  2819. // possibly creates a new asset type, so we need to update the
  2820. // concrete type mappings
  2821. newAsset := as.Assets[k].Add(asset)
  2822. removeFromConcreteMap(as, k, as.Assets[k])
  2823. // overwrite the existing asset with the new one, and re-add the new
  2824. // asset to the concrete type mappings
  2825. as.Assets[k] = newAsset
  2826. addToConcreteMap(as, k, newAsset)
  2827. }
  2828. // Expand the window, just to be safe. It's possible that the asset will
  2829. // be set into the map without expanding it to the AssetSet's window.
  2830. as.Assets[k].ExpandWindow(as.Window)
  2831. return nil
  2832. }
  2833. // IsEmpty returns true if the AssetSet is nil, or if it contains
  2834. // zero assets.
  2835. func (as *AssetSet) IsEmpty() bool {
  2836. return as == nil || len(as.Assets) == 0
  2837. }
  2838. func (as *AssetSet) Length() int {
  2839. if as == nil {
  2840. return 0
  2841. }
  2842. return len(as.Assets)
  2843. }
  2844. func (as *AssetSet) GetWindow() Window {
  2845. return as.Window
  2846. }
  2847. // Resolution returns the AssetSet's window duration
  2848. func (as *AssetSet) Resolution() time.Duration {
  2849. return as.Window.Duration()
  2850. }
  2851. func (as *AssetSet) Set(asset Asset, aggregateBy []string, labelConfig *LabelConfig) error {
  2852. if as.IsEmpty() {
  2853. as.Assets = map[string]Asset{}
  2854. }
  2855. // Expand the window to match the AssetSet, then set it
  2856. asset.ExpandWindow(as.Window)
  2857. k, err := key(asset, aggregateBy, labelConfig)
  2858. if err != nil {
  2859. return err
  2860. }
  2861. as.Assets[k] = asset
  2862. addToConcreteMap(as, k, asset)
  2863. return nil
  2864. }
  2865. func (as *AssetSet) Start() time.Time {
  2866. return *as.Window.Start()
  2867. }
  2868. func (as *AssetSet) TotalCost() float64 {
  2869. tc := 0.0
  2870. for _, a := range as.Assets {
  2871. tc += a.TotalCost()
  2872. }
  2873. return tc
  2874. }
  2875. func (as *AssetSet) UTCOffset() time.Duration {
  2876. _, zone := as.Start().Zone()
  2877. return time.Duration(zone) * time.Second
  2878. }
  2879. func (as *AssetSet) accumulate(that *AssetSet) (*AssetSet, error) {
  2880. if as == nil {
  2881. return that, nil
  2882. }
  2883. if that == nil {
  2884. return as, nil
  2885. }
  2886. // In the case of an AssetSetRange with empty entries, we may end up with
  2887. // an incoming `as` without an `aggregateBy`, even though we are tring to
  2888. // aggregate here. This handles that case by assigning the correct `aggregateBy`.
  2889. if !sameContents(as.AggregationKeys, that.AggregationKeys) {
  2890. if len(as.AggregationKeys) == 0 {
  2891. as.AggregationKeys = that.AggregationKeys
  2892. }
  2893. }
  2894. // Set start, end to min(start), max(end)
  2895. start := as.Start()
  2896. end := as.End()
  2897. if that.Start().Before(start) {
  2898. start = that.Start()
  2899. }
  2900. if that.End().After(end) {
  2901. end = that.End()
  2902. }
  2903. if as.IsEmpty() && that.IsEmpty() {
  2904. return NewAssetSet(start, end), nil
  2905. }
  2906. acc := NewAssetSet(start, end)
  2907. acc.AggregationKeys = as.AggregationKeys
  2908. for _, asset := range as.Assets {
  2909. err := acc.Insert(asset, nil)
  2910. if err != nil {
  2911. return nil, err
  2912. }
  2913. }
  2914. for _, asset := range that.Assets {
  2915. err := acc.Insert(asset, nil)
  2916. if err != nil {
  2917. return nil, err
  2918. }
  2919. }
  2920. return acc, nil
  2921. }
  2922. func (as *AssetSet) SanitizeNaN() {
  2923. for _, a := range as.Assets {
  2924. a.SanitizeNaN()
  2925. }
  2926. for _, a := range as.Any {
  2927. a.SanitizeNaN()
  2928. }
  2929. for _, c := range as.Cloud {
  2930. c.SanitizeNaN()
  2931. }
  2932. for _, cm := range as.ClusterManagement {
  2933. cm.SanitizeNaN()
  2934. }
  2935. for _, d := range as.Disks {
  2936. d.SanitizeNaN()
  2937. }
  2938. for _, n := range as.Network {
  2939. n.SanitizeNaN()
  2940. }
  2941. for _, n := range as.Nodes {
  2942. n.SanitizeNaN()
  2943. }
  2944. for _, lb := range as.LoadBalancers {
  2945. lb.SanitizeNaN()
  2946. }
  2947. for _, sa := range as.SharedAssets {
  2948. sa.SanitizeNaN()
  2949. }
  2950. }
  2951. type DiffKind string
  2952. const (
  2953. DiffAdded DiffKind = "added"
  2954. DiffRemoved = "removed"
  2955. DiffChanged = "changed"
  2956. )
  2957. // Diff stores an object and a string that denotes whether that object was
  2958. // added or removed from a set of those objects
  2959. type Diff[T any] struct {
  2960. Before T
  2961. After T
  2962. Kind DiffKind
  2963. }
  2964. // DiffAsset takes two AssetSets and returns a map of keys to Diffs by checking
  2965. // the keys of each AssetSet. If a key is not found or is found with a different total cost,
  2966. // a Diff is generated and added to the map. A found asset will only be added to the map if the new
  2967. // total cost is greater than ratioCostChange * the old total cost
  2968. func DiffAsset(before, after *AssetSet, ratioCostChange float64) (map[string]Diff[Asset], error) {
  2969. if ratioCostChange < 0.0 {
  2970. return nil, fmt.Errorf("Percent cost change cannot be less than 0")
  2971. }
  2972. changedItems := map[string]Diff[Asset]{}
  2973. for assetKey1, asset1 := range before.Assets {
  2974. if asset2, ok := after.Assets[assetKey1]; !ok {
  2975. d := Diff[Asset]{asset1, nil, DiffRemoved}
  2976. changedItems[assetKey1] = d
  2977. } else if math.Abs(asset1.TotalCost()-asset2.TotalCost()) > ratioCostChange*asset1.TotalCost() { //check if either value exceeds the other by more than pctCostChange
  2978. d := Diff[Asset]{asset1, asset2, DiffChanged}
  2979. changedItems[assetKey1] = d
  2980. }
  2981. }
  2982. for assetKey2, asset2 := range after.Assets {
  2983. if _, ok := before.Assets[assetKey2]; !ok {
  2984. d := Diff[Asset]{nil, asset2, DiffAdded}
  2985. changedItems[assetKey2] = d
  2986. }
  2987. }
  2988. return changedItems, nil
  2989. }
  2990. // AssetSetRange is a thread-safe slice of AssetSets. It is meant to
  2991. // be used such that the AssetSets held are consecutive and coherent with
  2992. // respect to using the same aggregation Properties, UTC offset, and
  2993. // resolution. However these rules are not necessarily enforced, so use wisely.
  2994. type AssetSetRange struct {
  2995. Assets []*AssetSet
  2996. FromStore string // stores the name of the store used to retrieve the data
  2997. }
  2998. // NewAssetSetRange instantiates a new range composed of the given
  2999. // AssetSets in the order provided.
  3000. func NewAssetSetRange(assets ...*AssetSet) *AssetSetRange {
  3001. return &AssetSetRange{
  3002. Assets: assets,
  3003. }
  3004. }
  3005. func (asr *AssetSetRange) Accumulate(accumulateBy AccumulateOption) (*AssetSetRange, error) {
  3006. switch accumulateBy {
  3007. case AccumulateOptionNone:
  3008. return asr.accumulateByNone()
  3009. case AccumulateOptionAll:
  3010. return asr.accumulateByAll()
  3011. case AccumulateOptionHour:
  3012. return asr.accumulateByHour()
  3013. case AccumulateOptionDay:
  3014. return asr.accumulateByDay()
  3015. case AccumulateOptionWeek:
  3016. return asr.accumulateByWeek()
  3017. case AccumulateOptionMonth:
  3018. return asr.accumulateByMonth()
  3019. default:
  3020. // ideally, this should never happen
  3021. return nil, fmt.Errorf("unexpected error, invalid accumulateByType: %s", accumulateBy)
  3022. }
  3023. }
  3024. func (asr *AssetSetRange) accumulateByNone() (*AssetSetRange, error) {
  3025. return asr.clone(), nil
  3026. }
  3027. func (asr *AssetSetRange) accumulateByAll() (*AssetSetRange, error) {
  3028. var err error
  3029. var as *AssetSet
  3030. as, err = asr.newAccumulation()
  3031. if err != nil {
  3032. return nil, fmt.Errorf("error accumulating all:%s", err)
  3033. }
  3034. accumulated := NewAssetSetRange(as)
  3035. return accumulated, nil
  3036. }
  3037. func (asr *AssetSetRange) accumulateByHour() (*AssetSetRange, error) {
  3038. // ensure that the asset sets have a 1-hour window, if a set exists
  3039. if len(asr.Assets) > 0 && asr.Assets[0].Window.Duration() != time.Hour {
  3040. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", asr.Assets[0].Window.Duration())
  3041. }
  3042. return asr.clone(), nil
  3043. }
  3044. func (asr *AssetSetRange) accumulateByDay() (*AssetSetRange, error) {
  3045. // if the asset set window is 1-day, just return the existing asset set range
  3046. if len(asr.Assets) > 0 && asr.Assets[0].Window.Duration() == time.Hour*24 {
  3047. return asr, nil
  3048. }
  3049. var toAccumulate *AssetSetRange
  3050. result := NewAssetSetRange()
  3051. for i, as := range asr.Assets {
  3052. if as.Window.Duration() != time.Hour {
  3053. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", as.Window.Duration())
  3054. }
  3055. hour := as.Window.Start().Hour()
  3056. if toAccumulate == nil {
  3057. toAccumulate = NewAssetSetRange()
  3058. as = as.Clone()
  3059. }
  3060. toAccumulate.Append(as)
  3061. asAccumulated, err := toAccumulate.accumulate()
  3062. if err != nil {
  3063. return nil, fmt.Errorf("error accumulating result: %s", err)
  3064. }
  3065. toAccumulate = NewAssetSetRange(asAccumulated)
  3066. if hour == 23 || i == len(asr.Assets)-1 {
  3067. if length := len(toAccumulate.Assets); length != 1 {
  3068. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  3069. }
  3070. result.Append(toAccumulate.Assets[0])
  3071. toAccumulate = nil
  3072. }
  3073. }
  3074. return result, nil
  3075. }
  3076. func (asr *AssetSetRange) accumulateByMonth() (*AssetSetRange, error) {
  3077. var toAccumulate *AssetSetRange
  3078. result := NewAssetSetRange()
  3079. for i, as := range asr.Assets {
  3080. if as.Window.Duration() != time.Hour*24 {
  3081. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
  3082. }
  3083. _, month, _ := as.Window.Start().Date()
  3084. _, nextDayMonth, _ := as.Window.Start().Add(time.Hour * 24).Date()
  3085. if toAccumulate == nil {
  3086. toAccumulate = NewAssetSetRange()
  3087. as = as.Clone()
  3088. }
  3089. toAccumulate.Append(as)
  3090. asAccumulated, err := toAccumulate.accumulate()
  3091. if err != nil {
  3092. return nil, fmt.Errorf("error accumulating result: %s", err)
  3093. }
  3094. toAccumulate = NewAssetSetRange(asAccumulated)
  3095. // either the month has ended, or there are no more asset sets
  3096. if month != nextDayMonth || i == len(asr.Assets)-1 {
  3097. if length := len(toAccumulate.Assets); length != 1 {
  3098. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  3099. }
  3100. result.Append(toAccumulate.Assets[0])
  3101. toAccumulate = nil
  3102. }
  3103. }
  3104. return result, nil
  3105. }
  3106. func (asr *AssetSetRange) accumulateByWeek() (*AssetSetRange, error) {
  3107. if len(asr.Assets) > 0 && asr.Assets[0].Window.Duration() == timeutil.Week {
  3108. return asr, nil
  3109. }
  3110. var toAccumulate *AssetSetRange
  3111. result := NewAssetSetRange()
  3112. for i, as := range asr.Assets {
  3113. if as.Window.Duration() != time.Hour*24 {
  3114. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
  3115. }
  3116. dayOfWeek := as.Window.Start().Weekday()
  3117. if toAccumulate == nil {
  3118. toAccumulate = NewAssetSetRange()
  3119. as = as.Clone()
  3120. }
  3121. toAccumulate.Append(as)
  3122. asAccumulated, err := toAccumulate.accumulate()
  3123. if err != nil {
  3124. return nil, fmt.Errorf("error accumulating result: %s", err)
  3125. }
  3126. toAccumulate = NewAssetSetRange(asAccumulated)
  3127. // current assumption is the week always ends on Saturday, or there are no more asset sets
  3128. if dayOfWeek == time.Saturday || i == len(asr.Assets)-1 {
  3129. if length := len(toAccumulate.Assets); length != 1 {
  3130. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  3131. }
  3132. result.Append(toAccumulate.Assets[0])
  3133. toAccumulate = nil
  3134. }
  3135. }
  3136. return result, nil
  3137. }
  3138. func (asr *AssetSetRange) AccumulateToAssetSet() (*AssetSet, error) {
  3139. return asr.accumulate()
  3140. }
  3141. // Accumulate sums each AssetSet in the given range, returning a single cumulative
  3142. // AssetSet for the entire range.
  3143. func (asr *AssetSetRange) accumulate() (*AssetSet, error) {
  3144. var assetSet *AssetSet
  3145. var err error
  3146. for _, as := range asr.Assets {
  3147. assetSet, err = assetSet.accumulate(as)
  3148. if err != nil {
  3149. return nil, err
  3150. }
  3151. }
  3152. return assetSet, nil
  3153. }
  3154. // NewAccumulation clones the first available AssetSet to use as the data structure to
  3155. // accumulate the remaining data. This leaves the original AssetSetRange intact.
  3156. func (asr *AssetSetRange) newAccumulation() (*AssetSet, error) {
  3157. var assetSet *AssetSet
  3158. var err error
  3159. if asr == nil {
  3160. return nil, fmt.Errorf("nil AssetSetRange in accumulation")
  3161. }
  3162. if len(asr.Assets) == 0 {
  3163. return nil, fmt.Errorf("AssetSetRange has empty AssetSet in accumulation")
  3164. }
  3165. for _, as := range asr.Assets {
  3166. if assetSet == nil {
  3167. assetSet = as.Clone()
  3168. continue
  3169. }
  3170. // copy if non-nil
  3171. var assetSetCopy *AssetSet = nil
  3172. if as != nil {
  3173. assetSetCopy = as.Clone()
  3174. }
  3175. // nil is acceptable to pass to accumulate
  3176. assetSet, err = assetSet.accumulate(assetSetCopy)
  3177. if err != nil {
  3178. return nil, err
  3179. }
  3180. }
  3181. return assetSet, nil
  3182. }
  3183. type AssetAggregationOptions struct {
  3184. SharedHourlyCosts map[string]float64
  3185. Filter filter21.Filter
  3186. LabelConfig *LabelConfig
  3187. }
  3188. func (asr *AssetSetRange) AggregateBy(aggregateBy []string, opts *AssetAggregationOptions) error {
  3189. aggRange := &AssetSetRange{Assets: []*AssetSet{}}
  3190. for _, as := range asr.Assets {
  3191. err := as.AggregateBy(aggregateBy, opts)
  3192. if err != nil {
  3193. return err
  3194. }
  3195. aggRange.Assets = append(aggRange.Assets, as)
  3196. }
  3197. asr.Assets = aggRange.Assets
  3198. return nil
  3199. }
  3200. func (asr *AssetSetRange) Append(that *AssetSet) {
  3201. asr.Assets = append(asr.Assets, that)
  3202. }
  3203. // Get provides bounds checked access into the AssetSetRange's AssetSets.
  3204. func (asr *AssetSetRange) Get(i int) (*AssetSet, error) {
  3205. if i < 0 || i >= len(asr.Assets) {
  3206. return nil, fmt.Errorf("AssetSetRange: index out of range: %d", i)
  3207. }
  3208. return asr.Assets[i], nil
  3209. }
  3210. // Length returns the total number of AssetSets in the range.
  3211. func (asr *AssetSetRange) Length() int {
  3212. if asr == nil {
  3213. return 0
  3214. }
  3215. return len(asr.Assets)
  3216. }
  3217. // InsertRange merges the given AssetSetRange into the receiving one by
  3218. // lining up sets with matching windows, then inserting each asset from
  3219. // the given ASR into the respective set in the receiving ASR. If the given
  3220. // ASR contains an AssetSetRange from a window that does not exist in the
  3221. // receiving ASR, then an error is returned. However, the given ASR does not
  3222. // need to cover the full range of the receiver.
  3223. func (asr *AssetSetRange) InsertRange(that *AssetSetRange) error {
  3224. if asr == nil {
  3225. return fmt.Errorf("cannot insert range into nil AssetSetRange")
  3226. }
  3227. // keys maps window to index in asr
  3228. keys := map[string]int{}
  3229. for i, as := range asr.Assets {
  3230. if as == nil {
  3231. continue
  3232. }
  3233. keys[as.Window.String()] = i
  3234. }
  3235. // Nothing to merge, so simply return
  3236. if len(keys) == 0 {
  3237. return nil
  3238. }
  3239. var err error
  3240. for _, thatAS := range that.Assets {
  3241. if thatAS == nil || err != nil {
  3242. continue
  3243. }
  3244. // Find matching AssetSet in asr
  3245. i, ok := keys[thatAS.Window.String()]
  3246. if !ok {
  3247. err = fmt.Errorf("cannot merge AssetSet into window that does not exist: %s", thatAS.Window.String())
  3248. continue
  3249. }
  3250. as, err := asr.Get(i)
  3251. if err != nil {
  3252. err = fmt.Errorf("AssetSetRange index does not exist: %d", i)
  3253. continue
  3254. }
  3255. // Insert each Asset from the given set
  3256. for _, asset := range thatAS.Assets {
  3257. err = as.Insert(asset, nil)
  3258. if err != nil {
  3259. err = fmt.Errorf("error inserting asset: %s", err)
  3260. continue
  3261. }
  3262. }
  3263. }
  3264. // err might be nil
  3265. return err
  3266. }
  3267. func (asr *AssetSetRange) GetWarnings() []string {
  3268. warnings := []string{}
  3269. for _, as := range asr.Assets {
  3270. if len(as.Warnings) > 0 {
  3271. warnings = append(warnings, as.Warnings...)
  3272. }
  3273. }
  3274. return warnings
  3275. }
  3276. func (asr *AssetSetRange) HasWarnings() bool {
  3277. for _, as := range asr.Assets {
  3278. if len(as.Warnings) > 0 {
  3279. return true
  3280. }
  3281. }
  3282. return false
  3283. }
  3284. // IsEmpty returns false if AssetSetRange contains a single AssetSet that is not empty
  3285. func (asr *AssetSetRange) IsEmpty() bool {
  3286. if asr == nil || asr.Length() == 0 {
  3287. return true
  3288. }
  3289. for _, asset := range asr.Assets {
  3290. if !asset.IsEmpty() {
  3291. return false
  3292. }
  3293. }
  3294. return true
  3295. }
  3296. func (asr *AssetSetRange) MarshalJSON() ([]byte, error) {
  3297. if asr == nil {
  3298. return json.Marshal([]*AssetSet{})
  3299. }
  3300. return json.Marshal(asr.Assets)
  3301. }
  3302. // As with AssetSet, AssetSetRange does not serialize all its fields,
  3303. // making it impossible to reconstruct the AssetSetRange from its json.
  3304. // Therefore, the type a marshaled AssetSetRange unmarshals to is
  3305. // AssetSetRangeResponse
  3306. type AssetSetRangeResponse struct {
  3307. Assets []*AssetSetResponse
  3308. }
  3309. func (asr *AssetSetRange) UTCOffset() time.Duration {
  3310. if asr.Length() == 0 {
  3311. return 0
  3312. }
  3313. as, err := asr.Get(0)
  3314. if err != nil {
  3315. return 0
  3316. }
  3317. return as.UTCOffset()
  3318. }
  3319. // Window returns the full window that the AssetSetRange spans, from the
  3320. // start of the first AssetSet to the end of the last one.
  3321. func (asr *AssetSetRange) Window() Window {
  3322. if asr == nil || asr.Length() == 0 {
  3323. return NewWindow(nil, nil)
  3324. }
  3325. start := asr.Assets[0].Start()
  3326. end := asr.Assets[asr.Length()-1].End()
  3327. return NewWindow(&start, &end)
  3328. }
  3329. // Start returns the earliest start of all Assets in the AssetSetRange.
  3330. // It returns an error if there are no assets
  3331. func (asr *AssetSetRange) Start() (time.Time, error) {
  3332. start := time.Time{}
  3333. if asr == nil {
  3334. return start, fmt.Errorf("had no data to compute a start from")
  3335. }
  3336. firstStartNotSet := true
  3337. for _, as := range asr.Assets {
  3338. for _, a := range as.Assets {
  3339. if firstStartNotSet {
  3340. start = a.GetStart()
  3341. firstStartNotSet = false
  3342. }
  3343. if a.GetStart().Before(start) {
  3344. start = a.GetStart()
  3345. }
  3346. }
  3347. }
  3348. if firstStartNotSet {
  3349. return start, fmt.Errorf("had no data to compute a start from")
  3350. }
  3351. return start, nil
  3352. }
  3353. // End returns the latest end of all Assets in the AssetSetRange.
  3354. // It returns an error if there are no assets.
  3355. func (asr *AssetSetRange) End() (time.Time, error) {
  3356. end := time.Time{}
  3357. if asr == nil {
  3358. return end, fmt.Errorf("had no data to compute an end from")
  3359. }
  3360. firstEndNotSet := true
  3361. for _, as := range asr.Assets {
  3362. for _, a := range as.Assets {
  3363. if firstEndNotSet {
  3364. end = a.GetEnd()
  3365. firstEndNotSet = false
  3366. }
  3367. if a.GetEnd().After(end) {
  3368. end = a.GetEnd()
  3369. }
  3370. }
  3371. }
  3372. if firstEndNotSet {
  3373. return end, fmt.Errorf("had no data to compute an end from")
  3374. }
  3375. return end, nil
  3376. }
  3377. // Each iterates over all AssetSets in the AssetSetRange and returns the start and end over
  3378. // the entire range
  3379. func (asr *AssetSetRange) StartAndEnd() (time.Time, time.Time, error) {
  3380. start := time.Time{}
  3381. end := time.Time{}
  3382. if asr == nil {
  3383. return start, end, fmt.Errorf("had no data to compute a start and end from")
  3384. }
  3385. firstStartNotSet := true
  3386. firstEndNotSet := true
  3387. for _, as := range asr.Assets {
  3388. for _, a := range as.Assets {
  3389. if firstStartNotSet {
  3390. start = a.GetStart()
  3391. firstStartNotSet = false
  3392. }
  3393. if a.GetStart().Before(start) {
  3394. start = a.GetStart()
  3395. }
  3396. if firstEndNotSet {
  3397. end = a.GetEnd()
  3398. firstEndNotSet = false
  3399. }
  3400. if a.GetEnd().After(end) {
  3401. end = a.GetEnd()
  3402. }
  3403. }
  3404. }
  3405. if firstStartNotSet {
  3406. return start, end, fmt.Errorf("had no data to compute a start from")
  3407. }
  3408. if firstEndNotSet {
  3409. return start, end, fmt.Errorf("had no data to compute an end from")
  3410. }
  3411. return start, end, nil
  3412. }
  3413. // Minutes returns the duration, in minutes, between the earliest start
  3414. // and the latest end of all assets in the AssetSetRange.
  3415. func (asr *AssetSetRange) Minutes() float64 {
  3416. if asr == nil {
  3417. return 0
  3418. }
  3419. start, end, err := asr.StartAndEnd()
  3420. if err != nil {
  3421. return 0
  3422. }
  3423. duration := end.Sub(start)
  3424. return duration.Minutes()
  3425. }
  3426. // TotalCost returns the AssetSetRange's total cost
  3427. func (asr *AssetSetRange) TotalCost() float64 {
  3428. if asr == nil {
  3429. return 0.0
  3430. }
  3431. tc := 0.0
  3432. for _, as := range asr.Assets {
  3433. tc += as.TotalCost()
  3434. }
  3435. return tc
  3436. }
  3437. func (asr *AssetSetRange) clone() *AssetSetRange {
  3438. asrClone := NewAssetSetRange()
  3439. asrClone.FromStore = asr.FromStore
  3440. for _, as := range asr.Assets {
  3441. asClone := as.Clone()
  3442. asrClone.Append(asClone)
  3443. }
  3444. return asrClone
  3445. }
  3446. // This is a helper type. The Asset API returns a json which cannot be natively
  3447. // unmarshaled into any Asset struct. Therefore, this struct IN COMBINATION WITH
  3448. // DESERIALIZATION LOGIC DEFINED IN asset_json.go can unmarshal a json directly
  3449. // from an Assets API query
  3450. type AssetAPIResponse struct {
  3451. Code int `json:"code"`
  3452. Data AssetSetRangeResponse `json:"data"`
  3453. }
  3454. // Returns true if string slices a and b contain all of the same strings, in any order.
  3455. func sameContents(a, b []string) bool {
  3456. if len(a) != len(b) {
  3457. return false
  3458. }
  3459. for i := range a {
  3460. if !contains(b, a[i]) {
  3461. return false
  3462. }
  3463. }
  3464. return true
  3465. }
  3466. func contains(slice []string, item string) bool {
  3467. for _, element := range slice {
  3468. if element == item {
  3469. return true
  3470. }
  3471. }
  3472. return false
  3473. }
  3474. func GetNodePoolName(provider string, labels map[string]string) string {
  3475. switch provider {
  3476. case AzureProvider:
  3477. return getPoolNameHelper(AKSNodepoolLabel, labels)
  3478. case AWSProvider:
  3479. return getPoolNameHelper(EKSNodepoolLabel, labels)
  3480. case GCPProvider:
  3481. return getPoolNameHelper(GKENodePoolLabel, labels)
  3482. default:
  3483. log.Warnf("node pool name not supported for this provider")
  3484. return ""
  3485. }
  3486. }
  3487. func getPoolNameHelper(label string, labels map[string]string) string {
  3488. sanitizedLabel := regexp.MustCompile(`[^a-zA-Z0-9 ]+`).ReplaceAllString(label, "_")
  3489. if poolName, found := labels[fmt.Sprintf("label_%s", sanitizedLabel)]; found {
  3490. return poolName
  3491. } else {
  3492. log.Warnf("unable to derive node pool name from node labels")
  3493. return ""
  3494. }
  3495. }