mirror of
https://github.com/coder/coder.git
synced 2026-06-04 05:28:20 +00:00
bddb808b25
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example: ``` import ( "context" "time" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" "cdr.dev/slog/v3" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/serpent" ) ``` 3 groups: standard library, 3rd partly libs, Coder libs. This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"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/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, database.GetQuotaConsumedForUserParams{
|
|
OwnerID: workspace.OwnerID,
|
|
OrganizationID: workspace.OrganizationID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
budget, err = s.GetQuotaAllowanceForUser(ctx, database.GetQuotaAllowanceForUserParams{
|
|
UserID: workspace.OwnerID,
|
|
OrganizationID: workspace.OrganizationID,
|
|
})
|
|
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
|
|
}, &database.TxOptions{
|
|
Isolation: sql.LevelSerializable,
|
|
TxIdentifier: "commit_quota",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &proto.CommitQuotaResponse{
|
|
Ok: permit,
|
|
// #nosec G115 - Safe conversion as quota credits consumed value is expected to be within int32 range
|
|
CreditsConsumed: int32(consumed),
|
|
// #nosec G115 - Safe conversion as quota budget value is expected to be within int32 range
|
|
Budget: int32(budget),
|
|
}, nil
|
|
}
|
|
|
|
// @Summary Get workspace quota by user deprecated
|
|
// @ID get-workspace-quota-by-user-deprecated
|
|
// @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]
|
|
// @Deprecated this endpoint will be removed, use /organizations/{organization}/members/{user}/workspace-quota instead
|
|
func (api *API) workspaceQuotaByUser(rw http.ResponseWriter, r *http.Request) {
|
|
defaultOrg, err := api.Database.GetDefaultOrganization(r.Context())
|
|
if err != nil {
|
|
httpapi.InternalServerError(rw, err)
|
|
return
|
|
}
|
|
|
|
// defer to the new endpoint using default org as the organization
|
|
chi.RouteContext(r.Context()).URLParams.Add("organization", defaultOrg.ID.String())
|
|
mw := httpmw.ExtractOrganizationParam(api.Database)
|
|
mw(http.HandlerFunc(api.workspaceQuota)).ServeHTTP(rw, r)
|
|
}
|
|
|
|
// @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"
|
|
// @Param organization path string true "Organization ID" format(uuid)
|
|
// @Success 200 {object} codersdk.WorkspaceQuota
|
|
// @Router /organizations/{organization}/members/{user}/workspace-quota [get]
|
|
func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
organization = httpmw.OrganizationParam(r)
|
|
user = httpmw.UserParam(r)
|
|
)
|
|
|
|
licensed := api.Entitlements.Enabled(codersdk.FeatureTemplateRBAC)
|
|
|
|
// 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(), database.GetQuotaAllowanceForUserParams{
|
|
UserID: user.ID,
|
|
OrganizationID: organization.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(), database.GetQuotaConsumedForUserParams{
|
|
OwnerID: user.ID,
|
|
OrganizationID: organization.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),
|
|
})
|
|
}
|