allocationprops.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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. }
  99. // AllocationLabels is a schema-free mapping of key/value pairs that can be
  100. // attributed to an Allocation
  101. type AllocationLabels map[string]string
  102. // AllocationAnnotations is a schema-free mapping of key/value pairs that can be
  103. // attributed to an Allocation
  104. type AllocationAnnotations map[string]string
  105. func (p *AllocationProperties) Clone() *AllocationProperties {
  106. if p == nil {
  107. return nil
  108. }
  109. clone := &AllocationProperties{}
  110. clone.Cluster = p.Cluster
  111. clone.Node = p.Node
  112. clone.Container = p.Container
  113. clone.Controller = p.Controller
  114. clone.ControllerKind = p.ControllerKind
  115. clone.Namespace = p.Namespace
  116. clone.Pod = p.Pod
  117. clone.ProviderID = p.ProviderID
  118. var services []string
  119. services = append(services, p.Services...)
  120. clone.Services = services
  121. labels := make(map[string]string, len(p.Labels))
  122. for k, v := range p.Labels {
  123. labels[k] = v
  124. }
  125. clone.Labels = labels
  126. annotations := make(map[string]string, len(p.Annotations))
  127. for k, v := range p.Annotations {
  128. annotations[k] = v
  129. }
  130. clone.Annotations = annotations
  131. return clone
  132. }
  133. func (p *AllocationProperties) Equal(that *AllocationProperties) bool {
  134. if p == nil || that == nil {
  135. return false
  136. }
  137. if p.Cluster != that.Cluster {
  138. return false
  139. }
  140. if p.Node != that.Node {
  141. return false
  142. }
  143. if p.Container != that.Container {
  144. return false
  145. }
  146. if p.Controller != that.Controller {
  147. return false
  148. }
  149. if p.ControllerKind != that.ControllerKind {
  150. return false
  151. }
  152. if p.Namespace != that.Namespace {
  153. return false
  154. }
  155. if p.Pod != that.Pod {
  156. return false
  157. }
  158. if p.ProviderID != that.ProviderID {
  159. return false
  160. }
  161. pLabels := p.Labels
  162. thatLabels := that.Labels
  163. if len(pLabels) == len(thatLabels) {
  164. for k, pv := range pLabels {
  165. tv, ok := thatLabels[k]
  166. if !ok || tv != pv {
  167. return false
  168. }
  169. }
  170. } else {
  171. return false
  172. }
  173. pAnnotations := p.Annotations
  174. thatAnnotations := that.Annotations
  175. if len(pAnnotations) == len(thatAnnotations) {
  176. for k, pv := range pAnnotations {
  177. tv, ok := thatAnnotations[k]
  178. if !ok || tv != pv {
  179. return false
  180. }
  181. }
  182. } else {
  183. return false
  184. }
  185. pServices := p.Services
  186. thatServices := that.Services
  187. if len(pServices) == len(thatServices) {
  188. sort.Strings(pServices)
  189. sort.Strings(thatServices)
  190. for i, pv := range pServices {
  191. tv := thatServices[i]
  192. if tv != pv {
  193. return false
  194. }
  195. }
  196. } else {
  197. return false
  198. }
  199. return true
  200. }
  201. // GenerateKey generates a string that represents the key by which the
  202. // AllocationProperties should be aggregated, given the properties defined by
  203. // the aggregateBy parameter and the given label configuration.
  204. func (p *AllocationProperties) GenerateKey(aggregateBy []string, labelConfig *LabelConfig) string {
  205. if p == nil {
  206. return ""
  207. }
  208. if labelConfig == nil {
  209. labelConfig = NewLabelConfig()
  210. }
  211. // Names will ultimately be joined into a single name, which uniquely
  212. // identifies allocations.
  213. names := []string{}
  214. for _, agg := range aggregateBy {
  215. switch true {
  216. case agg == AllocationClusterProp:
  217. names = append(names, p.Cluster)
  218. case agg == AllocationNodeProp:
  219. names = append(names, p.Node)
  220. case agg == AllocationNamespaceProp:
  221. names = append(names, p.Namespace)
  222. case agg == AllocationControllerKindProp:
  223. controllerKind := p.ControllerKind
  224. if controllerKind == "" {
  225. // Indicate that allocation has no controller
  226. controllerKind = UnallocatedSuffix
  227. }
  228. names = append(names, controllerKind)
  229. case agg == AllocationDaemonSetProp || agg == AllocationStatefulSetProp || agg == AllocationDeploymentProp || agg == AllocationJobProp:
  230. controller := p.Controller
  231. if agg != p.ControllerKind || controller == "" {
  232. // The allocation does not have the specified controller kind
  233. controller = UnallocatedSuffix
  234. }
  235. names = append(names, controller)
  236. case agg == AllocationControllerProp:
  237. controller := p.Controller
  238. if controller == "" {
  239. // Indicate that allocation has no controller
  240. controller = UnallocatedSuffix
  241. } else if p.ControllerKind != "" {
  242. controller = fmt.Sprintf("%s:%s", p.ControllerKind, controller)
  243. }
  244. names = append(names, controller)
  245. case agg == AllocationPodProp:
  246. names = append(names, p.Pod)
  247. case agg == AllocationContainerProp:
  248. names = append(names, p.Container)
  249. case agg == AllocationServiceProp:
  250. services := p.Services
  251. if len(services) == 0 {
  252. // Indicate that allocation has no services
  253. names = append(names, UnallocatedSuffix)
  254. } else {
  255. // This just uses the first service
  256. for _, service := range services {
  257. names = append(names, service)
  258. break
  259. }
  260. }
  261. case strings.HasPrefix(agg, "label:"):
  262. labels := p.Labels
  263. if labels == nil {
  264. names = append(names, UnallocatedSuffix)
  265. } else {
  266. labelName := labelConfig.Sanitize(strings.TrimPrefix(agg, "label:"))
  267. if labelValue, ok := labels[labelName]; ok {
  268. names = append(names, fmt.Sprintf("%s=%s", labelName, labelValue))
  269. } else {
  270. names = append(names, UnallocatedSuffix)
  271. }
  272. }
  273. case strings.HasPrefix(agg, "annotation:"):
  274. annotations := p.Annotations
  275. if annotations == nil {
  276. names = append(names, UnallocatedSuffix)
  277. } else {
  278. annotationName := labelConfig.Sanitize(strings.TrimPrefix(agg, "annotation:"))
  279. if annotationValue, ok := annotations[annotationName]; ok {
  280. names = append(names, fmt.Sprintf("%s=%s", annotationName, annotationValue))
  281. } else {
  282. names = append(names, UnallocatedSuffix)
  283. }
  284. }
  285. case agg == AllocationDepartmentProp:
  286. labels := p.Labels
  287. if labels == nil {
  288. names = append(names, UnallocatedSuffix)
  289. } else {
  290. labelNames := strings.Split(labelConfig.DepartmentLabel, ",")
  291. for _, labelName := range labelNames {
  292. labelName = labelConfig.Sanitize(labelName)
  293. if labelValue, ok := labels[labelName]; ok {
  294. names = append(names, labelValue)
  295. } else {
  296. names = append(names, UnallocatedSuffix)
  297. }
  298. }
  299. }
  300. case agg == AllocationEnvironmentProp:
  301. labels := p.Labels
  302. if labels == nil {
  303. names = append(names, UnallocatedSuffix)
  304. } else {
  305. labelNames := strings.Split(labelConfig.EnvironmentLabel, ",")
  306. for _, labelName := range labelNames {
  307. labelName = labelConfig.Sanitize(labelName)
  308. if labelValue, ok := labels[labelName]; ok {
  309. names = append(names, labelValue)
  310. } else {
  311. names = append(names, UnallocatedSuffix)
  312. }
  313. }
  314. }
  315. case agg == AllocationOwnerProp:
  316. labels := p.Labels
  317. if labels == nil {
  318. names = append(names, UnallocatedSuffix)
  319. } else {
  320. labelNames := strings.Split(labelConfig.OwnerLabel, ",")
  321. for _, labelName := range labelNames {
  322. labelName = labelConfig.Sanitize(labelName)
  323. if labelValue, ok := labels[labelName]; ok {
  324. names = append(names, labelValue)
  325. } else {
  326. names = append(names, UnallocatedSuffix)
  327. }
  328. }
  329. }
  330. case agg == AllocationProductProp:
  331. labels := p.Labels
  332. if labels == nil {
  333. names = append(names, UnallocatedSuffix)
  334. } else {
  335. labelNames := strings.Split(labelConfig.ProductLabel, ",")
  336. for _, labelName := range labelNames {
  337. labelName = labelConfig.Sanitize(labelName)
  338. if labelValue, ok := labels[labelName]; ok {
  339. names = append(names, labelValue)
  340. } else {
  341. names = append(names, UnallocatedSuffix)
  342. }
  343. }
  344. }
  345. case agg == AllocationTeamProp:
  346. labels := p.Labels
  347. if labels == nil {
  348. names = append(names, UnallocatedSuffix)
  349. } else {
  350. labelNames := strings.Split(labelConfig.TeamLabel, ",")
  351. for _, labelName := range labelNames {
  352. labelName = labelConfig.Sanitize(labelName)
  353. if labelValue, ok := labels[labelName]; ok {
  354. names = append(names, labelValue)
  355. } else {
  356. names = append(names, UnallocatedSuffix)
  357. }
  358. }
  359. }
  360. default:
  361. // This case should never be reached, as input up until this point
  362. // should be checked and rejected if invalid. But if we do get a
  363. // value we don't recognize, log a warning.
  364. log.Warnf("generateKey: illegal aggregation parameter: %s", agg)
  365. }
  366. }
  367. return strings.Join(names, "/")
  368. }
  369. // Intersection returns an *AllocationProperties which contains all matching fields between the calling and parameter AllocationProperties
  370. // nillable slices and maps are left as nil
  371. func (p *AllocationProperties) Intersection(that *AllocationProperties) *AllocationProperties {
  372. if p == nil || that == nil {
  373. return nil
  374. }
  375. intersectionProps := &AllocationProperties{}
  376. if p.Cluster == that.Cluster {
  377. intersectionProps.Cluster = p.Cluster
  378. }
  379. if p.Node == that.Node {
  380. intersectionProps.Node = p.Node
  381. }
  382. if p.Container == that.Container {
  383. intersectionProps.Container = p.Container
  384. }
  385. if p.Controller == that.Controller {
  386. intersectionProps.Controller = p.Controller
  387. }
  388. if p.ControllerKind == that.ControllerKind {
  389. intersectionProps.ControllerKind = p.ControllerKind
  390. }
  391. if p.Namespace == that.Namespace {
  392. intersectionProps.Namespace = p.Namespace
  393. }
  394. if p.Pod == that.Pod {
  395. intersectionProps.Pod = p.Pod
  396. }
  397. if p.ProviderID == that.ProviderID {
  398. intersectionProps.ProviderID = p.ProviderID
  399. }
  400. return intersectionProps
  401. }
  402. func (p *AllocationProperties) String() string {
  403. if p == nil {
  404. return "<nil>"
  405. }
  406. strs := []string{}
  407. if p.Cluster != "" {
  408. strs = append(strs, "Cluster:"+p.Cluster)
  409. }
  410. if p.Node != "" {
  411. strs = append(strs, "Node:"+p.Node)
  412. }
  413. if p.Container != "" {
  414. strs = append(strs, "Container:"+p.Container)
  415. }
  416. if p.Controller != "" {
  417. strs = append(strs, "Controller:"+p.Controller)
  418. }
  419. if p.ControllerKind != "" {
  420. strs = append(strs, "ControllerKind:"+p.ControllerKind)
  421. }
  422. if p.Namespace != "" {
  423. strs = append(strs, "Namespace:"+p.Namespace)
  424. }
  425. if p.Pod != "" {
  426. strs = append(strs, "Pod:"+p.Pod)
  427. }
  428. if p.ProviderID != "" {
  429. strs = append(strs, "ProviderID:"+p.ProviderID)
  430. }
  431. if len(p.Services) > 0 {
  432. strs = append(strs, "Services:"+strings.Join(p.Services, ";"))
  433. }
  434. var labelStrs []string
  435. for k, prop := range p.Labels {
  436. labelStrs = append(labelStrs, fmt.Sprintf("%s:%s", k, prop))
  437. }
  438. strs = append(strs, fmt.Sprintf("Labels:{%s}", strings.Join(labelStrs, ",")))
  439. var annotationStrs []string
  440. for k, prop := range p.Annotations {
  441. annotationStrs = append(annotationStrs, fmt.Sprintf("%s:%s", k, prop))
  442. }
  443. strs = append(strs, fmt.Sprintf("Annotations:{%s}", strings.Join(annotationStrs, ",")))
  444. return fmt.Sprintf("{%s}", strings.Join(strs, "; "))
  445. }