mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
Just moved `rbac.Action` -> `policy.Action`. This is for the stacked PR to not have circular dependencies when doing autogen. Without this, the autogen can produce broken golang code, which prevents the autogen from compiling. So just avoiding circular dependencies. Doing this in it's own PR to reduce LoC diffs in the primary PR, since this has 0 functional changes.
163 lines
4.0 KiB
Go
163 lines
4.0 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"cdr.dev/slog"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/coderd/httpmw"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/provisionerd/proto"
|
|
)
|
|
|
|
type committer struct {
|
|
Log slog.Logger
|
|
Database database.Store
|
|
}
|
|
|
|
func (c *committer) CommitQuota(
|
|
ctx context.Context, request *proto.CommitQuotaRequest,
|
|
) (*proto.CommitQuotaResponse, error) {
|
|
jobID, err := uuid.Parse(request.JobId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nextBuild, err := c.Database.GetWorkspaceBuildByJobID(ctx, jobID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
workspace, err := c.Database.GetWorkspaceByID(ctx, nextBuild.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
consumed int64
|
|
budget int64
|
|
permit bool
|
|
)
|
|
err = c.Database.InTx(func(s database.Store) error {
|
|
var err error
|
|
consumed, err = s.GetQuotaConsumedForUser(ctx, workspace.OwnerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
budget, err = s.GetQuotaAllowanceForUser(ctx, workspace.OwnerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the new build will reduce overall quota consumption, then we
|
|
// allow it even if the user is over quota.
|
|
netIncrease := true
|
|
prevBuild, err := s.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
|
|
WorkspaceID: workspace.ID,
|
|
BuildNumber: nextBuild.BuildNumber - 1,
|
|
})
|
|
if err == nil {
|
|
netIncrease = request.DailyCost >= prevBuild.DailyCost
|
|
c.Log.Debug(
|
|
ctx, "previous build cost",
|
|
slog.F("prev_cost", prevBuild.DailyCost),
|
|
slog.F("next_cost", request.DailyCost),
|
|
slog.F("net_increase", netIncrease),
|
|
)
|
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
|
|
newConsumed := int64(request.DailyCost) + consumed
|
|
if newConsumed > budget && netIncrease {
|
|
c.Log.Debug(
|
|
ctx, "over quota, rejecting",
|
|
slog.F("prev_consumed", consumed),
|
|
slog.F("next_consumed", newConsumed),
|
|
slog.F("budget", budget),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
err = s.UpdateWorkspaceBuildCostByID(ctx, database.UpdateWorkspaceBuildCostByIDParams{
|
|
ID: nextBuild.ID,
|
|
DailyCost: request.DailyCost,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
permit = true
|
|
consumed = newConsumed
|
|
return nil
|
|
}, &sql.TxOptions{
|
|
Isolation: sql.LevelSerializable,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &proto.CommitQuotaResponse{
|
|
Ok: permit,
|
|
CreditsConsumed: int32(consumed),
|
|
Budget: int32(budget),
|
|
}, nil
|
|
}
|
|
|
|
// @Summary Get workspace quota by user
|
|
// @ID get-workspace-quota-by-user
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param user path string true "User ID, name, or me"
|
|
// @Success 200 {object} codersdk.WorkspaceQuota
|
|
// @Router /workspace-quota/{user} [get]
|
|
func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) {
|
|
user := httpmw.UserParam(r)
|
|
|
|
if !api.AGPL.Authorize(r, policy.ActionRead, user) {
|
|
httpapi.ResourceNotFound(rw)
|
|
return
|
|
}
|
|
|
|
api.entitlementsMu.RLock()
|
|
licensed := api.entitlements.Features[codersdk.FeatureTemplateRBAC].Enabled
|
|
api.entitlementsMu.RUnlock()
|
|
|
|
// There are no groups and thus no allowance if RBAC isn't licensed.
|
|
var quotaAllowance int64 = -1
|
|
if licensed {
|
|
var err error
|
|
quotaAllowance, err = api.Database.GetQuotaAllowanceForUser(r.Context(), user.ID)
|
|
if err != nil {
|
|
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to get allowance",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
quotaConsumed, err := api.Database.GetQuotaConsumedForUser(r.Context(), user.ID)
|
|
if err != nil {
|
|
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to get consumed",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.WorkspaceQuota{
|
|
CreditsConsumed: int(quotaConsumed),
|
|
Budget: int(quotaAllowance),
|
|
})
|
|
}
|