usage.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package usage
  2. import (
  3. "errors"
  4. "time"
  5. "github.com/porter-dev/porter/api/types"
  6. "github.com/porter-dev/porter/internal/kubernetes"
  7. "github.com/porter-dev/porter/internal/kubernetes/nodes"
  8. "github.com/porter-dev/porter/internal/models"
  9. "github.com/porter-dev/porter/internal/repository"
  10. "golang.org/x/oauth2"
  11. "gorm.io/gorm"
  12. )
  13. type GetUsageOpts struct {
  14. Repo repository.Repository
  15. DOConf *oauth2.Config
  16. Project *models.Project
  17. WhitelistedUsers map[uint]uint
  18. }
  19. // GetUsage gets a project's current usage and usage limit
  20. func GetUsage(opts *GetUsageOpts) (
  21. current, limit *types.ProjectUsage,
  22. resourceUse *models.ProjectUsageCache,
  23. err error,
  24. ) {
  25. limit, err = GetLimit(opts.Repo, opts.Project)
  26. if err != nil {
  27. return nil, nil, nil, err
  28. }
  29. usageCache, err := opts.Repo.ProjectUsage().ReadProjectUsageCache(opts.Project.ID)
  30. isCacheFound := true
  31. if isCacheFound = !errors.Is(err, gorm.ErrRecordNotFound); err != nil && isCacheFound {
  32. return nil, nil, nil, err
  33. }
  34. // query for the linked cluster counts
  35. clusters, err := opts.Repo.Cluster().ListClustersByProjectID(opts.Project.ID)
  36. if err != nil {
  37. return nil, nil, nil, err
  38. }
  39. // query for the linked user counts
  40. roles, err := opts.Repo.ProjectRole().ListProjectRoles(opts.Project.ID)
  41. if err != nil {
  42. return nil, nil, nil, err
  43. }
  44. countedRoles := make(map[uint]bool)
  45. for _, role := range roles {
  46. for _, u := range role.Users {
  47. if _, exists := opts.WhitelistedUsers[u.ID]; !exists {
  48. countedRoles[u.ID] = true
  49. }
  50. }
  51. }
  52. if !isCacheFound {
  53. usageCache = &models.ProjectUsageCache{
  54. ProjectID: opts.Project.ID,
  55. }
  56. }
  57. oldUsageCache := *usageCache
  58. usageCache.Clusters = uint(len(clusters))
  59. usageCache.Users = uint(len(countedRoles))
  60. // if the usage cache is 1 hour old, was not found, usage is currently over limit, or the clusters/users
  61. // counts have changed, re-query for the usage
  62. if !isCacheFound || usageCache.Is1HrOld() || isUsageExceeded(usageCache, limit) || isUsageChanged(&oldUsageCache, usageCache) {
  63. cpu, memory, err := getResourceUsage(opts, clusters)
  64. if err != nil {
  65. return nil, nil, nil, err
  66. }
  67. usageCache.ResourceCPU = cpu
  68. usageCache.ResourceMemory = memory
  69. }
  70. isExceeded := isUsageExceeded(usageCache, limit)
  71. if !usageCache.Exceeded && isExceeded {
  72. // update the usage cache with a time exceeded
  73. currTime := time.Now()
  74. usageCache.ExceededSince = &currTime
  75. }
  76. usageCache.Exceeded = isExceeded
  77. if !isCacheFound {
  78. usageCache, err = opts.Repo.ProjectUsage().CreateProjectUsageCache(usageCache)
  79. } else if isUsageChanged(&oldUsageCache, usageCache) {
  80. usageCache, err = opts.Repo.ProjectUsage().UpdateProjectUsageCache(usageCache)
  81. }
  82. if err != nil {
  83. return nil, nil, nil, err
  84. }
  85. return &types.ProjectUsage{
  86. ResourceCPU: usageCache.ResourceCPU,
  87. ResourceMemory: usageCache.ResourceMemory,
  88. Clusters: usageCache.Clusters,
  89. Users: usageCache.Users,
  90. }, limit, usageCache, nil
  91. }
  92. func isUsageExceeded(usageCache *models.ProjectUsageCache, limit *types.ProjectUsage) bool {
  93. isCPUExceeded := limit.ResourceCPU != 0 && usageCache.ResourceCPU > limit.ResourceCPU
  94. isMemExceeded := limit.ResourceMemory != 0 && usageCache.ResourceMemory > limit.ResourceMemory
  95. isUsersExceeded := limit.Users != 0 && usageCache.Users > limit.Users
  96. isClustersExceeded := limit.Clusters != 0 && usageCache.Clusters > limit.Clusters
  97. return isCPUExceeded || isMemExceeded || isUsersExceeded || isClustersExceeded
  98. }
  99. func isUsageChanged(oldUsageCache, currUsageCache *models.ProjectUsageCache) bool {
  100. return oldUsageCache.Exceeded != currUsageCache.Exceeded ||
  101. oldUsageCache.Clusters != currUsageCache.Clusters ||
  102. oldUsageCache.Users != currUsageCache.Users ||
  103. oldUsageCache.ResourceCPU != currUsageCache.ResourceCPU ||
  104. oldUsageCache.ResourceMemory != currUsageCache.ResourceMemory
  105. }
  106. // gets the total resource usage across all nodes in all clusters
  107. func getResourceUsage(opts *GetUsageOpts, clusters []*models.Cluster) (uint, uint, error) {
  108. var totCPU, totMem uint = 0, 0
  109. for _, cluster := range clusters {
  110. ooc := &kubernetes.OutOfClusterConfig{
  111. Cluster: cluster,
  112. Repo: opts.Repo,
  113. DigitalOceanOAuth: opts.DOConf,
  114. AllowInClusterConnections: false,
  115. }
  116. agent, err := kubernetes.GetAgentOutOfClusterConfig(ooc)
  117. if err != nil {
  118. continue
  119. }
  120. totAlloc, err := nodes.GetAllocatableResources(agent.Clientset)
  121. if err != nil {
  122. continue
  123. }
  124. totCPU += totAlloc.CPU
  125. totMem += totAlloc.Memory
  126. }
  127. return totCPU / 1000, totMem / (1000 * 1000), nil
  128. }