allocationprops.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. package kubecost
  2. import (
  3. "fmt"
  4. "sort"
  5. "strings"
  6. "github.com/opencost/opencost/pkg/log"
  7. "github.com/opencost/opencost/pkg/prom"
  8. )
  9. const (
  10. AllocationNilProp string = ""
  11. AllocationClusterProp string = "cluster"
  12. AllocationNodeProp string = "node"
  13. AllocationContainerProp string = "container"
  14. AllocationControllerProp string = "controller"
  15. AllocationControllerKindProp string = "controllerKind"
  16. AllocationNamespaceProp string = "namespace"
  17. AllocationPodProp string = "pod"
  18. AllocationProviderIDProp string = "providerID"
  19. AllocationServiceProp string = "service"
  20. AllocationLabelProp string = "label"
  21. AllocationAnnotationProp string = "annotation"
  22. AllocationDeploymentProp string = "deployment"
  23. AllocationStatefulSetProp string = "statefulset"
  24. AllocationDaemonSetProp string = "daemonset"
  25. AllocationJobProp string = "job"
  26. AllocationDepartmentProp string = "department"
  27. AllocationEnvironmentProp string = "environment"
  28. AllocationOwnerProp string = "owner"
  29. AllocationProductProp string = "product"
  30. AllocationTeamProp string = "team"
  31. )
  32. func ParseProperty(text string) (string, error) {
  33. switch strings.TrimSpace(strings.ToLower(text)) {
  34. case "cluster":
  35. return AllocationClusterProp, nil
  36. case "node":
  37. return AllocationNodeProp, nil
  38. case "container":
  39. return AllocationContainerProp, nil
  40. case "controller":
  41. return AllocationControllerProp, nil
  42. case "controllerkind":
  43. return AllocationControllerKindProp, nil
  44. case "namespace":
  45. return AllocationNamespaceProp, nil
  46. case "pod":
  47. return AllocationPodProp, nil
  48. case "providerid":
  49. return AllocationProviderIDProp, nil
  50. case "service":
  51. return AllocationServiceProp, nil
  52. case "label":
  53. return AllocationLabelProp, nil
  54. case "annotation":
  55. return AllocationAnnotationProp, nil
  56. case "deployment":
  57. return AllocationDeploymentProp, nil
  58. case "daemonset":
  59. return AllocationDaemonSetProp, nil
  60. case "statefulset":
  61. return AllocationStatefulSetProp, nil
  62. case "job":
  63. return AllocationJobProp, nil
  64. case "department":
  65. return AllocationDepartmentProp, nil
  66. case "environment":
  67. return AllocationEnvironmentProp, nil
  68. case "owner":
  69. return AllocationOwnerProp, nil
  70. case "product":
  71. return AllocationProductProp, nil
  72. case "team":
  73. return AllocationTeamProp, nil
  74. }
  75. if strings.HasPrefix(text, "label:") {
  76. label := prom.SanitizeLabelName(strings.TrimSpace(strings.TrimPrefix(text, "label:")))
  77. return fmt.Sprintf("label:%s", label), nil
  78. }
  79. if strings.HasPrefix(text, "annotation:") {
  80. annotation := prom.SanitizeLabelName(strings.TrimSpace(strings.TrimPrefix(text, "annotation:")))
  81. return fmt.Sprintf("annotation:%s", annotation), nil
  82. }
  83. return AllocationNilProp, fmt.Errorf("invalid allocation property: %s", text)
  84. }
  85. // AllocationProperties describes a set of Kubernetes objects.
  86. type AllocationProperties struct {
  87. Cluster string `json:"cluster,omitempty"`
  88. Node string `json:"node,omitempty"`
  89. Container string `json:"container,omitempty"`
  90. Controller string `json:"controller,omitempty"`
  91. ControllerKind string `json:"controllerKind,omitempty"`
  92. Namespace string `json:"namespace,omitempty"`
  93. Pod string `json:"pod,omitempty"`
  94. Services []string `json:"services,omitempty"`
  95. ProviderID string `json:"providerID,omitempty"`
  96. Labels AllocationLabels `json:"labels,omitempty"`
  97. Annotations AllocationAnnotations `json:"annotations,omitempty"`
  98. NamespaceLabels AllocationLabels `json:"namespaceLabels,omitempty"` // @bingen:field[version=17]
  99. NamespaceAnnotations AllocationAnnotations `json:"namespaceAnnotations,omitempty"` // @bingen:field[version=17]
  100. // When set to true, maintain the intersection of all labels + annotations
  101. // in the aggregated AllocationProperties object
  102. AggregatedMetadata bool `json:"-"` //@bingen:field[ignore]
  103. }
  104. // AllocationLabels is a schema-free mapping of key/value pairs that can be
  105. // attributed to an Allocation
  106. type AllocationLabels map[string]string
  107. // AllocationAnnotations is a schema-free mapping of key/value pairs that can be
  108. // attributed to an Allocation
  109. type AllocationAnnotations map[string]string
  110. func (p *AllocationProperties) Clone() *AllocationProperties {
  111. if p == nil {
  112. return nil
  113. }
  114. clone := &AllocationProperties{}
  115. clone.Cluster = p.Cluster
  116. clone.Node = p.Node
  117. clone.Container = p.Container
  118. clone.Controller = p.Controller
  119. clone.ControllerKind = p.ControllerKind
  120. clone.Namespace = p.Namespace
  121. clone.Pod = p.Pod
  122. clone.ProviderID = p.ProviderID
  123. var services []string
  124. services = append(services, p.Services...)
  125. clone.Services = services
  126. labels := make(map[string]string, len(p.Labels))
  127. for k, v := range p.Labels {
  128. labels[k] = v
  129. }
  130. clone.Labels = labels
  131. nsLabels := make(map[string]string, len(p.NamespaceLabels))
  132. for k, v := range p.NamespaceLabels {
  133. nsLabels[k] = v
  134. }
  135. clone.NamespaceLabels = nsLabels
  136. annotations := make(map[string]string, len(p.Annotations))
  137. for k, v := range p.Annotations {
  138. annotations[k] = v
  139. }
  140. clone.Annotations = annotations
  141. nsAnnotations := make(map[string]string, len(p.NamespaceAnnotations))
  142. for k, v := range p.NamespaceAnnotations {
  143. nsAnnotations[k] = v
  144. }
  145. clone.NamespaceAnnotations = nsAnnotations
  146. clone.AggregatedMetadata = p.AggregatedMetadata
  147. return clone
  148. }
  149. func (p *AllocationProperties) Equal(that *AllocationProperties) bool {
  150. if p == nil || that == nil {
  151. return false
  152. }
  153. if p.Cluster != that.Cluster {
  154. return false
  155. }
  156. if p.Node != that.Node {
  157. return false
  158. }
  159. if p.Container != that.Container {
  160. return false
  161. }
  162. if p.Controller != that.Controller {
  163. return false
  164. }
  165. if p.ControllerKind != that.ControllerKind {
  166. return false
  167. }
  168. if p.Namespace != that.Namespace {
  169. return false
  170. }
  171. if p.Pod != that.Pod {
  172. return false
  173. }
  174. if p.ProviderID != that.ProviderID {
  175. return false
  176. }
  177. pLabels := p.Labels
  178. thatLabels := that.Labels
  179. if len(pLabels) == len(thatLabels) {
  180. for k, pv := range pLabels {
  181. tv, ok := thatLabels[k]
  182. if !ok || tv != pv {
  183. return false
  184. }
  185. }
  186. } else {
  187. return false
  188. }
  189. pNamespaceLabels := p.NamespaceLabels
  190. thatNamespaceLabels := that.NamespaceLabels
  191. if len(pNamespaceLabels) == len(thatNamespaceLabels) {
  192. for k, pv := range pNamespaceLabels {
  193. tv, ok := thatNamespaceLabels[k]
  194. if !ok || tv != pv {
  195. return false
  196. }
  197. }
  198. } else {
  199. return false
  200. }
  201. pAnnotations := p.Annotations
  202. thatAnnotations := that.Annotations
  203. if len(pAnnotations) == len(thatAnnotations) {
  204. for k, pv := range pAnnotations {
  205. tv, ok := thatAnnotations[k]
  206. if !ok || tv != pv {
  207. return false
  208. }
  209. }
  210. } else {
  211. return false
  212. }
  213. pNamespaceAnnotations := p.NamespaceAnnotations
  214. thatNamespaceAnnotations := that.NamespaceAnnotations
  215. if len(pNamespaceAnnotations) == len(thatNamespaceAnnotations) {
  216. for k, pv := range pNamespaceAnnotations {
  217. tv, ok := thatNamespaceAnnotations[k]
  218. if !ok || tv != pv {
  219. return false
  220. }
  221. }
  222. } else {
  223. return false
  224. }
  225. pServices := p.Services
  226. thatServices := that.Services
  227. if len(pServices) == len(thatServices) {
  228. sort.Strings(pServices)
  229. sort.Strings(thatServices)
  230. for i, pv := range pServices {
  231. tv := thatServices[i]
  232. if tv != pv {
  233. return false
  234. }
  235. }
  236. } else {
  237. return false
  238. }
  239. return true
  240. }
  241. // GenerateKey generates a string that represents the key by which the
  242. // AllocationProperties should be aggregated, given the properties defined by
  243. // the aggregateBy parameter and the given label configuration.
  244. func (p *AllocationProperties) GenerateKey(aggregateBy []string, labelConfig *LabelConfig) string {
  245. if p == nil {
  246. return ""
  247. }
  248. if labelConfig == nil {
  249. labelConfig = NewLabelConfig()
  250. }
  251. // Names will ultimately be joined into a single name, which uniquely
  252. // identifies allocations.
  253. names := []string{}
  254. for _, agg := range aggregateBy {
  255. switch true {
  256. case agg == AllocationClusterProp:
  257. names = append(names, p.Cluster)
  258. case agg == AllocationNodeProp:
  259. names = append(names, p.Node)
  260. case agg == AllocationNamespaceProp:
  261. names = append(names, p.Namespace)
  262. case agg == AllocationControllerKindProp:
  263. controllerKind := p.ControllerKind
  264. if controllerKind == "" {
  265. // Indicate that allocation has no controller
  266. controllerKind = UnallocatedSuffix
  267. }
  268. names = append(names, controllerKind)
  269. case agg == AllocationDaemonSetProp || agg == AllocationStatefulSetProp || agg == AllocationDeploymentProp || agg == AllocationJobProp:
  270. controller := p.Controller
  271. if agg != p.ControllerKind || controller == "" {
  272. // The allocation does not have the specified controller kind
  273. controller = UnallocatedSuffix
  274. }
  275. names = append(names, controller)
  276. case agg == AllocationControllerProp:
  277. controller := p.Controller
  278. if controller == "" {
  279. // Indicate that allocation has no controller
  280. controller = UnallocatedSuffix
  281. } else if p.ControllerKind != "" {
  282. controller = fmt.Sprintf("%s:%s", p.ControllerKind, controller)
  283. }
  284. names = append(names, controller)
  285. case agg == AllocationPodProp:
  286. names = append(names, p.Pod)
  287. case agg == AllocationContainerProp:
  288. names = append(names, p.Container)
  289. case agg == AllocationServiceProp:
  290. services := p.Services
  291. if len(services) == 0 {
  292. // Indicate that allocation has no services
  293. names = append(names, UnallocatedSuffix)
  294. } else {
  295. // Unmounted load balancers lead to __unmounted__ Allocations whose
  296. // services field is populated. If we don't have a special case, the
  297. // __unmounted__ Allocation will be transformed into a regular Allocation,
  298. // causing issues with AggregateBy and drilldown
  299. if p.Pod == UnmountedSuffix || p.Namespace == UnmountedSuffix || p.Container == UnmountedSuffix {
  300. names = append(names, UnmountedSuffix)
  301. } else {
  302. // This just uses the first service
  303. for _, service := range services {
  304. names = append(names, service)
  305. break
  306. }
  307. }
  308. }
  309. case strings.HasPrefix(agg, "label:"):
  310. labels := p.Labels
  311. if labels == nil {
  312. names = append(names, UnallocatedSuffix)
  313. } else {
  314. labelName := labelConfig.Sanitize(strings.TrimPrefix(agg, "label:"))
  315. if labelValue, ok := labels[labelName]; ok {
  316. names = append(names, fmt.Sprintf("%s", labelValue))
  317. } else {
  318. names = append(names, UnallocatedSuffix)
  319. }
  320. }
  321. case strings.HasPrefix(agg, "annotation:"):
  322. annotations := p.Annotations
  323. if annotations == nil {
  324. names = append(names, UnallocatedSuffix)
  325. } else {
  326. annotationName := labelConfig.Sanitize(strings.TrimPrefix(agg, "annotation:"))
  327. if annotationValue, ok := annotations[annotationName]; ok {
  328. names = append(names, fmt.Sprintf("%s", annotationValue))
  329. } else {
  330. names = append(names, UnallocatedSuffix)
  331. }
  332. }
  333. case agg == AllocationDepartmentProp:
  334. labels := p.Labels
  335. annotations := p.Annotations
  336. if labels == nil && annotations == nil {
  337. names = append(names, UnallocatedSuffix)
  338. } else {
  339. labelNames := strings.Split(labelConfig.DepartmentLabel, ",")
  340. for _, labelName := range labelNames {
  341. labelName = labelConfig.Sanitize(labelName)
  342. if labelValue, ok := labels[labelName]; ok {
  343. names = append(names, labelValue)
  344. } else if annotationValue, ok := annotations[labelName]; ok {
  345. names = append(names, annotationValue)
  346. } else {
  347. names = append(names, UnallocatedSuffix)
  348. }
  349. }
  350. }
  351. case agg == AllocationEnvironmentProp:
  352. labels := p.Labels
  353. annotations := p.Annotations
  354. if labels == nil && annotations == nil {
  355. names = append(names, UnallocatedSuffix)
  356. } else {
  357. labelNames := strings.Split(labelConfig.EnvironmentLabel, ",")
  358. for _, labelName := range labelNames {
  359. labelName = labelConfig.Sanitize(labelName)
  360. if labelValue, ok := labels[labelName]; ok {
  361. names = append(names, labelValue)
  362. } else if annotationValue, ok := annotations[labelName]; ok {
  363. names = append(names, annotationValue)
  364. } else {
  365. names = append(names, UnallocatedSuffix)
  366. }
  367. }
  368. }
  369. case agg == AllocationOwnerProp:
  370. labels := p.Labels
  371. annotations := p.Annotations
  372. if labels == nil && annotations == nil {
  373. names = append(names, UnallocatedSuffix)
  374. } else {
  375. labelNames := strings.Split(labelConfig.OwnerLabel, ",")
  376. for _, labelName := range labelNames {
  377. labelName = labelConfig.Sanitize(labelName)
  378. if labelValue, ok := labels[labelName]; ok {
  379. names = append(names, labelValue)
  380. } else if annotationValue, ok := annotations[labelName]; ok {
  381. names = append(names, annotationValue)
  382. } else {
  383. names = append(names, UnallocatedSuffix)
  384. }
  385. }
  386. }
  387. case agg == AllocationProductProp:
  388. labels := p.Labels
  389. annotations := p.Annotations
  390. if labels == nil && annotations == nil {
  391. names = append(names, UnallocatedSuffix)
  392. } else {
  393. labelNames := strings.Split(labelConfig.ProductLabel, ",")
  394. for _, labelName := range labelNames {
  395. labelName = labelConfig.Sanitize(labelName)
  396. if labelValue, ok := labels[labelName]; ok {
  397. names = append(names, labelValue)
  398. } else if annotationValue, ok := annotations[labelName]; ok {
  399. names = append(names, annotationValue)
  400. } else {
  401. names = append(names, UnallocatedSuffix)
  402. }
  403. }
  404. }
  405. case agg == AllocationTeamProp:
  406. labels := p.Labels
  407. annotations := p.Annotations
  408. if labels == nil && annotations == nil {
  409. names = append(names, UnallocatedSuffix)
  410. } else {
  411. labelNames := strings.Split(labelConfig.TeamLabel, ",")
  412. for _, labelName := range labelNames {
  413. labelName = labelConfig.Sanitize(labelName)
  414. if labelValue, ok := labels[labelName]; ok {
  415. names = append(names, labelValue)
  416. } else if annotationValue, ok := annotations[labelName]; ok {
  417. names = append(names, annotationValue)
  418. } else {
  419. names = append(names, UnallocatedSuffix)
  420. }
  421. }
  422. }
  423. default:
  424. // This case should never be reached, as input up until this point
  425. // should be checked and rejected if invalid. But if we do get a
  426. // value we don't recognize, log a warning.
  427. log.Warnf("generateKey: illegal aggregation parameter: %s", agg)
  428. }
  429. }
  430. return strings.Join(names, "/")
  431. }
  432. // Intersection returns an *AllocationProperties which contains all matching fields between the calling and parameter AllocationProperties
  433. // nillable slices and maps are left as nil
  434. func (p *AllocationProperties) Intersection(that *AllocationProperties) *AllocationProperties {
  435. if p == nil || that == nil {
  436. return nil
  437. }
  438. intersectionProps := &AllocationProperties{}
  439. if p.Cluster == that.Cluster {
  440. intersectionProps.Cluster = p.Cluster
  441. }
  442. if p.Node == that.Node {
  443. intersectionProps.Node = p.Node
  444. }
  445. if p.Container == that.Container {
  446. intersectionProps.Container = p.Container
  447. }
  448. if p.Controller == that.Controller {
  449. intersectionProps.Controller = p.Controller
  450. }
  451. if p.ControllerKind == that.ControllerKind {
  452. intersectionProps.ControllerKind = p.ControllerKind
  453. }
  454. if p.Namespace == that.Namespace {
  455. intersectionProps.Namespace = p.Namespace
  456. // CORE-140: In the case that the namespace is the same, also copy over the namespaceLabels and annotations
  457. // Note - assume that if the namespace is the same on both, then namespace label/annotation sets
  458. // will be the same, so just carry one set over
  459. if p.Container == UnmountedSuffix {
  460. // This logic is designed to effectively ignore the unmounted/unallocated objects
  461. // and just copy over the labels from the other, 'legitimate' allocation.
  462. intersectionProps.NamespaceLabels = copyStringMap(that.NamespaceLabels)
  463. intersectionProps.NamespaceAnnotations = copyStringMap(that.NamespaceAnnotations)
  464. } else {
  465. intersectionProps.NamespaceLabels = copyStringMap(p.NamespaceLabels)
  466. intersectionProps.NamespaceAnnotations = copyStringMap(p.NamespaceAnnotations)
  467. }
  468. // ignore the incoming labels from unallocated or unmounted special case pods
  469. if p.AggregatedMetadata || that.AggregatedMetadata {
  470. intersectionProps.AggregatedMetadata = true
  471. // When aggregating by metadata, we maintain the intersection of the labels/annotations
  472. // of the two AllocationProperties objects being intersected here.
  473. // Special case unallocated/unmounted Allocations never have any labels or annotations.
  474. // As a result, they have the effect of always clearing out the intersection,
  475. // regardless if all the other actual allocations/etc have them.
  476. // This logic is designed to effectively ignore the unmounted/unallocated objects
  477. // and just copy over the labels from the other object - we only take the intersection
  478. // of 'legitimate' allocations.
  479. if p.Container == UnmountedSuffix {
  480. intersectionProps.Annotations = that.Annotations
  481. intersectionProps.Labels = that.Labels
  482. } else if that.Container == UnmountedSuffix {
  483. intersectionProps.Annotations = p.Annotations
  484. intersectionProps.Labels = p.Labels
  485. } else {
  486. intersectionProps.Annotations = mapIntersection(p.Annotations, that.Annotations)
  487. intersectionProps.Labels = mapIntersection(p.Labels, that.Labels)
  488. }
  489. }
  490. }
  491. if p.Pod == that.Pod {
  492. intersectionProps.Pod = p.Pod
  493. }
  494. if p.ProviderID == that.ProviderID {
  495. intersectionProps.ProviderID = p.ProviderID
  496. }
  497. return intersectionProps
  498. }
  499. func copyStringMap(original map[string]string) map[string]string {
  500. copy := make(map[string]string)
  501. for key, value := range original {
  502. copy[key] = value
  503. }
  504. return copy
  505. }
  506. func mapIntersection(map1, map2 map[string]string) map[string]string {
  507. result := make(map[string]string)
  508. for key, value := range map1 {
  509. if value2, ok := map2[key]; ok {
  510. if value2 == value {
  511. result[key] = value
  512. }
  513. }
  514. }
  515. return result
  516. }
  517. func (p *AllocationProperties) String() string {
  518. if p == nil {
  519. return "<nil>"
  520. }
  521. strs := []string{}
  522. if p.Cluster != "" {
  523. strs = append(strs, "Cluster:"+p.Cluster)
  524. }
  525. if p.Node != "" {
  526. strs = append(strs, "Node:"+p.Node)
  527. }
  528. if p.Container != "" {
  529. strs = append(strs, "Container:"+p.Container)
  530. }
  531. if p.Controller != "" {
  532. strs = append(strs, "Controller:"+p.Controller)
  533. }
  534. if p.ControllerKind != "" {
  535. strs = append(strs, "ControllerKind:"+p.ControllerKind)
  536. }
  537. if p.Namespace != "" {
  538. strs = append(strs, "Namespace:"+p.Namespace)
  539. }
  540. if p.Pod != "" {
  541. strs = append(strs, "Pod:"+p.Pod)
  542. }
  543. if p.ProviderID != "" {
  544. strs = append(strs, "ProviderID:"+p.ProviderID)
  545. }
  546. if len(p.Services) > 0 {
  547. strs = append(strs, "Services:"+strings.Join(p.Services, ";"))
  548. }
  549. var labelStrs []string
  550. for k, prop := range p.Labels {
  551. labelStrs = append(labelStrs, fmt.Sprintf("%s:%s", k, prop))
  552. }
  553. strs = append(strs, fmt.Sprintf("Labels:{%s}", strings.Join(labelStrs, ",")))
  554. var nsLabelStrs []string
  555. for k, prop := range p.NamespaceLabels {
  556. nsLabelStrs = append(nsLabelStrs, fmt.Sprintf("%s:%s", k, prop))
  557. }
  558. strs = append(strs, fmt.Sprintf("NamespaceLabels:{%s}", strings.Join(nsLabelStrs, ",")))
  559. var annotationStrs []string
  560. for k, prop := range p.Annotations {
  561. annotationStrs = append(annotationStrs, fmt.Sprintf("%s:%s", k, prop))
  562. }
  563. strs = append(strs, fmt.Sprintf("Annotations:{%s}", strings.Join(annotationStrs, ",")))
  564. var nsAnnotationStrs []string
  565. for k, prop := range p.NamespaceAnnotations {
  566. nsAnnotationStrs = append(nsAnnotationStrs, fmt.Sprintf("%s:%s", k, prop))
  567. }
  568. strs = append(strs, fmt.Sprintf("NamespaceAnnotations:{%s}", strings.Join(nsAnnotationStrs, ",")))
  569. return fmt.Sprintf("{%s}", strings.Join(strs, "; "))
  570. }