Skip to content

Commit

Permalink
Auto-Generate Quotas (#170)
Browse files Browse the repository at this point in the history
Elegant upgrade path alert... if an organization quota does not exist,
then create a virtual one.  If an individual quota doesn't exist, then
create a virtual one from the metadata.  If some old quotas exist, then
delete them!
  • Loading branch information
spjmurray authored Feb 3, 2025
1 parent 0b5f8c5 commit 8e0c255
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 18 deletions.
4 changes: 2 additions & 2 deletions charts/identity/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ description: A Helm chart for deploying Unikorn's IdP

type: application

version: v0.2.53-rc4
appVersion: v0.2.53-rc4
version: v0.2.53-rc5
appVersion: v0.2.53-rc5

icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png

Expand Down
65 changes: 52 additions & 13 deletions pkg/handler/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ func (c *Client) ProjectNamespace(ctx context.Context, organizationID, projectID
return &resources.Items[0], nil
}

func (c *Client) GetQuota(ctx context.Context, organizationID string) (*unikornv1.Quota, error) {
func (c *Client) GetQuota(ctx context.Context, organizationID string) (*unikornv1.Quota, bool, error) {
selector, err := organizationSelector(organizationID)
if err != nil {
return nil, err
return nil, false, err
}

options := &client.ListOptions{
Expand All @@ -108,14 +108,58 @@ func (c *Client) GetQuota(ctx context.Context, organizationID string) (*unikornv
var resources unikornv1.QuotaList

if err := c.client.List(ctx, &resources, options); err != nil {
return nil, err
return nil, false, err
}

if len(resources.Items) != 1 {
return nil, fmt.Errorf("%w: expected to find 1 project namespace", ErrConsistency)
if len(resources.Items) > 1 {
return nil, false, fmt.Errorf("%w: expected to find 1 project namespace", ErrConsistency)
}

return &resources.Items[0], nil
// We are going to lazily create the quota and any new quota items that come
// into existence.
var quota *unikornv1.Quota

var virtual bool

if len(resources.Items) == 0 {
quota = &unikornv1.Quota{}

virtual = true
} else {
quota = &resources.Items[0]
}

metadata := &unikornv1.QuotaMetadataList{}

if err := c.client.List(ctx, metadata, &client.ListOptions{}); err != nil {
return nil, false, err
}

names := make([]string, len(metadata.Items))

for i, meta := range metadata.Items {
names[i] = meta.Name

findQuota := func(q unikornv1.ResourceQuota) bool {
return q.Kind == meta.Name
}

if index := slices.IndexFunc(quota.Spec.Quotas, findQuota); index >= 0 {
continue
}

quota.Spec.Quotas = append(quota.Spec.Quotas, unikornv1.ResourceQuota{
Kind: meta.Name,
Quantity: meta.Spec.Default,
})
}

// And remove anything that's been retired.
quota.Spec.Quotas = slices.DeleteFunc(quota.Spec.Quotas, func(q unikornv1.ResourceQuota) bool {
return !slices.Contains(names, q.Kind)
})

return quota, virtual, nil
}

func (c *Client) GetAllocations(ctx context.Context, organizationID string) (*unikornv1.AllocationList, error) {
Expand Down Expand Up @@ -145,7 +189,7 @@ func (c *Client) GetAllocations(ctx context.Context, organizationID string) (*un
func (c *Client) CheckQuotaConsistency(ctx context.Context, organizationID string, quota *unikornv1.Quota, allocation *unikornv1.Allocation) error {
// Handle the default quota.
if quota == nil {
temp, err := c.GetQuota(ctx, organizationID)
temp, _, err := c.GetQuota(ctx, organizationID)
if err != nil {
return err
}
Expand Down Expand Up @@ -197,12 +241,7 @@ func checkQuotaConsistency(quota *unikornv1.Quota, allocations *unikornv1.Alloca
}

for k, v := range totals {
capacity, ok := capacities[k]
if !ok {
return fmt.Errorf("%w: allocation of %s not defined in quota", ErrConsistency, k)
}

if v > capacity {
if capacity, ok := capacities[k]; ok && v > capacity {
return fmt.Errorf("%w: total allocation of %d would exceed quota limit of %d", ErrConsistency, v, capacity)
}
}
Expand Down
1 change: 0 additions & 1 deletion pkg/handler/organizations/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ func (c *Client) List(ctx context.Context, rbacClient *rbac.RBAC) (openapi.Organ
return nil, errors.OAuth2ServerError("failed to list organizations").WithError(err)
}

// TODO: service accounts???
users, err := rbacClient.GetActiveUsers(ctx, info.Userinfo.Sub)
if err != nil {
return nil, errors.OAuth2ServerError("failed to list active subjects").WithError(err)
Expand Down
12 changes: 10 additions & 2 deletions pkg/handler/quotas/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func (c *Client) GetMetadata(ctx context.Context) (openapi.QuotaMetadataRead, er
}

func (c *Client) Get(ctx context.Context, organizationID string) (*openapi.QuotasRead, error) {
result, err := common.New(c.client).GetQuota(ctx, organizationID)
result, _, err := common.New(c.client).GetQuota(ctx, organizationID)
if err != nil {
return nil, err
}
Expand All @@ -188,7 +188,7 @@ func (c *Client) Update(ctx context.Context, organizationID string, request *ope
return nil, err
}

current, err := common.GetQuota(ctx, organizationID)
current, virtual, err := common.GetQuota(ctx, organizationID)
if err != nil {
return nil, errors.OAuth2InvalidRequest("unnable to read quota").WithError(err)
}
Expand All @@ -198,6 +198,14 @@ func (c *Client) Update(ctx context.Context, organizationID string, request *ope
return nil, err
}

if virtual {
if err := c.client.Create(ctx, required); err != nil {
return nil, errors.OAuth2InvalidRequest("unnable to create quota").WithError(err)
}

return c.convert(ctx, required, organizationID)
}

updated := current.DeepCopy()
updated.Labels = required.Labels
updated.Annotations = required.Annotations
Expand Down

0 comments on commit 8e0c255

Please sign in to comment.