mirror of https://github.com/coder/coder.git
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),
|
|
})
|
|
}
|