diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 43ed251b74140..1f309c7314f06 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -362,12 +362,6 @@ func newConfig() *codersdk.DeploymentConfig { Enterprise: true, Secret: true, }, - UserWorkspaceQuota: &codersdk.DeploymentConfigField[int]{ - Name: "User Workspace Quota", - Usage: "Enables and sets a limit on how many workspaces each user can create.", - Flag: "user-workspace-quota", - Enterprise: true, - }, Provisioner: &codersdk.ProvisionerConfig{ Daemons: &codersdk.DeploymentConfigField[int]{ Name: "Provisioner Daemons", diff --git a/cli/deployment/config_test.go b/cli/deployment/config_test.go index b14f890e9d561..84be72bcd4754 100644 --- a/cli/deployment/config_test.go +++ b/cli/deployment/config_test.go @@ -79,16 +79,14 @@ func TestConfig(t *testing.T) { }, { Name: "Enterprise", Env: map[string]string{ - "CODER_AUDIT_LOGGING": "false", - "CODER_BROWSER_ONLY": "true", - "CODER_SCIM_API_KEY": "some-key", - "CODER_USER_WORKSPACE_QUOTA": "10", + "CODER_AUDIT_LOGGING": "false", + "CODER_BROWSER_ONLY": "true", + "CODER_SCIM_API_KEY": "some-key", }, Valid: func(config *codersdk.DeploymentConfig) { require.Equal(t, config.AuditLogging.Value, false) require.Equal(t, config.BrowserOnly.Value, true) require.Equal(t, config.SCIMAPIKey.Value, "some-key") - require.Equal(t, config.UserWorkspaceQuota.Value, 10) }, }, { Name: "TLS", diff --git a/coderd/activitybump.go b/coderd/activitybump.go index 0dc9db8adb0ad..5b4bd16f22817 100644 --- a/coderd/activitybump.go +++ b/coderd/activitybump.go @@ -63,7 +63,7 @@ func activityBumpWorkspace(log slog.Logger, db database.Store, workspace databas return xerrors.Errorf("update workspace build: %w", err) } return nil - }) + }, nil) if err != nil { log.Error( ctx, "bump failed", diff --git a/coderd/autobuild/executor/lifecycle_executor.go b/coderd/autobuild/executor/lifecycle_executor.go index f21fde9a8af7b..90126d074b190 100644 --- a/coderd/autobuild/executor/lifecycle_executor.go +++ b/coderd/autobuild/executor/lifecycle_executor.go @@ -177,7 +177,7 @@ func (e *Executor) runOnce(t time.Time) Stats { } return nil - }) + }, nil) if err != nil { log.Error(e.ctx, "workspace scheduling failed", slog.Error(err)) } diff --git a/coderd/coderd.go b/coderd/coderd.go index 5d9497457783f..a6e7690b8441f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -40,9 +40,9 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/telemetry" "github.com/coder/coder/coderd/tracing" - "github.com/coder/coder/coderd/workspacequota" "github.com/coder/coder/coderd/wsconncache" "github.com/coder/coder/codersdk" + "github.com/coder/coder/provisionerd/proto" "github.com/coder/coder/site" "github.com/coder/coder/tailnet" ) @@ -66,7 +66,6 @@ type Options struct { CacheDir string Auditor audit.Auditor - WorkspaceQuotaEnforcer workspacequota.Enforcer AgentConnectionUpdateFrequency time.Duration AgentInactiveDisconnectTimeout time.Duration // APIRateLimit is the minutely throughput rate limit per user or ip. @@ -145,9 +144,6 @@ func New(options *Options) *API { if options.Auditor == nil { options.Auditor = audit.NewNop() } - if options.WorkspaceQuotaEnforcer == nil { - options.WorkspaceQuotaEnforcer = workspacequota.NewNop() - } siteCacheDir := options.CacheDir if siteCacheDir != "" { @@ -174,12 +170,10 @@ func New(options *Options) *API { Authorizer: options.Authorizer, Logger: options.Logger, }, - metricsCache: metricsCache, - Auditor: atomic.Pointer[audit.Auditor]{}, - WorkspaceQuotaEnforcer: atomic.Pointer[workspacequota.Enforcer]{}, + metricsCache: metricsCache, + Auditor: atomic.Pointer[audit.Auditor]{}, } api.Auditor.Store(&options.Auditor) - api.WorkspaceQuotaEnforcer.Store(&options.WorkspaceQuotaEnforcer) api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0) api.TailnetCoordinator.Store(&options.TailnetCoordinator) oauthConfigs := &httpmw.OAuth2Configs{ @@ -590,8 +584,8 @@ type API struct { ID uuid.UUID Auditor atomic.Pointer[audit.Auditor] WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool] - WorkspaceQuotaEnforcer atomic.Pointer[workspacequota.Enforcer] TailnetCoordinator atomic.Pointer[tailnet.Coordinator] + QuotaCommitter atomic.Pointer[proto.QuotaCommitter] HTTPAuth *HTTPAuthorizer // APIHandler serves "/api/v2" diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 6cb3870c64127..0f253a0ceeffe 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -528,7 +528,8 @@ func AwaitWorkspaceBuildJob(t *testing.T, client *codersdk.Client, build uuid.UU t.Logf("waiting for workspace build job %s", build) var workspaceBuild codersdk.WorkspaceBuild require.Eventually(t, func() bool { - workspaceBuild, err := client.WorkspaceBuild(context.Background(), build) + var err error + workspaceBuild, err = client.WorkspaceBuild(context.Background(), build) return assert.NoError(t, err) && workspaceBuild.Job.CompletedAt != nil }, testutil.WaitShort, testutil.IntervalFast) return workspaceBuild diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index c28d0b54dce87..f033974f3d896 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -121,7 +121,7 @@ func (*fakeQuerier) Ping(_ context.Context) (time.Duration, error) { } // InTx doesn't rollback data properly for in-memory yet. -func (q *fakeQuerier) InTx(fn func(database.Store) error) error { +func (q *fakeQuerier) InTx(fn func(database.Store) error, _ *sql.TxOptions) error { q.mutex.Lock() defer q.mutex.Unlock() return fn(&fakeQuerier{mutex: inTxMutex{}, data: q.data}) @@ -2246,6 +2246,7 @@ func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.In Name: arg.Name, Hide: arg.Hide, Icon: arg.Icon, + DailyCost: arg.DailyCost, } q.provisionerJobResources = append(q.provisionerJobResources, resource) return resource, nil @@ -2757,6 +2758,20 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U } return database.WorkspaceBuild{}, sql.ErrNoRows } +func (q *fakeQuerier) UpdateWorkspaceBuildCostByID(_ context.Context, arg database.UpdateWorkspaceBuildCostByIDParams) (database.WorkspaceBuild, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.ID != arg.ID { + continue + } + workspaceBuild.DailyCost = arg.DailyCost + q.workspaceBuilds[index] = workspaceBuild + return workspaceBuild, nil + } + return database.WorkspaceBuild{}, sql.ErrNoRows +} func (q *fakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database.UpdateWorkspaceDeletedByIDParams) error { q.mutex.Lock() @@ -2858,6 +2873,7 @@ func (q *fakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGrou if group.ID == arg.ID { group.Name = arg.Name group.AvatarURL = arg.AvatarURL + group.QuotaAllowance = arg.QuotaAllowance q.groups[i] = group return group, nil } @@ -3230,6 +3246,7 @@ func (q *fakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar Name: arg.Name, OrganizationID: arg.OrganizationID, AvatarURL: arg.AvatarURL, + QuotaAllowance: arg.QuotaAllowance, } q.groups = append(q.groups, group) @@ -3430,3 +3447,46 @@ func (q *fakeQuerier) UpdateGitAuthLink(_ context.Context, arg database.UpdateGi } return nil } + +func (q *fakeQuerier) GetQuotaAllowanceForUser(_ context.Context, userID uuid.UUID) (int64, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + var sum int64 + for _, member := range q.groupMembers { + if member.UserID != userID { + continue + } + for _, group := range q.groups { + if group.ID == member.GroupID { + sum += int64(group.QuotaAllowance) + } + } + } + return sum, nil +} + +func (q *fakeQuerier) GetQuotaConsumedForUser(_ context.Context, userID uuid.UUID) (int64, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + var sum int64 + for _, workspace := range q.workspaces { + if workspace.OwnerID != userID { + continue + } + if workspace.Deleted { + continue + } + + var lastBuild database.WorkspaceBuild + for _, build := range q.workspaceBuilds { + if build.WorkspaceID != workspace.ID { + continue + } + if build.CreatedAt.After(lastBuild.CreatedAt) { + lastBuild = build + } + } + sum += int64(lastBuild.DailyCost) + } + return sum, nil +} diff --git a/coderd/database/databasefake/databasefake_test.go b/coderd/database/databasefake/databasefake_test.go index 21c88c1f0261d..f3f53de7d55d1 100644 --- a/coderd/database/databasefake/databasefake_test.go +++ b/coderd/database/databasefake/databasefake_test.go @@ -38,7 +38,7 @@ func TestInTx(t *testing.T) { }) assert.NoError(t, err) return nil - }) + }, nil) assert.NoError(t, err) }() var nums []int diff --git a/coderd/database/db.go b/coderd/database/db.go index 020000888f8eb..f8de976e92f72 100644 --- a/coderd/database/db.go +++ b/coderd/database/db.go @@ -26,7 +26,7 @@ type Store interface { customQuerier Ping(ctx context.Context) (time.Duration, error) - InTx(func(Store) error) error + InTx(func(Store) error, *sql.TxOptions) error } // DBTX represents a database connection or transaction. @@ -68,7 +68,7 @@ func (q *sqlQuerier) Ping(ctx context.Context) (time.Duration, error) { } // InTx performs database operations inside a transaction. -func (q *sqlQuerier) InTx(function func(Store) error) error { +func (q *sqlQuerier) InTx(function func(Store) error, txOpts *sql.TxOptions) error { if _, ok := q.db.(*sqlx.Tx); ok { // If the current inner "db" is already a transaction, we just reuse it. // We do not need to handle commit/rollback as the outer tx will handle @@ -80,7 +80,7 @@ func (q *sqlQuerier) InTx(function func(Store) error) error { return nil } - transaction, err := q.sdb.BeginTxx(context.Background(), nil) + transaction, err := q.sdb.BeginTxx(context.Background(), txOpts) if err != nil { return xerrors.Errorf("begin transaction: %w", err) } diff --git a/coderd/database/db_test.go b/coderd/database/db_test.go index bf0afc31f3119..b92138994d99c 100644 --- a/coderd/database/db_test.go +++ b/coderd/database/db_test.go @@ -43,8 +43,8 @@ func TestNestedInTx(t *testing.T) { LoginType: database.LoginTypeGithub, }) return err - }) - }) + }, nil) + }, nil) require.NoError(t, err, "outer tx: %w", err) user, err := db.GetUserByID(context.Background(), uid) diff --git a/coderd/database/dbtestutil/db.go b/coderd/database/dbtestutil/db.go index 2ca9e95a8af25..bf44bca32fd44 100644 --- a/coderd/database/dbtestutil/db.go +++ b/coderd/database/dbtestutil/db.go @@ -19,9 +19,16 @@ func NewDB(t *testing.T) (database.Store, database.Pubsub) { db := databasefake.New() pubsub := database.NewPubsubInMemory() if os.Getenv("DB") != "" { - connectionURL, closePg, err := postgres.Open() - require.NoError(t, err) - t.Cleanup(closePg) + connectionURL := os.Getenv("CODER_PG_CONNECTION_URL") + if connectionURL == "" { + var ( + err error + closePg func() + ) + connectionURL, closePg, err = postgres.Open() + require.NoError(t, err) + t.Cleanup(closePg) + } sqlDB, err := sql.Open("postgres", connectionURL) require.NoError(t, err) t.Cleanup(func() { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index ed8ebcee65b91..0eca61e961c8a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -192,7 +192,8 @@ CREATE TABLE groups ( id uuid NOT NULL, name text NOT NULL, organization_id uuid NOT NULL, - avatar_url text DEFAULT ''::text NOT NULL + avatar_url text DEFAULT ''::text NOT NULL, + quota_allowance integer DEFAULT 0 NOT NULL ); CREATE TABLE licenses ( @@ -444,7 +445,8 @@ CREATE TABLE workspace_builds ( provisioner_state bytea, job_id uuid NOT NULL, deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, - reason build_reason DEFAULT 'initiator'::build_reason NOT NULL + reason build_reason DEFAULT 'initiator'::build_reason NOT NULL, + daily_cost integer DEFAULT 0 NOT NULL ); CREATE TABLE workspace_resource_metadata ( @@ -463,7 +465,8 @@ CREATE TABLE workspace_resources ( name character varying(64) NOT NULL, hide boolean DEFAULT false NOT NULL, icon character varying(256) DEFAULT ''::character varying NOT NULL, - instance_type character varying(256) + instance_type character varying(256), + daily_cost integer DEFAULT 0 NOT NULL ); CREATE TABLE workspaces ( diff --git a/coderd/database/migrations/000076_cost.down.sql b/coderd/database/migrations/000076_cost.down.sql new file mode 100644 index 0000000000000..6c89c48fa70bc --- /dev/null +++ b/coderd/database/migrations/000076_cost.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE workspace_builds DROP COLUMN daily_cost; +ALTER TABLE workspace_resources DROP COLUMN daily_cost; +ALTER TABLE groups DROP COLUMN quota_allowance; diff --git a/coderd/database/migrations/000076_cost.up.sql b/coderd/database/migrations/000076_cost.up.sql new file mode 100644 index 0000000000000..fb94cc318cb8b --- /dev/null +++ b/coderd/database/migrations/000076_cost.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE workspace_builds ADD COLUMN daily_cost int NOT NULL DEFAULT 0; +ALTER TABLE workspace_resources ADD COLUMN daily_cost int NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN quota_allowance int NOT NULL DEFAULT 0; diff --git a/coderd/database/models.go b/coderd/database/models.go index 58a849fd25b11..9cc8b495b25e6 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -454,6 +454,7 @@ type Group struct { Name string `db:"name" json:"name"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` AvatarURL string `db:"avatar_url" json:"avatar_url"` + QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"` } type GroupMember struct { @@ -700,6 +701,7 @@ type WorkspaceBuild struct { JobID uuid.UUID `db:"job_id" json:"job_id"` Deadline time.Time `db:"deadline" json:"deadline"` Reason BuildReason `db:"reason" json:"reason"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` } type WorkspaceResource struct { @@ -712,6 +714,7 @@ type WorkspaceResource struct { Hide bool `db:"hide" json:"hide"` Icon string `db:"icon" json:"icon"` InstanceType sql.NullString `db:"instance_type" json:"instance_type"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` } type WorkspaceResourceMetadatum struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7ace117d404d0..603ceac7da8b1 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -72,6 +72,8 @@ type sqlcQuerier interface { GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) GetProvisionerLogsByIDBetween(ctx context.Context, arg GetProvisionerLogsByIDBetweenParams) ([]ProvisionerJobLog, error) + GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) + GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error) GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) @@ -187,6 +189,7 @@ type sqlcQuerier interface { UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuild, error) + UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5fef1c7149cb8..96e33ae7e0adb 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1079,7 +1079,7 @@ func (q *sqlQuerier) GetAllOrganizationMembers(ctx context.Context, organization const getGroupByID = `-- name: GetGroupByID :one SELECT - id, name, organization_id, avatar_url + id, name, organization_id, avatar_url, quota_allowance FROM groups WHERE @@ -1096,13 +1096,14 @@ func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, err &i.Name, &i.OrganizationID, &i.AvatarURL, + &i.QuotaAllowance, ) return i, err } const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one SELECT - id, name, organization_id, avatar_url + id, name, organization_id, avatar_url, quota_allowance FROM groups WHERE @@ -1126,6 +1127,7 @@ func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrg &i.Name, &i.OrganizationID, &i.AvatarURL, + &i.QuotaAllowance, ) return i, err } @@ -1185,7 +1187,7 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([] const getGroupsByOrganizationID = `-- name: GetGroupsByOrganizationID :many SELECT - id, name, organization_id, avatar_url + id, name, organization_id, avatar_url, quota_allowance FROM groups WHERE @@ -1208,6 +1210,7 @@ func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organization &i.Name, &i.OrganizationID, &i.AvatarURL, + &i.QuotaAllowance, ); err != nil { return nil, err } @@ -1224,7 +1227,7 @@ func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organization const getUserGroups = `-- name: GetUserGroups :many SELECT - groups.id, groups.name, groups.organization_id, groups.avatar_url + groups.id, groups.name, groups.organization_id, groups.avatar_url, groups.quota_allowance FROM groups JOIN @@ -1249,6 +1252,7 @@ func (q *sqlQuerier) GetUserGroups(ctx context.Context, userID uuid.UUID) ([]Gro &i.Name, &i.OrganizationID, &i.AvatarURL, + &i.QuotaAllowance, ); err != nil { return nil, err } @@ -1270,7 +1274,7 @@ INSERT INTO groups ( organization_id ) VALUES - ( $1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url + ( $1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance ` // We use the organization_id as the id @@ -1284,6 +1288,7 @@ func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uui &i.Name, &i.OrganizationID, &i.AvatarURL, + &i.QuotaAllowance, ) return i, err } @@ -1293,10 +1298,11 @@ INSERT INTO groups ( id, name, organization_id, - avatar_url + avatar_url, + quota_allowance ) VALUES - ( $1, $2, $3, $4) RETURNING id, name, organization_id, avatar_url + ( $1, $2, $3, $4, $5) RETURNING id, name, organization_id, avatar_url, quota_allowance ` type InsertGroupParams struct { @@ -1304,6 +1310,7 @@ type InsertGroupParams struct { Name string `db:"name" json:"name"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` AvatarURL string `db:"avatar_url" json:"avatar_url"` + QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"` } func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) { @@ -1312,6 +1319,7 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr arg.Name, arg.OrganizationID, arg.AvatarURL, + arg.QuotaAllowance, ) var i Group err := row.Scan( @@ -1319,6 +1327,7 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr &i.Name, &i.OrganizationID, &i.AvatarURL, + &i.QuotaAllowance, ) return i, err } @@ -1346,26 +1355,34 @@ UPDATE groups SET name = $1, - avatar_url = $2 + avatar_url = $2, + quota_allowance = $3 WHERE - id = $3 -RETURNING id, name, organization_id, avatar_url + id = $4 +RETURNING id, name, organization_id, avatar_url, quota_allowance ` type UpdateGroupByIDParams struct { - Name string `db:"name" json:"name"` - AvatarURL string `db:"avatar_url" json:"avatar_url"` - ID uuid.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + AvatarURL string `db:"avatar_url" json:"avatar_url"` + QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"` + ID uuid.UUID `db:"id" json:"id"` } func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) { - row := q.db.QueryRowContext(ctx, updateGroupByID, arg.Name, arg.AvatarURL, arg.ID) + row := q.db.QueryRowContext(ctx, updateGroupByID, + arg.Name, + arg.AvatarURL, + arg.QuotaAllowance, + arg.ID, + ) var i Group err := row.Scan( &i.ID, &i.Name, &i.OrganizationID, &i.AvatarURL, + &i.QuotaAllowance, ) return i, err } @@ -2770,6 +2787,53 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a return err } +const getQuotaAllowanceForUser = `-- name: GetQuotaAllowanceForUser :one +SELECT + coalesce(SUM(quota_allowance), 0)::BIGINT +FROM + group_members gm +JOIN groups g ON + g.id = gm.group_id +WHERE + user_id = $1 +` + +func (q *sqlQuerier) GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) { + row := q.db.QueryRowContext(ctx, getQuotaAllowanceForUser, userID) + var column_1 int64 + err := row.Scan(&column_1) + return column_1, err +} + +const getQuotaConsumedForUser = `-- name: GetQuotaConsumedForUser :one +WITH latest_builds AS ( +SELECT + DISTINCT ON + (workspace_id) id, + workspace_id, + daily_cost +FROM + workspace_builds wb +ORDER BY + workspace_id, + created_at DESC +) +SELECT + coalesce(SUM(daily_cost), 0)::BIGINT +FROM + workspaces +JOIN latest_builds ON + latest_builds.workspace_id = workspaces.id +WHERE NOT deleted AND workspaces.owner_id = $1 +` + +func (q *sqlQuerier) GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error) { + row := q.db.QueryRowContext(ctx, getQuotaConsumedForUser, ownerID) + var column_1 int64 + err := row.Scan(&column_1) + return column_1, err +} + const deleteReplicasUpdatedBefore = `-- name: DeleteReplicasUpdatedBefore :exec DELETE FROM replicas WHERE updated_at < $1 ` @@ -5135,7 +5199,7 @@ func (q *sqlQuerier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg Updat const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE @@ -5162,12 +5226,13 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ) return i, err } const getLatestWorkspaceBuilds = `-- name: GetLatestWorkspaceBuilds :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -5203,6 +5268,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ); err != nil { return nil, err } @@ -5218,7 +5284,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB } const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -5256,6 +5322,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ); err != nil { return nil, err } @@ -5272,7 +5339,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE @@ -5297,13 +5364,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ) return i, err } const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE @@ -5328,13 +5396,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ) return i, err } const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE @@ -5363,13 +5432,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ) return i, err } const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE @@ -5437,6 +5507,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ); err != nil { return nil, err } @@ -5452,7 +5523,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge } const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many -SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason FROM workspace_builds WHERE created_at > $1 +SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) { @@ -5477,6 +5548,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ); err != nil { return nil, err } @@ -5508,7 +5580,7 @@ INSERT INTO reason ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost ` type InsertWorkspaceBuildParams struct { @@ -5555,6 +5627,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, ) return i, err } @@ -5567,7 +5640,7 @@ SET provisioner_state = $3, deadline = $4 WHERE - id = $1 RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason + id = $1 RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost ` type UpdateWorkspaceBuildByIDParams struct { @@ -5598,13 +5671,49 @@ func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWor &i.JobID, &i.Deadline, &i.Reason, + &i.DailyCost, + ) + return i, err +} + +const updateWorkspaceBuildCostByID = `-- name: UpdateWorkspaceBuildCostByID :one +UPDATE + workspace_builds +SET + daily_cost = $2 +WHERE + id = $1 RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost +` + +type UpdateWorkspaceBuildCostByIDParams struct { + ID uuid.UUID `db:"id" json:"id"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` +} + +func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, updateWorkspaceBuildCostByID, arg.ID, arg.DailyCost) + var i WorkspaceBuild + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.WorkspaceID, + &i.TemplateVersionID, + &i.BuildNumber, + &i.Transition, + &i.InitiatorID, + &i.ProvisionerState, + &i.JobID, + &i.Deadline, + &i.Reason, + &i.DailyCost, ) return i, err } const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one SELECT - id, created_at, job_id, transition, type, name, hide, icon, instance_type + id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost FROM workspace_resources WHERE @@ -5624,6 +5733,7 @@ func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) &i.Hide, &i.Icon, &i.InstanceType, + &i.DailyCost, ) return i, err } @@ -5738,7 +5848,7 @@ func (q *sqlQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Contex const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many SELECT - id, created_at, job_id, transition, type, name, hide, icon, instance_type + id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost FROM workspace_resources WHERE @@ -5764,6 +5874,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui &i.Hide, &i.Icon, &i.InstanceType, + &i.DailyCost, ); err != nil { return nil, err } @@ -5780,7 +5891,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui const getWorkspaceResourcesByJobIDs = `-- name: GetWorkspaceResourcesByJobIDs :many SELECT - id, created_at, job_id, transition, type, name, hide, icon, instance_type + id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost FROM workspace_resources WHERE @@ -5806,6 +5917,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uu &i.Hide, &i.Icon, &i.InstanceType, + &i.DailyCost, ); err != nil { return nil, err } @@ -5821,7 +5933,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uu } const getWorkspaceResourcesCreatedAfter = `-- name: GetWorkspaceResourcesCreatedAfter :many -SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type FROM workspace_resources WHERE created_at > $1 +SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost FROM workspace_resources WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) { @@ -5843,6 +5955,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, crea &i.Hide, &i.Icon, &i.InstanceType, + &i.DailyCost, ); err != nil { return nil, err } @@ -5859,9 +5972,9 @@ func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, crea const insertWorkspaceResource = `-- name: InsertWorkspaceResource :one INSERT INTO - workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type) + workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at, job_id, transition, type, name, hide, icon, instance_type + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost ` type InsertWorkspaceResourceParams struct { @@ -5874,6 +5987,7 @@ type InsertWorkspaceResourceParams struct { Hide bool `db:"hide" json:"hide"` Icon string `db:"icon" json:"icon"` InstanceType sql.NullString `db:"instance_type" json:"instance_type"` + DailyCost int32 `db:"daily_cost" json:"daily_cost"` } func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) { @@ -5887,6 +6001,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork arg.Hide, arg.Icon, arg.InstanceType, + arg.DailyCost, ) var i WorkspaceResource err := row.Scan( @@ -5899,6 +6014,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork &i.Hide, &i.Icon, &i.InstanceType, + &i.DailyCost, ) return i, err } diff --git a/coderd/database/queries/groups.sql b/coderd/database/queries/groups.sql index 618ce785526aa..dba2ae79b0ee5 100644 --- a/coderd/database/queries/groups.sql +++ b/coderd/database/queries/groups.sql @@ -75,10 +75,11 @@ INSERT INTO groups ( id, name, organization_id, - avatar_url + avatar_url, + quota_allowance ) VALUES - ( $1, $2, $3, $4) RETURNING *; + ( $1, $2, $3, $4, $5) RETURNING *; -- We use the organization_id as the id -- for simplicity since all users is @@ -97,9 +98,10 @@ UPDATE groups SET name = $1, - avatar_url = $2 + avatar_url = $2, + quota_allowance = $3 WHERE - id = $3 + id = $4 RETURNING *; -- name: InsertGroupMember :exec diff --git a/coderd/database/queries/quotas.sql b/coderd/database/queries/quotas.sql new file mode 100644 index 0000000000000..c640ba02ce982 --- /dev/null +++ b/coderd/database/queries/quotas.sql @@ -0,0 +1,30 @@ +-- name: GetQuotaAllowanceForUser :one +SELECT + coalesce(SUM(quota_allowance), 0)::BIGINT +FROM + group_members gm +JOIN groups g ON + g.id = gm.group_id +WHERE + user_id = $1; + +-- name: GetQuotaConsumedForUser :one +WITH latest_builds AS ( +SELECT + DISTINCT ON + (workspace_id) id, + workspace_id, + daily_cost +FROM + workspace_builds wb +ORDER BY + workspace_id, + created_at DESC +) +SELECT + coalesce(SUM(daily_cost), 0)::BIGINT +FROM + workspaces +JOIN latest_builds ON + latest_builds.workspace_id = workspaces.id +WHERE NOT deleted AND workspaces.owner_id = $1; diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 590421daea353..30658634da4e0 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -133,3 +133,12 @@ SET deadline = $4 WHERE id = $1 RETURNING *; + +-- name: UpdateWorkspaceBuildCostByID :one +UPDATE + workspace_builds +SET + daily_cost = $2 +WHERE + id = $1 RETURNING *; + diff --git a/coderd/database/queries/workspaceresources.sql b/coderd/database/queries/workspaceresources.sql index 9f37e6379a57f..4afcfe3026425 100644 --- a/coderd/database/queries/workspaceresources.sql +++ b/coderd/database/queries/workspaceresources.sql @@ -27,9 +27,9 @@ SELECT * FROM workspace_resources WHERE created_at > $1; -- name: InsertWorkspaceResource :one INSERT INTO - workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type) + workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *; -- name: GetWorkspaceResourceMetadataByResourceID :many SELECT diff --git a/coderd/organizations.go b/coderd/organizations.go index 0165f395c43c8..9068f523488ad 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -88,7 +88,7 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("create %q group: %w", database.AllUsersGroup, err) } return nil - }) + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error inserting organization member.", diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index cc99eefc779e1..fb21c75956aaf 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -86,6 +86,7 @@ func (api *API) ListenProvisionerDaemon(ctx context.Context, acquireJobDebounce Telemetry: api.Telemetry, Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)), AcquireJobDebounce: acquireJobDebounce, + QuotaCommitter: &api.QuotaCommitter, }) if err != nil { return nil, err diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 901e19e2a67c5..0b267de5b17fc 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -9,6 +9,7 @@ import ( "net/url" "reflect" "sync" + "sync/atomic" "time" "github.com/google/uuid" @@ -34,13 +35,14 @@ var ( ) type Server struct { - AccessURL *url.URL - ID uuid.UUID - Logger slog.Logger - Provisioners []database.ProvisionerType - Database database.Store - Pubsub database.Pubsub - Telemetry telemetry.Reporter + AccessURL *url.URL + ID uuid.UUID + Logger slog.Logger + Provisioners []database.ProvisionerType + Database database.Store + Pubsub database.Pubsub + Telemetry telemetry.Reporter + QuotaCommitter *atomic.Pointer[proto.QuotaCommitter] AcquireJobDebounce time.Duration } @@ -252,6 +254,35 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac return protoJob, err } +func (server *Server) CommitQuota(ctx context.Context, request *proto.CommitQuotaRequest) (*proto.CommitQuotaResponse, error) { + jobID, err := uuid.Parse(request.JobId) + if err != nil { + return nil, xerrors.Errorf("parse job id: %w", err) + } + + job, err := server.Database.GetProvisionerJobByID(ctx, jobID) + if err != nil { + return nil, xerrors.Errorf("get job: %w", err) + } + if !job.WorkerID.Valid { + return nil, xerrors.New("job isn't running yet") + } + + if job.WorkerID.UUID.String() != server.ID.String() { + return nil, xerrors.New("you don't own this job") + } + + q := server.QuotaCommitter.Load() + if q == nil { + // We're probably in community edition or a test. + return &proto.CommitQuotaResponse{ + Budget: -1, + Ok: true, + }, nil + } + return (*q).CommitQuota(ctx, request) +} + func (server *Server) UpdateJob(ctx context.Context, request *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) { parsedID, err := uuid.Parse(request.JobId) if err != nil { @@ -620,7 +651,7 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete } return nil - }) + }, nil) if err != nil { return nil, xerrors.Errorf("complete job: %w", err) } @@ -690,6 +721,7 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. Name: protoResource.Name, Hide: protoResource.Hide, Icon: protoResource.Icon, + DailyCost: protoResource.DailyCost, InstanceType: sql.NullString{ String: protoResource.InstanceType, Valid: protoResource.InstanceType != "", diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index d8046f5a64e01..c63775a4e1839 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -745,8 +745,9 @@ func TestInsertWorkspaceResource(t *testing.T) { db := databasefake.New() job := uuid.New() err := insert(db, job, &sdkproto.Resource{ - Name: "something", - Type: "aws_instance", + Name: "something", + Type: "aws_instance", + DailyCost: 10, Agents: []*sdkproto.Agent{{ Name: "dev", Env: map[string]string{ @@ -767,6 +768,7 @@ func TestInsertWorkspaceResource(t *testing.T) { resources, err := db.GetWorkspaceResourcesByJobID(ctx, job) require.NoError(t, err) require.Len(t, resources, 1) + require.EqualValues(t, 10, resources[0].DailyCost) agents, err := db.GetWorkspaceAgentsByResourceIDs(ctx, []uuid.UUID{resources[0].ID}) require.NoError(t, err) require.Len(t, agents, 1) diff --git a/coderd/templates.go b/coderd/templates.go index ec601ab0e22be..69fcd711acad1 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -290,7 +290,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque template = api.convertTemplate(dbTemplate, 0, createdByNameMap[dbTemplate.ID.String()]) return nil - }) + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error inserting template.", @@ -511,7 +511,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { } return nil - }) + }, nil) if err != nil { httpapi.InternalServerError(rw, err) return @@ -690,7 +690,7 @@ func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateO } return nil - }) + }, nil) return template, err } diff --git a/coderd/templateversions.go b/coderd/templateversions.go index 330f0c39b6500..34478b529da99 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -538,7 +538,7 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque } return nil - }) + }, nil) if err != nil { return } @@ -651,7 +651,7 @@ func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Reque return xerrors.Errorf("update active version: %w", err) } return nil - }) + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error updating active template version.", @@ -852,7 +852,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht return xerrors.Errorf("insert template version: %w", err) } return nil - }) + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: err.Error(), diff --git a/coderd/userauth.go b/coderd/userauth.go index 2132df20415ef..eaec92f0ab178 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -520,7 +520,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook } return nil - }) + }, nil) if err != nil { return nil, xerrors.Errorf("in tx: %w", err) } diff --git a/coderd/users.go b/coderd/users.go index 91a2512b6ebc9..116bfe695d351 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -700,7 +700,7 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) { } return nil - }) + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error updating user's password.", @@ -1147,7 +1147,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create return xerrors.Errorf("create organization member: %w", err) } return nil - }) + }, nil) } func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User { diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 54f8d2a9affaa..89dd05dec1a0e 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -136,7 +136,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { } return nil - }) + }, nil) if err != nil { return } @@ -536,7 +536,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { } return nil - }) + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error inserting workspace build.", @@ -931,6 +931,7 @@ func (api *API) convertWorkspaceBuild( Reason: codersdk.BuildReason(build.Reason), Resources: apiResources, Status: convertWorkspaceStatus(apiJob.Status, transition), + DailyCost: build.DailyCost, }, nil } @@ -974,6 +975,7 @@ func convertWorkspaceResource(resource database.WorkspaceResource, agents []code Icon: resource.Icon, Agents: agents, Metadata: convertedMetadata, + DailyCost: resource.DailyCost, } } diff --git a/coderd/workspacequota/workspacequota.go b/coderd/workspacequota/workspacequota.go deleted file mode 100644 index 54bd46ca4165d..0000000000000 --- a/coderd/workspacequota/workspacequota.go +++ /dev/null @@ -1,19 +0,0 @@ -package workspacequota - -type Enforcer interface { - UserWorkspaceLimit() int - CanCreateWorkspace(count int) bool -} - -type nop struct{} - -func NewNop() Enforcer { - return &nop{} -} - -func (*nop) UserWorkspaceLimit() int { - return 0 -} -func (*nop) CanCreateWorkspace(_ int) bool { - return true -} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 4a6b36a8760e9..5f17e5da5e05e 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -342,25 +342,6 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } - workspaceCount, err := api.Database.GetWorkspaceCountByUserID(ctx, user.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace count.", - Detail: err.Error(), - }) - return - } - - // make sure the user has not hit their quota limit - e := *api.WorkspaceQuotaEnforcer.Load() - canCreate := e.CanCreateWorkspace(int(workspaceCount)) - if !canCreate { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("User workspace limit of %d is already reached.", e.UserWorkspaceLimit()), - }) - return - } - templateVersion, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -479,7 +460,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return xerrors.Errorf("insert workspace build: %w", err) } return nil - }) + }, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error creating workspace.", @@ -710,7 +691,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { } return nil - }) + }, nil) if err != nil { resp := codersdk.Response{ Message: "Error updating workspace time until shutdown.", @@ -807,7 +788,7 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) { resp.Message = "Deadline updated to " + newDeadline.Format(time.RFC3339) + "." return nil - }) + }, nil) if err != nil { api.Logger.Info(ctx, "extending workspace", slog.Error(err)) } diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go index 904838201dedf..cb56ba5aa88e0 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deploymentconfig.go @@ -37,7 +37,6 @@ type DeploymentConfig struct { AuditLogging *DeploymentConfigField[bool] `json:"audit_logging" typescript:",notnull"` BrowserOnly *DeploymentConfigField[bool] `json:"browser_only" typescript:",notnull"` SCIMAPIKey *DeploymentConfigField[string] `json:"scim_api_key" typescript:",notnull"` - UserWorkspaceQuota *DeploymentConfigField[int] `json:"user_workspace_quota" typescript:",notnull"` Provisioner *ProvisionerConfig `json:"provisioner" typescript:",notnull"` APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"` Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"` diff --git a/codersdk/features.go b/codersdk/features.go index 336c01cc9eda9..07e836715ab6e 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -19,7 +19,6 @@ const ( FeatureAuditLog = "audit_log" FeatureBrowserOnly = "browser_only" FeatureSCIM = "scim" - FeatureWorkspaceQuota = "workspace_quota" FeatureTemplateRBAC = "template_rbac" FeatureHighAvailability = "high_availability" FeatureMultipleGitAuth = "multiple_git_auth" @@ -30,7 +29,6 @@ var FeatureNames = []string{ FeatureAuditLog, FeatureBrowserOnly, FeatureSCIM, - FeatureWorkspaceQuota, FeatureTemplateRBAC, FeatureHighAvailability, FeatureMultipleGitAuth, diff --git a/codersdk/groups.go b/codersdk/groups.go index 340fffaddd859..0647efd6e9d93 100644 --- a/codersdk/groups.go +++ b/codersdk/groups.go @@ -11,8 +11,9 @@ import ( ) type CreateGroupRequest struct { - Name string `json:"name"` - AvatarURL string `json:"avatar_url"` + Name string `json:"name"` + AvatarURL string `json:"avatar_url"` + QuotaAllowance int `json:"quota_allowance"` } type Group struct { @@ -21,6 +22,7 @@ type Group struct { OrganizationID uuid.UUID `json:"organization_id"` Members []User `json:"members"` AvatarURL string `json:"avatar_url"` + QuotaAllowance int `json:"quota_allowance"` } func (c *Client) CreateGroup(ctx context.Context, orgID uuid.UUID, req CreateGroupRequest) (Group, error) { @@ -93,10 +95,11 @@ func (c *Client) Group(ctx context.Context, group uuid.UUID) (Group, error) { } type PatchGroupRequest struct { - AddUsers []string `json:"add_users"` - RemoveUsers []string `json:"remove_users"` - Name string `json:"name"` - AvatarURL *string `json:"avatar_url"` + AddUsers []string `json:"add_users"` + RemoveUsers []string `json:"remove_users"` + Name string `json:"name"` + AvatarURL *string `json:"avatar_url"` + QuotaAllowance *int `json:"quota_allowance"` } func (c *Client) PatchGroup(ctx context.Context, group uuid.UUID, req PatchGroupRequest) (Group, error) { diff --git a/codersdk/workspacequota.go b/codersdk/quota.go similarity index 83% rename from codersdk/workspacequota.go rename to codersdk/quota.go index 823e843e0e6e6..60ca4569aba0f 100644 --- a/codersdk/workspacequota.go +++ b/codersdk/quota.go @@ -8,8 +8,8 @@ import ( ) type WorkspaceQuota struct { - UserWorkspaceCount int `json:"user_workspace_count"` - UserWorkspaceLimit int `json:"user_workspace_limit"` + CreditsConsumed int `json:"credits_consumed"` + Budget int `json:"budget"` } func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQuota, error) { diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 19ad6acecf6f5..c98ed4c3ee9ee 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -68,6 +68,7 @@ type WorkspaceBuild struct { Resources []WorkspaceResource `json:"resources"` Deadline NullTime `json:"deadline,omitempty"` Status WorkspaceStatus `json:"status"` + DailyCost int32 `json:"daily_cost"` } type WorkspaceResource struct { @@ -81,6 +82,7 @@ type WorkspaceResource struct { Icon string `json:"icon"` Agents []WorkspaceAgent `json:"agents,omitempty"` Metadata []WorkspaceResourceMetadata `json:"metadata,omitempty"` + DailyCost int32 `json:"daily_cost"` } type WorkspaceResourceMetadata struct { diff --git a/docs/admin/quotas.md b/docs/admin/quotas.md index 77c43acd66038..59158b7dac87b 100644 --- a/docs/admin/quotas.md +++ b/docs/admin/quotas.md @@ -1,23 +1,95 @@ # Quotas -Coder Enterprise admins may define deployment-level quotas to protect against -Denial-of-Service, control costs, and ensure equitable access to cloud resources. +Coder Enterprise admins may define quotas to control costs +and ensure equitable access to cloud resources. The quota system controls +instantaneous cost. For example, the system can ensure that every user in your +deployment has a spend rate lower than $10/day at any given moment. -The quota is enabled by either the `CODER_USER_WORKSPACE_QUOTA` -environment variable or the `--user-workspace-quota` flag. For example, -you may limit each user in a deployment to 5 workspaces like so: +The workspace provisioner enforces quota during workspace start and stop operations. +When users reach their quota, they may unblock themselves by stopping or deleting +their workspace(s). -```bash -coder server --user-workspace-quota=5 +Quotas are licensed with [Groups](./groups.md). + +## Definitions + +- **Credits** is the fundamental unit of the quota system. They map to the + smallest denomination of your preferred currency. For example, if you work with USD, + think of each credit as a cent. +- **Budget** is the per-user, enforced, upper limit to credit spend. +- **Allowance** is a grant of credits to the budget. + +## Establishing Costs + +Templates describe their cost through the `daily_cost` attribute in +[`resource_metadata`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata). +Since costs are associated with resources, an offline workspace may consume +less quota than an online workspace. + +A common use case is separating costs for a persistent volume and ephemeral compute: + +```hcl +resource "coder_metadata" "volume" { + resource_id = "${docker_volume.home_volume.id}" + cost = 10 +} + +resource "docker_volume" "home_volume" { + name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" +} + +resource "coder_metadata" "container" { + resource_id = "${docker_container.workspace.id}" + cost = 20 +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = "codercom/code-server:latest" + ... + volumes { + container_path = "/home/coder/" + volume_name = docker_volume.home_volume.name + read_only = false + } +} ``` -Then, when users create workspaces they would see: +In that template, the workspace consumes 10 quota credits when it's offline, and +30 when it's online. + +## Establishing Budgets + +Each group has a configurable Quota Allowance. A user's budget is calculated as +the sum of their allowances. + +![group-settings](../images/admin/quota-groups.png) + +For example: + +| Group Name | Quota Allowance | +| ---------- | --------------- | +| Frontend | 100 | +| Backend | 200 | +| Data | 300 | + +
+ +| Username | Groups | Effective Budget | +| -------- | ----------------- | ---------------- | +| jill | Frontend, Backend | 300 | +| jack | Backend, Data | 500 | +| sam | Data | 300 | +| alex | Frontend | 100 | - +## Quota Enforcement -## Enabling this feature +Coder enforces Quota on workspace start and stop operations. The workspace +build process dynamically calculates costs, so quota violation fails builds +as opposed to failing the build-triggering operation. For example, the Workspace +Create Form will never get held up by quota enforcement. -This feature is only available with an enterprise license. [Learn more](../enterprise.md) +![build-log](../images/admin/quota-buildlog.png) ## Up next diff --git a/docs/images/admin/quota-buildlog.png b/docs/images/admin/quota-buildlog.png new file mode 100644 index 0000000000000..4bee4ab435ac8 Binary files /dev/null and b/docs/images/admin/quota-buildlog.png differ diff --git a/docs/images/admin/quota-groups.png b/docs/images/admin/quota-groups.png new file mode 100644 index 0000000000000..5874b2f8b1ff9 Binary files /dev/null and b/docs/images/admin/quota-groups.png differ diff --git a/docs/images/admin/quotas.png b/docs/images/admin/quotas.png deleted file mode 100644 index 5149c9125b2ed..0000000000000 Binary files a/docs/images/admin/quotas.png and /dev/null differ diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 18649460d0aa3..68f51813627c1 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -109,6 +109,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "name": ActionTrack, "organization_id": ActionIgnore, // Never changes. "avatar_url": ActionTrack, + "quota_allowance": ActionTrack, }, // We don't show any diff for the WorkspaceBuild resource, // save for the template_version_id @@ -125,6 +126,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "job_id": ActionIgnore, "deadline": ActionIgnore, "reason": ActionIgnore, + "daily_cost": ActionIgnore, }, }) diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index 46595ca3fdd00..4a2d41a7eceea 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -61,7 +61,6 @@ func server() *cobra.Command { AuditLogging: options.DeploymentConfig.AuditLogging.Value, BrowserOnly: options.DeploymentConfig.BrowserOnly.Value, SCIMAPIKey: []byte(options.DeploymentConfig.SCIMAPIKey.Value), - UserWorkspaceQuota: options.DeploymentConfig.UserWorkspaceQuota.Value, RBAC: true, DERPServerRelayAddress: options.DeploymentConfig.DERP.Server.RelayURL.Value, DERPServerRegionID: options.DeploymentConfig.DERP.Server.RegionID.Value, diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 02664556914c8..10c40b1b38718 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -20,12 +20,12 @@ import ( "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" - "github.com/coder/coder/coderd/workspacequota" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/enterprise/derpmesh" "github.com/coder/coder/enterprise/replicasync" "github.com/coder/coder/enterprise/tailnet" + "github.com/coder/coder/provisionerd/proto" agpltailnet "github.com/coder/coder/tailnet" ) @@ -113,7 +113,9 @@ func New(ctx context.Context, options *Options) (*API, error) { }) r.Route("/workspace-quota", func(r chi.Router) { - r.Use(apiKeyMiddleware) + r.Use( + apiKeyMiddleware, + ) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database, false)) r.Get("/", api.workspaceQuota) @@ -183,9 +185,8 @@ type Options struct { RBAC bool AuditLogging bool // Whether to block non-browser connections. - BrowserOnly bool - SCIMAPIKey []byte - UserWorkspaceQuota int + BrowserOnly bool + SCIMAPIKey []byte // Used for high availability. DERPServerRelayAddress string @@ -224,7 +225,6 @@ func (api *API) updateEntitlements(ctx context.Context) error { codersdk.FeatureAuditLog: api.AuditLogging, codersdk.FeatureBrowserOnly: api.BrowserOnly, codersdk.FeatureSCIM: len(api.SCIMAPIKey) != 0, - codersdk.FeatureWorkspaceQuota: api.UserWorkspaceQuota != 0, codersdk.FeatureHighAvailability: api.DERPServerRelayAddress != "", codersdk.FeatureMultipleGitAuth: len(api.GitAuthConfigs) > 1, codersdk.FeatureTemplateRBAC: api.RBAC, @@ -262,12 +262,14 @@ func (api *API) updateEntitlements(ctx context.Context) error { api.AGPL.WorkspaceClientCoordinateOverride.Store(&handler) } - if changed, enabled := featureChanged(codersdk.FeatureWorkspaceQuota); changed { - enforcer := workspacequota.NewNop() + if changed, enabled := featureChanged(codersdk.FeatureTemplateRBAC); changed { if enabled { - enforcer = NewEnforcer(api.Options.UserWorkspaceQuota) + committer := committer{Database: api.Database} + ptr := proto.QuotaCommitter(&committer) + api.AGPL.QuotaCommitter.Store(&ptr) + } else { + api.AGPL.QuotaCommitter.Store(nil) } - api.AGPL.WorkspaceQuotaEnforcer.Store(&enforcer) } if changed, enabled := featureChanged(codersdk.FeatureHighAvailability); changed { diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 341cb87dff1ec..3348227c7d29b 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -70,7 +70,6 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c SCIMAPIKey: options.SCIMAPIKey, DERPServerRelayAddress: oop.AccessURL.String(), DERPServerRegionID: oop.DERPMap.RegionIDs()[0], - UserWorkspaceQuota: options.UserWorkspaceQuota, Options: oop, EntitlementsUpdateInterval: options.EntitlementsUpdateInterval, Keys: Keys, @@ -110,7 +109,6 @@ type LicenseOptions struct { AuditLog bool BrowserOnly bool SCIM bool - WorkspaceQuota bool TemplateRBAC bool HighAvailability bool MultipleGitAuth bool @@ -145,10 +143,6 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { if options.SCIM { scim = 1 } - var workspaceQuota int64 - if options.WorkspaceQuota { - workspaceQuota = 1 - } highAvailability := int64(0) if options.HighAvailability { highAvailability = 1 @@ -182,7 +176,6 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { AuditLog: auditLog, BrowserOnly: browserOnly, SCIM: scim, - WorkspaceQuota: workspaceQuota, HighAvailability: highAvailability, TemplateRBAC: rbacEnabled, MultipleGitAuth: multipleGitAuth, diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index 6537602eb3b7d..e467ba5b06fa4 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -53,6 +53,7 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) Name: req.Name, OrganizationID: org.ID, AvatarURL: req.AvatarURL, + QuotaAllowance: int32(req.QuotaAllowance), }) if database.IsUniqueViolation(err) { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ @@ -155,19 +156,25 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("get group by ID: %w", err) } + updateGroupParams := database.UpdateGroupByIDParams{ + ID: group.ID, + AvatarURL: group.AvatarURL, + Name: group.Name, + QuotaAllowance: group.QuotaAllowance, + } + // TODO: Do we care about validating this? if req.AvatarURL != nil { - group.AvatarURL = *req.AvatarURL + updateGroupParams.AvatarURL = *req.AvatarURL } if req.Name != "" { - group.Name = req.Name + updateGroupParams.Name = req.Name + } + if req.QuotaAllowance != nil { + updateGroupParams.QuotaAllowance = int32(*req.QuotaAllowance) } - group, err = tx.UpdateGroupByID(ctx, database.UpdateGroupByIDParams{ - ID: group.ID, - Name: group.Name, - AvatarURL: group.AvatarURL, - }) + group, err = tx.UpdateGroupByID(ctx, updateGroupParams) if err != nil { return xerrors.Errorf("update group by ID: %w", err) } @@ -188,7 +195,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { } } return nil - }) + }, nil) if database.IsUniqueViolation(err) { httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{ Message: "Cannot add the same user to a group twice!", @@ -327,6 +334,7 @@ func convertGroup(g database.Group, users []database.User) codersdk.Group { Name: g.Name, OrganizationID: g.OrganizationID, AvatarURL: g.AvatarURL, + QuotaAllowance: int(g.QuotaAllowance), Members: convertUsers(users, orgs), } } diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 2f800c1ad41ce..13381a2faa5d5 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -129,18 +129,22 @@ func TestPatchGroup(t *testing.T) { }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ - Name: "hi", - AvatarURL: "https://example.com", + Name: "hi", + AvatarURL: "https://example.com", + QuotaAllowance: 10, }) require.NoError(t, err) + require.Equal(t, 10, group.QuotaAllowance) group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{ - Name: "bye", - AvatarURL: pointer.String("https://google.com"), + Name: "bye", + AvatarURL: pointer.String("https://google.com"), + QuotaAllowance: pointer.Int(20), }) require.NoError(t, err) require.Equal(t, "bye", group.Name) require.Equal(t, "https://google.com", group.AvatarURL) + require.Equal(t, 20, group.QuotaAllowance) }) // The FE sends a request from the edit page where the old name == new name. diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index 80f502f527f2d..5307c490e3ae5 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -99,12 +99,6 @@ func Entitlements( Enabled: enablements[codersdk.FeatureSCIM], } } - if claims.Features.WorkspaceQuota > 0 { - entitlements.Features[codersdk.FeatureWorkspaceQuota] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: enablements[codersdk.FeatureWorkspaceQuota], - } - } if claims.Features.HighAvailability > 0 { entitlements.Features[codersdk.FeatureHighAvailability] = codersdk.Feature{ Entitlement: entitlement, @@ -248,7 +242,6 @@ type Features struct { AuditLog int64 `json:"audit_log"` BrowserOnly int64 `json:"browser_only"` SCIM int64 `json:"scim"` - WorkspaceQuota int64 `json:"workspace_quota"` TemplateRBAC int64 `json:"template_rbac"` HighAvailability int64 `json:"high_availability"` MultipleGitAuth int64 `json:"multiple_git_auth"` diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go index a1b26f8dab9e4..d1262e0833178 100644 --- a/enterprise/coderd/license/license_test.go +++ b/enterprise/coderd/license/license_test.go @@ -23,7 +23,6 @@ func TestEntitlements(t *testing.T) { codersdk.FeatureAuditLog: true, codersdk.FeatureBrowserOnly: true, codersdk.FeatureSCIM: true, - codersdk.FeatureWorkspaceQuota: true, codersdk.FeatureHighAvailability: true, codersdk.FeatureTemplateRBAC: true, codersdk.FeatureMultipleGitAuth: true, @@ -66,7 +65,6 @@ func TestEntitlements(t *testing.T) { AuditLog: true, BrowserOnly: true, SCIM: true, - WorkspaceQuota: true, HighAvailability: true, TemplateRBAC: true, MultipleGitAuth: true, @@ -90,7 +88,6 @@ func TestEntitlements(t *testing.T) { AuditLog: true, BrowserOnly: true, SCIM: true, - WorkspaceQuota: true, HighAvailability: true, TemplateRBAC: true, GraceAt: time.Now().Add(-time.Hour), diff --git a/enterprise/coderd/licenses_test.go b/enterprise/coderd/licenses_test.go index b4254bfccf4f5..0605ff2f742c1 100644 --- a/enterprise/coderd/licenses_test.go +++ b/enterprise/coderd/licenses_test.go @@ -105,7 +105,6 @@ func TestGetLicense(t *testing.T) { codersdk.FeatureAuditLog: json.Number("1"), codersdk.FeatureSCIM: json.Number("1"), codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureWorkspaceQuota: json.Number("0"), codersdk.FeatureHighAvailability: json.Number("0"), codersdk.FeatureTemplateRBAC: json.Number("1"), codersdk.FeatureMultipleGitAuth: json.Number("0"), @@ -118,7 +117,6 @@ func TestGetLicense(t *testing.T) { codersdk.FeatureAuditLog: json.Number("1"), codersdk.FeatureSCIM: json.Number("1"), codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureWorkspaceQuota: json.Number("0"), codersdk.FeatureHighAvailability: json.Number("0"), codersdk.FeatureTemplateRBAC: json.Number("0"), codersdk.FeatureMultipleGitAuth: json.Number("0"), diff --git a/enterprise/coderd/templates.go b/enterprise/coderd/templates.go index 9fbcb403735e8..0e11261710f00 100644 --- a/enterprise/coderd/templates.go +++ b/enterprise/coderd/templates.go @@ -171,7 +171,7 @@ func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("update template ACL by ID: %w", err) } return nil - }) + }, nil) if err != nil { httpapi.InternalServerError(rw, err) return diff --git a/enterprise/coderd/workspacequota.go b/enterprise/coderd/workspacequota.go index ab345d3681f87..67d07d91939ca 100644 --- a/enterprise/coderd/workspacequota.go +++ b/enterprise/coderd/workspacequota.go @@ -1,36 +1,102 @@ package coderd import ( + "context" + "database/sql" "net/http" + "github.com/google/uuid" + "golang.org/x/xerrors" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/coderd/rbac" - "github.com/coder/coder/coderd/workspacequota" "github.com/coder/coder/codersdk" + "github.com/coder/coder/provisionerd/proto" ) -type enforcer struct { - userWorkspaceLimit int +type committer struct { + Database database.Store } -func NewEnforcer(userWorkspaceLimit int) workspacequota.Enforcer { - return &enforcer{ - userWorkspaceLimit: userWorkspaceLimit, +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 } -} -func (e *enforcer) UserWorkspaceLimit() int { - return e.userWorkspaceLimit -} + build, err := c.Database.GetWorkspaceBuildByJobID(ctx, jobID) + if err != nil { + return nil, err + } -func (e *enforcer) CanCreateWorkspace(count int) bool { - if e.userWorkspaceLimit == 0 { - return true + workspace, err := c.Database.GetWorkspaceByID(ctx, build.WorkspaceID) + if err != nil { + return nil, err } - return count < e.userWorkspaceLimit + 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 + previousBuild, err := s.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ + WorkspaceID: workspace.ID, + BuildNumber: build.BuildNumber - 1, + }) + if err == nil { + if build.DailyCost < previousBuild.DailyCost { + netIncrease = false + } + } else if !xerrors.Is(err, sql.ErrNoRows) { + return err + } + + newConsumed := int64(request.DailyCost) + consumed + if newConsumed > budget && netIncrease { + return nil + } + + _, err = s.UpdateWorkspaceBuildCostByID(ctx, database.UpdateWorkspaceBuildCostByIDParams{ + ID: build.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 } func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) { @@ -41,20 +107,35 @@ func (api *API) workspaceQuota(rw http.ResponseWriter, r *http.Request) { return } - workspaces, err := api.Database.GetWorkspaces(r.Context(), database.GetWorkspacesParams{ - OwnerID: user.ID, - }) + 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: "Internal error fetching workspaces.", + Message: "Failed to get consumed", Detail: err.Error(), }) return } - e := *api.AGPL.WorkspaceQuotaEnforcer.Load() httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.WorkspaceQuota{ - UserWorkspaceCount: len(workspaces), - UserWorkspaceLimit: e.UserWorkspaceLimit(), + CreditsConsumed: int(quotaConsumed), + Budget: int(quotaAllowance), }) } diff --git a/enterprise/coderd/workspacequota_test.go b/enterprise/coderd/workspacequota_test.go index 450eeb35242e7..98118f310aa7f 100644 --- a/enterprise/coderd/workspacequota_test.go +++ b/enterprise/coderd/workspacequota_test.go @@ -3,13 +3,11 @@ package coderd_test import ( "context" "testing" - "time" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" "github.com/coder/coder/provisioner/echo" @@ -17,49 +15,22 @@ import ( "github.com/coder/coder/testutil" ) +func verifyQuota(ctx context.Context, t *testing.T, client *codersdk.Client, consumed, total int) { + t.Helper() + + got, err := client.WorkspaceQuota(ctx, codersdk.Me) + require.NoError(t, err) + require.EqualValues(t, codersdk.WorkspaceQuota{ + Budget: total, + CreditsConsumed: consumed, + }, got) +} + func TestWorkspaceQuota(t *testing.T) { + // TODO: refactor for new impl + t.Parallel() - t.Run("Disabled", func(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - client := coderdenttest.New(t, &coderdenttest.Options{}) - _ = coderdtest.CreateFirstUser(t, client) - coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - WorkspaceQuota: true, - }) - q1, err := client.WorkspaceQuota(ctx, codersdk.Me) - require.NoError(t, err) - require.EqualValues(t, q1.UserWorkspaceLimit, 0) - }) - t.Run("Enabled", func(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - max := 3 - client := coderdenttest.New(t, &coderdenttest.Options{ - UserWorkspaceQuota: max, - }) - user := coderdtest.CreateFirstUser(t, client) - coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - WorkspaceQuota: true, - }) - q1, err := client.WorkspaceQuota(ctx, codersdk.Me) - require.NoError(t, err) - require.EqualValues(t, q1.UserWorkspaceLimit, max) - - // ensure other user IDs work too - u2, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ - Email: "whatever@yo.com", - Username: "haha", - Password: "laskjdnvkaj", - OrganizationID: user.OrganizationID, - }) - require.NoError(t, err) - q2, err := client.WorkspaceQuota(ctx, u2.ID.String()) - require.NoError(t, err) - require.EqualValues(t, q1, q2) - }) + t.Run("BlocksBuild", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -71,14 +42,38 @@ func TestWorkspaceQuota(t *testing.T) { IncludeProvisionerDaemon: true, }, }) + user := coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - WorkspaceQuota: true, + TemplateRBAC: true, + }) + + verifyQuota(ctx, t, client, 0, 0) + + // Add user to two groups, granting them a total budget of 3. + group1, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ + Name: "test-1", + QuotaAllowance: 1, + }) + require.NoError(t, err) + + group2, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ + Name: "test-2", + QuotaAllowance: 2, }) - q1, err := client.WorkspaceQuota(ctx, codersdk.Me) require.NoError(t, err) - require.EqualValues(t, q1.UserWorkspaceCount, 0) - require.EqualValues(t, q1.UserWorkspaceLimit, max) + + _, err = client.PatchGroup(ctx, group1.ID, codersdk.PatchGroupRequest{ + AddUsers: []string{user.UserID.String()}, + }) + require.NoError(t, err) + + _, err = client.PatchGroup(ctx, group2.ID, codersdk.PatchGroupRequest{ + AddUsers: []string{user.UserID.String()}, + }) + require.NoError(t, err) + + verifyQuota(ctx, t, client, 0, 3) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ @@ -87,8 +82,9 @@ func TestWorkspaceQuota(t *testing.T) { Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", + Name: "example", + Type: "aws_instance", + DailyCost: 1, Agents: []*proto.Agent{{ Id: uuid.NewString(), Name: "example", @@ -103,20 +99,45 @@ func TestWorkspaceQuota(t *testing.T) { }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - _, err = client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{ - TemplateID: template.ID, - Name: "ajksdnvksjd", - AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"), - TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()), - }) - require.Error(t, err) - require.ErrorContains(t, err, "User workspace limit") - // ensure count increments - q1, err = client.WorkspaceQuota(ctx, codersdk.Me) + // Spin up three workspaces fine + for i := 0; i < 3; i++ { + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + verifyQuota(ctx, t, client, i+1, 3) + require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status) + } + + // Next one must fail + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + // Consumed shouldn't bump + verifyQuota(ctx, t, client, 3, 3) + require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status) + require.Contains(t, build.Job.Error, "quota") + + // Delete one random workspace, then quota should recover. + workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err) - require.EqualValues(t, q1.UserWorkspaceCount, 1) - require.EqualValues(t, q1.UserWorkspaceLimit, max) + for _, w := range workspaces.Workspaces { + if w.LatestBuild.Status != codersdk.WorkspaceStatusRunning { + continue + } + build, err := client.CreateWorkspaceBuild(ctx, w.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionDelete, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + verifyQuota(ctx, t, client, 2, 3) + break + } + + // Next one should now succeed + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + build = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + verifyQuota(ctx, t, client, 3, 3) + require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status) }) } diff --git a/loadtest/placebo/run_test.go b/loadtest/placebo/run_test.go index 2ec3e00abde62..a48073b756b15 100644 --- a/loadtest/placebo/run_test.go +++ b/loadtest/placebo/run_test.go @@ -14,6 +14,7 @@ import ( ) func Test_Runner(t *testing.T) { + t.Skip("This test is flakey, see https://github.com/coder/coder/actions/runs/3463709674/jobs/5784335013#step:9:215") t.Parallel() t.Run("NoSleep", func(t *testing.T) { diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index f1ad47e3c21b9..e1fead516213b 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -55,6 +55,7 @@ type metadataAttributes struct { ResourceID string `mapstructure:"resource_id"` Hide bool `mapstructure:"hide"` Icon string `mapstructure:"icon"` + DailyCost int32 `mapstructure:"daily_cost"` Items []metadataItem `mapstructure:"item"` } @@ -301,6 +302,8 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res resourceMetadata := map[string][]*proto.Resource_Metadata{} resourceHidden := map[string]bool{} resourceIcon := map[string]string{} + resourceCost := map[string]int32{} + for _, resource := range tfResourceByLabel { if resource.Type != "coder_metadata" { continue @@ -360,6 +363,7 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res resourceHidden[targetLabel] = attrs.Hide resourceIcon[targetLabel] = attrs.Icon + resourceCost[targetLabel] = attrs.DailyCost for _, item := range attrs.Items { resourceMetadata[targetLabel] = append(resourceMetadata[targetLabel], &proto.Resource_Metadata{ @@ -389,9 +393,10 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res Name: resource.Name, Type: resource.Type, Agents: agents, + Metadata: resourceMetadata[label], Hide: resourceHidden[label], Icon: resourceIcon[label], - Metadata: resourceMetadata[label], + DailyCost: resourceCost[label], InstanceType: applyInstanceType(resource), }) } diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 15708d1203f2d..781f3ae5d0bea 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -145,10 +145,11 @@ func TestConvertResources(t *testing.T) { }}, // Tests fetching metadata about workspace resources. "resource-metadata": {{ - Name: "about", - Type: "null_resource", - Hide: true, - Icon: "/icon/server.svg", + Name: "about", + Type: "null_resource", + Hide: true, + Icon: "/icon/server.svg", + DailyCost: 29, Metadata: []*proto.Resource_Metadata{{ Key: "hello", Value: "world", diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json index 46381b0166485..348af6e0f3912 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json index 6972321b7372a..d4287a851a47e 100644 --- a/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json +++ b/provisioner/terraform/testdata/calling-module/calling-module.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "f7ee18b5-2baf-461b-9d82-7654c669930c", + "id": "5c92d003-112d-4eb1-8e5f-d3009aa52fcb", "init_script": "", "os": "linux", "startup_script": null, - "token": "2b47c5a8-1511-46f5-9821-95510c83afb2", + "token": "fedbf404-c42d-4360-815b-5ffc34198df3", "troubleshooting_url": null }, "sensitive_values": {} diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json index 005545d0e9d61..a7d149ee63801 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json index 51177db018be9..0261042eb39de 100644 --- a/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json +++ b/provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "9f9cf95a-77ea-40bf-b9bc-055f4971923d", + "id": "6cc2be0d-fe90-4256-944f-482787433587", "init_script": "", "os": "linux", "startup_script": null, - "token": "0d846d9b-1fa9-4ab3-962c-f249395645a7", + "token": "1927809c-5fcf-4fdd-94d7-9a619fb86d13", "troubleshooting_url": null }, "sensitive_values": {} diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json index 36668eb68b3fa..773e0b31f0d71 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json index 9d482ecd3d634..f090953a2c2b8 100644 --- a/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json +++ b/provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "aac3d245-52fa-4588-9759-694017908e7c", + "id": "bcaf2577-5dfd-4083-a446-789092a7babe", "init_script": "", "os": "linux", "startup_script": null, - "token": "a9f32b40-d630-47f0-80a6-5727fd729fae", + "token": "862867af-cf08-4aea-a2af-70d0014f848b", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,7 +34,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "5577006791947779410", + "id": "8674665223082153551", "triggers": null }, "sensitive_values": {}, @@ -50,7 +50,7 @@ "provider_name": "registry.terraform.io/hashicorp/null", "schema_version": 0, "values": { - "id": "8674665223082153551", + "id": "5577006791947779410", "triggers": null }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json index a9f1e328d9a0d..f2c2863a8eee2 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json index b771a3fc263b9..d13bba529a1aa 100644 --- a/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json +++ b/provisioner/terraform/testdata/instance-id/instance-id.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "327a022c-8b88-4d57-8d25-4b1dd66d817b", + "id": "30431432-7afb-4d73-8eeb-ee464a28e157", "init_script": "", "os": "linux", "startup_script": null, - "token": "a0082875-2172-456c-a8fe-ae45499443af", + "token": "3ce9bbd8-0f31-4460-842b-8e9c1de9a567", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,8 +34,8 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "327a022c-8b88-4d57-8d25-4b1dd66d817b", - "id": "ddb786af-b1a5-4b22-954b-387177e17f16", + "agent_id": "30431432-7afb-4d73-8eeb-ee464a28e157", + "id": "679f9bf2-8887-4201-a5cd-e53913e8d361", "instance_id": "example" }, "sensitive_values": {}, diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json index 937e59a44394e..a6c92cf1e4fb2 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json index 55e960948ad08..eebfce2376cd5 100644 --- a/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json +++ b/provisioner/terraform/testdata/multiple-agents/multiple-agents.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "2777eff3-2f9f-4515-8cea-0dc7dbb53bf0", + "id": "e545d734-f852-4fda-ac8f-39e3ff094e58", "init_script": "", "os": "linux", "startup_script": null, - "token": "0fb4dd96-6acd-48d2-a41a-396e957cf5f6", + "token": "c2c47266-af7a-467c-9ffc-30c3270ffecb", "troubleshooting_url": null }, "sensitive_values": {} @@ -39,11 +39,11 @@ "connection_timeout": 1, "dir": null, "env": null, - "id": "48c7e389-c6a3-4cff-8331-aec26ee42cc4", + "id": "b5e18556-d202-478f-80d9-76f34a4cb105", "init_script": "", "os": "darwin", "startup_script": null, - "token": "0e9a30ca-59a4-4070-8517-0f7ebc5d1ab8", + "token": "795082f9-642a-4647-a595-6539edaa74a3", "troubleshooting_url": null }, "sensitive_values": {} @@ -61,11 +61,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "f4b435ff-47a5-4fd5-8529-5ca0288eec6d", + "id": "27e1114a-bc92-4e35-ab57-1680f3b7658f", "init_script": "", "os": "windows", "startup_script": null, - "token": "763e2baa-36d0-45d6-9511-08034fa752ca", + "token": "c4fc1679-eb42-4d9f-bca8-fcf9641a7256", "troubleshooting_url": "https://coder.com/troubleshoot" }, "sensitive_values": {} diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json index ef24b1ccee579..04c77276e3669 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "planned_values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json index 83b442def3129..07c2f8ce17eed 100644 --- a/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json +++ b/provisioner/terraform/testdata/multiple-apps/multiple-apps.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "8cf70a43-80fa-4f84-b651-f6ff21f7fbcc", + "id": "f911bd98-54fc-476a-aec1-df6e525630a9", "init_script": "", "os": "linux", "startup_script": null, - "token": "b41f44b7-f889-4ce9-9061-4f34952103d7", + "token": "fa05ad9c-2062-4707-a27f-12364c89641e", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,12 +34,12 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "8cf70a43-80fa-4f84-b651-f6ff21f7fbcc", + "agent_id": "f911bd98-54fc-476a-aec1-df6e525630a9", "command": null, "display_name": null, "healthcheck": [], "icon": null, - "id": "927154ef-0b53-4b90-b6de-034581c46759", + "id": "038d0f6c-90b7-465b-915a-8a9f0cf21757", "name": null, "relative_path": null, "share": "owner", @@ -62,7 +62,7 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "8cf70a43-80fa-4f84-b651-f6ff21f7fbcc", + "agent_id": "f911bd98-54fc-476a-aec1-df6e525630a9", "command": null, "display_name": null, "healthcheck": [ @@ -73,7 +73,7 @@ } ], "icon": null, - "id": "e8b2c750-ac93-4126-964f-603cb06aa12e", + "id": "c00ec121-a167-4418-8c4e-2ccae0a0cd6e", "name": null, "relative_path": null, "share": "owner", @@ -98,12 +98,12 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "agent_id": "8cf70a43-80fa-4f84-b651-f6ff21f7fbcc", + "agent_id": "f911bd98-54fc-476a-aec1-df6e525630a9", "command": null, "display_name": null, "healthcheck": [], "icon": null, - "id": "a1897b85-9691-4c7b-9a10-ff87c12efc89", + "id": "e9226aa6-a1a6-42a7-8557-64620cbf3dc2", "name": null, "relative_path": null, "share": "owner", diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf index f08158652c2b9..07dcdffdac150 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.6.1" + version = "0.6.3" } } } @@ -18,6 +18,7 @@ resource "coder_metadata" "about_info" { resource_id = null_resource.about.id hide = true icon = "/icon/server.svg" + daily_cost = 29 item { key = "hello" value = "world" diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json index 6627f89591360..6fc6591b820a6 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.1", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "planned_values": { "root_module": { "resources": [ @@ -31,6 +31,7 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { + "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", "item": [ @@ -125,6 +126,7 @@ ], "before": null, "after": { + "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", "item": [ @@ -206,7 +208,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "0.6.1" + "version_constraint": "0.6.3" }, "null": { "name": "null", @@ -238,6 +240,9 @@ "name": "about_info", "provider_config_key": "coder", "expressions": { + "daily_cost": { + "constant_value": 29 + }, "hide": { "constant_value": true }, diff --git a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json index 9cf8f66d5c51d..0b0e51e5286a4 100644 --- a/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json +++ b/provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.3.4", + "terraform_version": "1.3.3", "values": { "root_module": { "resources": [ @@ -17,11 +17,11 @@ "connection_timeout": 120, "dir": null, "env": null, - "id": "3aeed2cf-2a5a-40f7-a0d6-2c3508f601a4", + "id": "7766b2a9-c00f-4cde-9acc-1fc05651dbdf", "init_script": "", "os": "linux", "startup_script": null, - "token": "c22c9b1f-b077-4ed3-afa6-e10fc5485399", + "token": "5e54c173-a813-4df0-b87d-0617082769dc", "troubleshooting_url": null }, "sensitive_values": {} @@ -34,9 +34,10 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { + "daily_cost": 29, "hide": true, "icon": "/icon/server.svg", - "id": "1f43c366-e7a6-49dc-ac19-894fd9fceac8", + "id": "e43f1cd6-5dbb-4d6b-8942-37f914b37be5", "item": [ { "is_null": false, diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 7a42fd40feb08..ee6f8f9e01cd2 100644 --- a/provisionerd/proto/provisionerd.pb.go +++ b/provisionerd/proto/provisionerd.pb.go @@ -667,6 +667,124 @@ func (x *UpdateJobResponse) GetParameterValues() []*proto.ParameterValue { return nil } +type CommitQuotaRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + DailyCost int32 `protobuf:"varint,2,opt,name=daily_cost,json=dailyCost,proto3" json:"daily_cost,omitempty"` +} + +func (x *CommitQuotaRequest) Reset() { + *x = CommitQuotaRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CommitQuotaRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitQuotaRequest) ProtoMessage() {} + +func (x *CommitQuotaRequest) ProtoReflect() protoreflect.Message { + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitQuotaRequest.ProtoReflect.Descriptor instead. +func (*CommitQuotaRequest) Descriptor() ([]byte, []int) { + return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{7} +} + +func (x *CommitQuotaRequest) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +func (x *CommitQuotaRequest) GetDailyCost() int32 { + if x != nil { + return x.DailyCost + } + return 0 +} + +type CommitQuotaResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + CreditsConsumed int32 `protobuf:"varint,2,opt,name=credits_consumed,json=creditsConsumed,proto3" json:"credits_consumed,omitempty"` + Budget int32 `protobuf:"varint,3,opt,name=budget,proto3" json:"budget,omitempty"` +} + +func (x *CommitQuotaResponse) Reset() { + *x = CommitQuotaResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CommitQuotaResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommitQuotaResponse) ProtoMessage() {} + +func (x *CommitQuotaResponse) ProtoReflect() protoreflect.Message { + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommitQuotaResponse.ProtoReflect.Descriptor instead. +func (*CommitQuotaResponse) Descriptor() ([]byte, []int) { + return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{8} +} + +func (x *CommitQuotaResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *CommitQuotaResponse) GetCreditsConsumed() int32 { + if x != nil { + return x.CreditsConsumed + } + return 0 +} + +func (x *CommitQuotaResponse) GetBudget() int32 { + if x != nil { + return x.Budget + } + return 0 +} + type AcquiredJob_WorkspaceBuild struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -682,7 +800,7 @@ type AcquiredJob_WorkspaceBuild struct { func (x *AcquiredJob_WorkspaceBuild) Reset() { *x = AcquiredJob_WorkspaceBuild{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[7] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -695,7 +813,7 @@ func (x *AcquiredJob_WorkspaceBuild) String() string { func (*AcquiredJob_WorkspaceBuild) ProtoMessage() {} func (x *AcquiredJob_WorkspaceBuild) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[7] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -757,7 +875,7 @@ type AcquiredJob_TemplateImport struct { func (x *AcquiredJob_TemplateImport) Reset() { *x = AcquiredJob_TemplateImport{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[8] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -770,7 +888,7 @@ func (x *AcquiredJob_TemplateImport) String() string { func (*AcquiredJob_TemplateImport) ProtoMessage() {} func (x *AcquiredJob_TemplateImport) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[8] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -805,7 +923,7 @@ type AcquiredJob_TemplateDryRun struct { func (x *AcquiredJob_TemplateDryRun) Reset() { *x = AcquiredJob_TemplateDryRun{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[9] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -818,7 +936,7 @@ func (x *AcquiredJob_TemplateDryRun) String() string { func (*AcquiredJob_TemplateDryRun) ProtoMessage() {} func (x *AcquiredJob_TemplateDryRun) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[9] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -859,7 +977,7 @@ type FailedJob_WorkspaceBuild struct { func (x *FailedJob_WorkspaceBuild) Reset() { *x = FailedJob_WorkspaceBuild{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[10] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -872,7 +990,7 @@ func (x *FailedJob_WorkspaceBuild) String() string { func (*FailedJob_WorkspaceBuild) ProtoMessage() {} func (x *FailedJob_WorkspaceBuild) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[10] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -904,7 +1022,7 @@ type FailedJob_TemplateImport struct { func (x *FailedJob_TemplateImport) Reset() { *x = FailedJob_TemplateImport{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[11] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -917,7 +1035,7 @@ func (x *FailedJob_TemplateImport) String() string { func (*FailedJob_TemplateImport) ProtoMessage() {} func (x *FailedJob_TemplateImport) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[11] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -942,7 +1060,7 @@ type FailedJob_TemplateDryRun struct { func (x *FailedJob_TemplateDryRun) Reset() { *x = FailedJob_TemplateDryRun{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[12] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -955,7 +1073,7 @@ func (x *FailedJob_TemplateDryRun) String() string { func (*FailedJob_TemplateDryRun) ProtoMessage() {} func (x *FailedJob_TemplateDryRun) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[12] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -983,7 +1101,7 @@ type CompletedJob_WorkspaceBuild struct { func (x *CompletedJob_WorkspaceBuild) Reset() { *x = CompletedJob_WorkspaceBuild{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[13] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -996,7 +1114,7 @@ func (x *CompletedJob_WorkspaceBuild) String() string { func (*CompletedJob_WorkspaceBuild) ProtoMessage() {} func (x *CompletedJob_WorkspaceBuild) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[13] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1038,7 +1156,7 @@ type CompletedJob_TemplateImport struct { func (x *CompletedJob_TemplateImport) Reset() { *x = CompletedJob_TemplateImport{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[14] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1051,7 +1169,7 @@ func (x *CompletedJob_TemplateImport) String() string { func (*CompletedJob_TemplateImport) ProtoMessage() {} func (x *CompletedJob_TemplateImport) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[14] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1092,7 +1210,7 @@ type CompletedJob_TemplateDryRun struct { func (x *CompletedJob_TemplateDryRun) Reset() { *x = CompletedJob_TemplateDryRun{} if protoimpl.UnsafeEnabled { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[15] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1105,7 +1223,7 @@ func (x *CompletedJob_TemplateDryRun) String() string { func (*CompletedJob_TemplateDryRun) ProtoMessage() {} func (x *CompletedJob_TemplateDryRun) ProtoReflect() protoreflect.Message { - mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[15] + mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1289,31 +1407,48 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2a, 0x34, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, - 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, - 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x32, 0x98, - 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x44, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, - 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, - 0x6f, 0x62, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, - 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x17, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, - 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, + 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, + 0x6f, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, + 0x43, 0x6f, 0x73, 0x74, 0x22, 0x68, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, + 0x6f, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, + 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, + 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, + 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x2a, 0x34, + 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, + 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, + 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, + 0x45, 0x52, 0x10, 0x01, 0x32, 0xec, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, + 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, + 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, + 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, + 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, + 0x69, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, + 0x6f, 0x62, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1329,7 +1464,7 @@ func file_provisionerd_proto_provisionerd_proto_rawDescGZIP() []byte { } var file_provisionerd_proto_provisionerd_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_provisionerd_proto_provisionerd_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_provisionerd_proto_provisionerd_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_provisionerd_proto_provisionerd_proto_goTypes = []interface{}{ (LogSource)(0), // 0: provisionerd.LogSource (*Empty)(nil), // 1: provisionerd.Empty @@ -1339,55 +1474,59 @@ var file_provisionerd_proto_provisionerd_proto_goTypes = []interface{}{ (*Log)(nil), // 5: provisionerd.Log (*UpdateJobRequest)(nil), // 6: provisionerd.UpdateJobRequest (*UpdateJobResponse)(nil), // 7: provisionerd.UpdateJobResponse - (*AcquiredJob_WorkspaceBuild)(nil), // 8: provisionerd.AcquiredJob.WorkspaceBuild - (*AcquiredJob_TemplateImport)(nil), // 9: provisionerd.AcquiredJob.TemplateImport - (*AcquiredJob_TemplateDryRun)(nil), // 10: provisionerd.AcquiredJob.TemplateDryRun - (*FailedJob_WorkspaceBuild)(nil), // 11: provisionerd.FailedJob.WorkspaceBuild - (*FailedJob_TemplateImport)(nil), // 12: provisionerd.FailedJob.TemplateImport - (*FailedJob_TemplateDryRun)(nil), // 13: provisionerd.FailedJob.TemplateDryRun - (*CompletedJob_WorkspaceBuild)(nil), // 14: provisionerd.CompletedJob.WorkspaceBuild - (*CompletedJob_TemplateImport)(nil), // 15: provisionerd.CompletedJob.TemplateImport - (*CompletedJob_TemplateDryRun)(nil), // 16: provisionerd.CompletedJob.TemplateDryRun - (proto.LogLevel)(0), // 17: provisioner.LogLevel - (*proto.ParameterSchema)(nil), // 18: provisioner.ParameterSchema - (*proto.ParameterValue)(nil), // 19: provisioner.ParameterValue - (*proto.Provision_Metadata)(nil), // 20: provisioner.Provision.Metadata - (*proto.Resource)(nil), // 21: provisioner.Resource + (*CommitQuotaRequest)(nil), // 8: provisionerd.CommitQuotaRequest + (*CommitQuotaResponse)(nil), // 9: provisionerd.CommitQuotaResponse + (*AcquiredJob_WorkspaceBuild)(nil), // 10: provisionerd.AcquiredJob.WorkspaceBuild + (*AcquiredJob_TemplateImport)(nil), // 11: provisionerd.AcquiredJob.TemplateImport + (*AcquiredJob_TemplateDryRun)(nil), // 12: provisionerd.AcquiredJob.TemplateDryRun + (*FailedJob_WorkspaceBuild)(nil), // 13: provisionerd.FailedJob.WorkspaceBuild + (*FailedJob_TemplateImport)(nil), // 14: provisionerd.FailedJob.TemplateImport + (*FailedJob_TemplateDryRun)(nil), // 15: provisionerd.FailedJob.TemplateDryRun + (*CompletedJob_WorkspaceBuild)(nil), // 16: provisionerd.CompletedJob.WorkspaceBuild + (*CompletedJob_TemplateImport)(nil), // 17: provisionerd.CompletedJob.TemplateImport + (*CompletedJob_TemplateDryRun)(nil), // 18: provisionerd.CompletedJob.TemplateDryRun + (proto.LogLevel)(0), // 19: provisioner.LogLevel + (*proto.ParameterSchema)(nil), // 20: provisioner.ParameterSchema + (*proto.ParameterValue)(nil), // 21: provisioner.ParameterValue + (*proto.Provision_Metadata)(nil), // 22: provisioner.Provision.Metadata + (*proto.Resource)(nil), // 23: provisioner.Resource } var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{ - 8, // 0: provisionerd.AcquiredJob.workspace_build:type_name -> provisionerd.AcquiredJob.WorkspaceBuild - 9, // 1: provisionerd.AcquiredJob.template_import:type_name -> provisionerd.AcquiredJob.TemplateImport - 10, // 2: provisionerd.AcquiredJob.template_dry_run:type_name -> provisionerd.AcquiredJob.TemplateDryRun - 11, // 3: provisionerd.FailedJob.workspace_build:type_name -> provisionerd.FailedJob.WorkspaceBuild - 12, // 4: provisionerd.FailedJob.template_import:type_name -> provisionerd.FailedJob.TemplateImport - 13, // 5: provisionerd.FailedJob.template_dry_run:type_name -> provisionerd.FailedJob.TemplateDryRun - 14, // 6: provisionerd.CompletedJob.workspace_build:type_name -> provisionerd.CompletedJob.WorkspaceBuild - 15, // 7: provisionerd.CompletedJob.template_import:type_name -> provisionerd.CompletedJob.TemplateImport - 16, // 8: provisionerd.CompletedJob.template_dry_run:type_name -> provisionerd.CompletedJob.TemplateDryRun + 10, // 0: provisionerd.AcquiredJob.workspace_build:type_name -> provisionerd.AcquiredJob.WorkspaceBuild + 11, // 1: provisionerd.AcquiredJob.template_import:type_name -> provisionerd.AcquiredJob.TemplateImport + 12, // 2: provisionerd.AcquiredJob.template_dry_run:type_name -> provisionerd.AcquiredJob.TemplateDryRun + 13, // 3: provisionerd.FailedJob.workspace_build:type_name -> provisionerd.FailedJob.WorkspaceBuild + 14, // 4: provisionerd.FailedJob.template_import:type_name -> provisionerd.FailedJob.TemplateImport + 15, // 5: provisionerd.FailedJob.template_dry_run:type_name -> provisionerd.FailedJob.TemplateDryRun + 16, // 6: provisionerd.CompletedJob.workspace_build:type_name -> provisionerd.CompletedJob.WorkspaceBuild + 17, // 7: provisionerd.CompletedJob.template_import:type_name -> provisionerd.CompletedJob.TemplateImport + 18, // 8: provisionerd.CompletedJob.template_dry_run:type_name -> provisionerd.CompletedJob.TemplateDryRun 0, // 9: provisionerd.Log.source:type_name -> provisionerd.LogSource - 17, // 10: provisionerd.Log.level:type_name -> provisioner.LogLevel + 19, // 10: provisionerd.Log.level:type_name -> provisioner.LogLevel 5, // 11: provisionerd.UpdateJobRequest.logs:type_name -> provisionerd.Log - 18, // 12: provisionerd.UpdateJobRequest.parameter_schemas:type_name -> provisioner.ParameterSchema - 19, // 13: provisionerd.UpdateJobResponse.parameter_values:type_name -> provisioner.ParameterValue - 19, // 14: provisionerd.AcquiredJob.WorkspaceBuild.parameter_values:type_name -> provisioner.ParameterValue - 20, // 15: provisionerd.AcquiredJob.WorkspaceBuild.metadata:type_name -> provisioner.Provision.Metadata - 20, // 16: provisionerd.AcquiredJob.TemplateImport.metadata:type_name -> provisioner.Provision.Metadata - 19, // 17: provisionerd.AcquiredJob.TemplateDryRun.parameter_values:type_name -> provisioner.ParameterValue - 20, // 18: provisionerd.AcquiredJob.TemplateDryRun.metadata:type_name -> provisioner.Provision.Metadata - 21, // 19: provisionerd.CompletedJob.WorkspaceBuild.resources:type_name -> provisioner.Resource - 21, // 20: provisionerd.CompletedJob.TemplateImport.start_resources:type_name -> provisioner.Resource - 21, // 21: provisionerd.CompletedJob.TemplateImport.stop_resources:type_name -> provisioner.Resource - 21, // 22: provisionerd.CompletedJob.TemplateDryRun.resources:type_name -> provisioner.Resource + 20, // 12: provisionerd.UpdateJobRequest.parameter_schemas:type_name -> provisioner.ParameterSchema + 21, // 13: provisionerd.UpdateJobResponse.parameter_values:type_name -> provisioner.ParameterValue + 21, // 14: provisionerd.AcquiredJob.WorkspaceBuild.parameter_values:type_name -> provisioner.ParameterValue + 22, // 15: provisionerd.AcquiredJob.WorkspaceBuild.metadata:type_name -> provisioner.Provision.Metadata + 22, // 16: provisionerd.AcquiredJob.TemplateImport.metadata:type_name -> provisioner.Provision.Metadata + 21, // 17: provisionerd.AcquiredJob.TemplateDryRun.parameter_values:type_name -> provisioner.ParameterValue + 22, // 18: provisionerd.AcquiredJob.TemplateDryRun.metadata:type_name -> provisioner.Provision.Metadata + 23, // 19: provisionerd.CompletedJob.WorkspaceBuild.resources:type_name -> provisioner.Resource + 23, // 20: provisionerd.CompletedJob.TemplateImport.start_resources:type_name -> provisioner.Resource + 23, // 21: provisionerd.CompletedJob.TemplateImport.stop_resources:type_name -> provisioner.Resource + 23, // 22: provisionerd.CompletedJob.TemplateDryRun.resources:type_name -> provisioner.Resource 1, // 23: provisionerd.ProvisionerDaemon.AcquireJob:input_type -> provisionerd.Empty - 6, // 24: provisionerd.ProvisionerDaemon.UpdateJob:input_type -> provisionerd.UpdateJobRequest - 3, // 25: provisionerd.ProvisionerDaemon.FailJob:input_type -> provisionerd.FailedJob - 4, // 26: provisionerd.ProvisionerDaemon.CompleteJob:input_type -> provisionerd.CompletedJob - 2, // 27: provisionerd.ProvisionerDaemon.AcquireJob:output_type -> provisionerd.AcquiredJob - 7, // 28: provisionerd.ProvisionerDaemon.UpdateJob:output_type -> provisionerd.UpdateJobResponse - 1, // 29: provisionerd.ProvisionerDaemon.FailJob:output_type -> provisionerd.Empty - 1, // 30: provisionerd.ProvisionerDaemon.CompleteJob:output_type -> provisionerd.Empty - 27, // [27:31] is the sub-list for method output_type - 23, // [23:27] is the sub-list for method input_type + 8, // 24: provisionerd.ProvisionerDaemon.CommitQuota:input_type -> provisionerd.CommitQuotaRequest + 6, // 25: provisionerd.ProvisionerDaemon.UpdateJob:input_type -> provisionerd.UpdateJobRequest + 3, // 26: provisionerd.ProvisionerDaemon.FailJob:input_type -> provisionerd.FailedJob + 4, // 27: provisionerd.ProvisionerDaemon.CompleteJob:input_type -> provisionerd.CompletedJob + 2, // 28: provisionerd.ProvisionerDaemon.AcquireJob:output_type -> provisionerd.AcquiredJob + 9, // 29: provisionerd.ProvisionerDaemon.CommitQuota:output_type -> provisionerd.CommitQuotaResponse + 7, // 30: provisionerd.ProvisionerDaemon.UpdateJob:output_type -> provisionerd.UpdateJobResponse + 1, // 31: provisionerd.ProvisionerDaemon.FailJob:output_type -> provisionerd.Empty + 1, // 32: provisionerd.ProvisionerDaemon.CompleteJob:output_type -> provisionerd.Empty + 28, // [28:33] is the sub-list for method output_type + 23, // [23:28] is the sub-list for method input_type 23, // [23:23] is the sub-list for extension type_name 23, // [23:23] is the sub-list for extension extendee 0, // [0:23] is the sub-list for field type_name @@ -1484,7 +1623,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AcquiredJob_WorkspaceBuild); i { + switch v := v.(*CommitQuotaRequest); i { case 0: return &v.state case 1: @@ -1496,7 +1635,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AcquiredJob_TemplateImport); i { + switch v := v.(*CommitQuotaResponse); i { case 0: return &v.state case 1: @@ -1508,7 +1647,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AcquiredJob_TemplateDryRun); i { + switch v := v.(*AcquiredJob_WorkspaceBuild); i { case 0: return &v.state case 1: @@ -1520,7 +1659,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FailedJob_WorkspaceBuild); i { + switch v := v.(*AcquiredJob_TemplateImport); i { case 0: return &v.state case 1: @@ -1532,7 +1671,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FailedJob_TemplateImport); i { + switch v := v.(*AcquiredJob_TemplateDryRun); i { case 0: return &v.state case 1: @@ -1544,7 +1683,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FailedJob_TemplateDryRun); i { + switch v := v.(*FailedJob_WorkspaceBuild); i { case 0: return &v.state case 1: @@ -1556,7 +1695,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CompletedJob_WorkspaceBuild); i { + switch v := v.(*FailedJob_TemplateImport); i { case 0: return &v.state case 1: @@ -1568,7 +1707,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CompletedJob_TemplateImport); i { + switch v := v.(*FailedJob_TemplateDryRun); i { case 0: return &v.state case 1: @@ -1580,6 +1719,30 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CompletedJob_WorkspaceBuild); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionerd_proto_provisionerd_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CompletedJob_TemplateImport); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionerd_proto_provisionerd_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CompletedJob_TemplateDryRun); i { case 0: return &v.state @@ -1613,7 +1776,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionerd_proto_provisionerd_proto_rawDesc, NumEnums: 1, - NumMessages: 16, + NumMessages: 18, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionerd/proto/provisionerd.proto b/provisionerd/proto/provisionerd.proto index 57f768ad68ada..6f9fbb53db5da 100644 --- a/provisionerd/proto/provisionerd.proto +++ b/provisionerd/proto/provisionerd.proto @@ -106,12 +106,25 @@ message UpdateJobResponse { repeated provisioner.ParameterValue parameter_values = 2; } +message CommitQuotaRequest { + string job_id = 1; + int32 daily_cost = 2; +} + +message CommitQuotaResponse { + bool ok = 1; + int32 credits_consumed = 2; + int32 budget = 3; +} + service ProvisionerDaemon { // AcquireJob requests a job. Implementations should // hold a lock on the job until CompleteJob() is // called with the matching ID. rpc AcquireJob(Empty) returns (AcquiredJob); + rpc CommitQuota(CommitQuotaRequest) returns (CommitQuotaResponse); + // UpdateJob streams periodic updates for a job. // Implementations should buffer logs so this stream // is non-blocking. diff --git a/provisionerd/proto/provisionerd_drpc.pb.go b/provisionerd/proto/provisionerd_drpc.pb.go index 646f855eabc70..6d73176475490 100644 --- a/provisionerd/proto/provisionerd_drpc.pb.go +++ b/provisionerd/proto/provisionerd_drpc.pb.go @@ -39,6 +39,7 @@ type DRPCProvisionerDaemonClient interface { DRPCConn() drpc.Conn AcquireJob(ctx context.Context, in *Empty) (*AcquiredJob, error) + CommitQuota(ctx context.Context, in *CommitQuotaRequest) (*CommitQuotaResponse, error) UpdateJob(ctx context.Context, in *UpdateJobRequest) (*UpdateJobResponse, error) FailJob(ctx context.Context, in *FailedJob) (*Empty, error) CompleteJob(ctx context.Context, in *CompletedJob) (*Empty, error) @@ -63,6 +64,15 @@ func (c *drpcProvisionerDaemonClient) AcquireJob(ctx context.Context, in *Empty) return out, nil } +func (c *drpcProvisionerDaemonClient) CommitQuota(ctx context.Context, in *CommitQuotaRequest) (*CommitQuotaResponse, error) { + out := new(CommitQuotaResponse) + err := c.cc.Invoke(ctx, "/provisionerd.ProvisionerDaemon/CommitQuota", drpcEncoding_File_provisionerd_proto_provisionerd_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + func (c *drpcProvisionerDaemonClient) UpdateJob(ctx context.Context, in *UpdateJobRequest) (*UpdateJobResponse, error) { out := new(UpdateJobResponse) err := c.cc.Invoke(ctx, "/provisionerd.ProvisionerDaemon/UpdateJob", drpcEncoding_File_provisionerd_proto_provisionerd_proto{}, in, out) @@ -92,6 +102,7 @@ func (c *drpcProvisionerDaemonClient) CompleteJob(ctx context.Context, in *Compl type DRPCProvisionerDaemonServer interface { AcquireJob(context.Context, *Empty) (*AcquiredJob, error) + CommitQuota(context.Context, *CommitQuotaRequest) (*CommitQuotaResponse, error) UpdateJob(context.Context, *UpdateJobRequest) (*UpdateJobResponse, error) FailJob(context.Context, *FailedJob) (*Empty, error) CompleteJob(context.Context, *CompletedJob) (*Empty, error) @@ -103,6 +114,10 @@ func (s *DRPCProvisionerDaemonUnimplementedServer) AcquireJob(context.Context, * return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCProvisionerDaemonUnimplementedServer) CommitQuota(context.Context, *CommitQuotaRequest) (*CommitQuotaResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + func (s *DRPCProvisionerDaemonUnimplementedServer) UpdateJob(context.Context, *UpdateJobRequest) (*UpdateJobResponse, error) { return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } @@ -117,7 +132,7 @@ func (s *DRPCProvisionerDaemonUnimplementedServer) CompleteJob(context.Context, type DRPCProvisionerDaemonDescription struct{} -func (DRPCProvisionerDaemonDescription) NumMethods() int { return 4 } +func (DRPCProvisionerDaemonDescription) NumMethods() int { return 5 } func (DRPCProvisionerDaemonDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -131,6 +146,15 @@ func (DRPCProvisionerDaemonDescription) Method(n int) (string, drpc.Encoding, dr ) }, DRPCProvisionerDaemonServer.AcquireJob, true case 1: + return "/provisionerd.ProvisionerDaemon/CommitQuota", drpcEncoding_File_provisionerd_proto_provisionerd_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCProvisionerDaemonServer). + CommitQuota( + ctx, + in1.(*CommitQuotaRequest), + ) + }, DRPCProvisionerDaemonServer.CommitQuota, true + case 2: return "/provisionerd.ProvisionerDaemon/UpdateJob", drpcEncoding_File_provisionerd_proto_provisionerd_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCProvisionerDaemonServer). @@ -139,7 +163,7 @@ func (DRPCProvisionerDaemonDescription) Method(n int) (string, drpc.Encoding, dr in1.(*UpdateJobRequest), ) }, DRPCProvisionerDaemonServer.UpdateJob, true - case 2: + case 3: return "/provisionerd.ProvisionerDaemon/FailJob", drpcEncoding_File_provisionerd_proto_provisionerd_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCProvisionerDaemonServer). @@ -148,7 +172,7 @@ func (DRPCProvisionerDaemonDescription) Method(n int) (string, drpc.Encoding, dr in1.(*FailedJob), ) }, DRPCProvisionerDaemonServer.FailJob, true - case 3: + case 4: return "/provisionerd.ProvisionerDaemon/CompleteJob", drpcEncoding_File_provisionerd_proto_provisionerd_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCProvisionerDaemonServer). @@ -182,6 +206,22 @@ func (x *drpcProvisionerDaemon_AcquireJobStream) SendAndClose(m *AcquiredJob) er return x.CloseSend() } +type DRPCProvisionerDaemon_CommitQuotaStream interface { + drpc.Stream + SendAndClose(*CommitQuotaResponse) error +} + +type drpcProvisionerDaemon_CommitQuotaStream struct { + drpc.Stream +} + +func (x *drpcProvisionerDaemon_CommitQuotaStream) SendAndClose(m *CommitQuotaResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_provisionerd_proto_provisionerd_proto{}); err != nil { + return err + } + return x.CloseSend() +} + type DRPCProvisionerDaemon_UpdateJobStream interface { drpc.Stream SendAndClose(*UpdateJobResponse) error diff --git a/provisionerd/proto/quota.go b/provisionerd/proto/quota.go new file mode 100644 index 0000000000000..75d1844269caf --- /dev/null +++ b/provisionerd/proto/quota.go @@ -0,0 +1,7 @@ +package proto + +import context "context" + +type QuotaCommitter interface { + CommitQuota(ctx context.Context, request *CommitQuotaRequest) (*CommitQuotaResponse, error) +} diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index 6e0227696437d..ce16ff2709172 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -324,6 +324,7 @@ func (p *Server) acquireJob(ctx context.Context) { job, runner.Options{ Updater: p, + QuotaCommitter: p, Logger: p.opts.Logger, Filesystem: p.opts.Filesystem, WorkDirectory: p.opts.WorkDirectory, @@ -365,6 +366,17 @@ func (p *Server) clientDoWithRetries( return nil, ctx.Err() } +func (p *Server) CommitQuota(ctx context.Context, in *proto.CommitQuotaRequest) (*proto.CommitQuotaResponse, error) { + out, err := p.clientDoWithRetries(ctx, func(ctx context.Context, client proto.DRPCProvisionerDaemonClient) (any, error) { + return client.CommitQuota(ctx, in) + }) + if err != nil { + return nil, err + } + // nolint: forcetypeassert + return out.(*proto.CommitQuotaResponse), nil +} + func (p *Server) UpdateJob(ctx context.Context, in *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) { out, err := p.clientDoWithRetries(ctx, func(ctx context.Context, client proto.DRPCProvisionerDaemonClient) (any, error) { return client.UpdateJob(ctx, in) diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index 0a4e87b81f665..d49b2f20c5c89 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -481,6 +481,97 @@ func TestProvisionerd(t *testing.T) { require.NoError(t, closer.Close()) }) + t.Run("WorkspaceBuildQuotaExceeded", func(t *testing.T) { + t.Parallel() + var ( + didComplete atomic.Bool + didLog atomic.Bool + didAcquireJob atomic.Bool + didFail atomic.Bool + completeChan = make(chan struct{}) + completeOnce sync.Once + ) + + closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { + return createProvisionerDaemonClient(t, provisionerDaemonTestServer{ + acquireJob: func(ctx context.Context, _ *proto.Empty) (*proto.AcquiredJob, error) { + if !didAcquireJob.CAS(false, true) { + completeOnce.Do(func() { close(completeChan) }) + return &proto.AcquiredJob{}, nil + } + + return &proto.AcquiredJob{ + JobId: "test", + Provisioner: "someprovisioner", + TemplateSourceArchive: createTar(t, map[string]string{ + "test.txt": "content", + }), + Type: &proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ + Metadata: &sdkproto.Provision_Metadata{}, + }, + }, + }, nil + }, + updateJob: func(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) { + if len(update.Logs) != 0 { + didLog.Store(true) + } + return &proto.UpdateJobResponse{}, nil + }, + completeJob: func(ctx context.Context, job *proto.CompletedJob) (*proto.Empty, error) { + didComplete.Store(true) + return &proto.Empty{}, nil + }, + commitQuota: func(ctx context.Context, com *proto.CommitQuotaRequest) (*proto.CommitQuotaResponse, error) { + return &proto.CommitQuotaResponse{ + Ok: com.DailyCost < 20, + }, nil + }, + failJob: func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) { + didFail.Store(true) + return &proto.Empty{}, nil + }, + }), nil + }, provisionerd.Provisioners{ + "someprovisioner": createProvisionerClient(t, provisionerTestServer{ + provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error { + err := stream.Send(&sdkproto.Provision_Response{ + Type: &sdkproto.Provision_Response_Log{ + Log: &sdkproto.Log{ + Level: sdkproto.LogLevel_DEBUG, + Output: "wow", + }, + }, + }) + require.NoError(t, err) + + err = stream.Send(&sdkproto.Provision_Response{ + Type: &sdkproto.Provision_Response_Complete{ + Complete: &sdkproto.Provision_Complete{ + Resources: []*sdkproto.Resource{ + { + DailyCost: 10, + }, + { + DailyCost: 15, + }, + }, + }, + }, + }) + require.NoError(t, err) + return nil + }, + }), + }) + require.Condition(t, closedWithin(completeChan, testutil.WaitShort)) + require.True(t, didLog.Load()) + require.True(t, didFail.Load()) + require.False(t, didComplete.Load()) + require.NoError(t, closer.Close()) + }) + t.Run("WorkspaceBuildFailComplete", func(t *testing.T) { t.Parallel() var ( @@ -1039,6 +1130,7 @@ func (p *provisionerTestServer) Provision(stream sdkproto.DRPCProvisioner_Provis // passable functions for dynamic functionality. type provisionerDaemonTestServer struct { acquireJob func(ctx context.Context, _ *proto.Empty) (*proto.AcquiredJob, error) + commitQuota func(ctx context.Context, com *proto.CommitQuotaRequest) (*proto.CommitQuotaResponse, error) updateJob func(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) failJob func(ctx context.Context, job *proto.FailedJob) (*proto.Empty, error) completeJob func(ctx context.Context, job *proto.CompletedJob) (*proto.Empty, error) @@ -1047,6 +1139,14 @@ type provisionerDaemonTestServer struct { func (p *provisionerDaemonTestServer) AcquireJob(ctx context.Context, empty *proto.Empty) (*proto.AcquiredJob, error) { return p.acquireJob(ctx, empty) } +func (p *provisionerDaemonTestServer) CommitQuota(ctx context.Context, com *proto.CommitQuotaRequest) (*proto.CommitQuotaResponse, error) { + if p.commitQuota == nil { + return &proto.CommitQuotaResponse{ + Ok: true, + }, nil + } + return p.commitQuota(ctx, com) +} func (p *provisionerDaemonTestServer) UpdateJob(ctx context.Context, update *proto.UpdateJobRequest) (*proto.UpdateJobResponse, error) { return p.updateJob(ctx, update) diff --git a/provisionerd/runner/quota.go b/provisionerd/runner/quota.go new file mode 100644 index 0000000000000..d773721713e6c --- /dev/null +++ b/provisionerd/runner/quota.go @@ -0,0 +1,11 @@ +package runner + +import "github.com/coder/coder/provisionersdk/proto" + +func sumDailyCost(resources []*proto.Resource) int { + var sum int + for _, r := range resources { + sum += int(r.DailyCost) + } + return sum +} diff --git a/provisionerd/runner/runner.go b/provisionerd/runner/runner.go index 81bb54e3fa894..927b57fbec72f 100644 --- a/provisionerd/runner/runner.go +++ b/provisionerd/runner/runner.go @@ -42,6 +42,7 @@ type Runner struct { metrics Metrics job *proto.AcquiredJob sender JobUpdater + quotaCommitter QuotaCommitter logger slog.Logger filesystem afero.Fs workDirectory string @@ -85,9 +86,13 @@ type JobUpdater interface { FailJob(ctx context.Context, in *proto.FailedJob) error CompleteJob(ctx context.Context, in *proto.CompletedJob) error } +type QuotaCommitter interface { + CommitQuota(ctx context.Context, in *proto.CommitQuotaRequest) (*proto.CommitQuotaResponse, error) +} type Options struct { Updater JobUpdater + QuotaCommitter QuotaCommitter Logger slog.Logger Filesystem afero.Fs WorkDirectory string @@ -115,6 +120,7 @@ func New( metrics: opts.Metrics, job: job, sender: opts.Updater, + quotaCommitter: opts.QuotaCommitter, logger: opts.Logger.With(slog.F("job_id", job.JobId)), filesystem: opts.Filesystem, workDirectory: opts.WorkDirectory, @@ -843,7 +849,7 @@ func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto } r.logger.Debug(context.Background(), "provision complete no error") - r.logger.Info(context.Background(), "provision successful; marked job as complete", + r.logger.Info(context.Background(), "provision successful", slog.F("resource_count", len(msgType.Complete.Resources)), slog.F("resources", msgType.Complete.Resources), slog.F("state_length", len(msgType.Complete.State)), @@ -856,35 +862,81 @@ func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto } } +func (r *Runner) commitQuota(ctx context.Context, resources []*sdkproto.Resource) *proto.FailedJob { + cost := sumDailyCost(resources) + if cost == 0 { + return nil + } + + const stage = "Commit quota" + + resp, err := r.quotaCommitter.CommitQuota(ctx, &proto.CommitQuotaRequest{ + JobId: r.job.JobId, + DailyCost: int32(cost), + }) + if err != nil { + r.queueLog(ctx, &proto.Log{ + Source: proto.LogSource_PROVISIONER, + Level: sdkproto.LogLevel_ERROR, + CreatedAt: time.Now().UnixMilli(), + Output: fmt.Sprintf("Failed to commit quota: %+v", err), + Stage: stage, + }) + return r.failedJobf("commit quota: %+v", err) + } + for _, line := range []string{ + fmt.Sprintf("Build cost — %v", cost), + fmt.Sprintf("Budget — %v", resp.Budget), + fmt.Sprintf("Credits consumed — %v", resp.CreditsConsumed), + } { + r.queueLog(ctx, &proto.Log{ + Source: proto.LogSource_PROVISIONER, + Level: sdkproto.LogLevel_INFO, + CreatedAt: time.Now().UnixMilli(), + Output: line, + Stage: stage, + }) + } + + if !resp.Ok { + r.queueLog(ctx, &proto.Log{ + Source: proto.LogSource_PROVISIONER, + Level: sdkproto.LogLevel_WARN, + CreatedAt: time.Now().UnixMilli(), + Output: "This build would exceed your quota. Failing.", + Stage: stage, + }) + return r.failedJobf("insufficient quota") + } + return nil +} + func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) { ctx, span := r.startTrace(ctx, tracing.FuncName()) defer span.End() var ( - applyStage string + applyStage string + commitQuota bool ) switch r.job.GetWorkspaceBuild().Metadata.WorkspaceTransition { case sdkproto.WorkspaceTransition_START: applyStage = "Starting workspace" + commitQuota = true case sdkproto.WorkspaceTransition_STOP: applyStage = "Stopping workspace" + commitQuota = true case sdkproto.WorkspaceTransition_DESTROY: applyStage = "Destroying workspace" } - r.queueLog(ctx, &proto.Log{ - Source: proto.LogSource_PROVISIONER_DAEMON, - Level: sdkproto.LogLevel_INFO, - Stage: applyStage, - CreatedAt: time.Now().UnixMilli(), - }) config := &sdkproto.Provision_Config{ Directory: r.workDirectory, Metadata: r.job.GetWorkspaceBuild().Metadata, State: r.job.GetWorkspaceBuild().State, } - completed, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Provision_Request{ + completedPlan, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Provision_Request{ Type: &sdkproto.Provision_Request_Plan{ Plan: &sdkproto.Provision_Plan{ Config: config, @@ -895,18 +947,34 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p if failed != nil { return nil, failed } + r.flushQueuedLogs(ctx) + if commitQuota { + failed = r.commitQuota(ctx, completedPlan.GetResources()) + r.flushQueuedLogs(ctx) + if failed != nil { + return nil, failed + } + } + + r.queueLog(ctx, &proto.Log{ + Source: proto.LogSource_PROVISIONER_DAEMON, + Level: sdkproto.LogLevel_INFO, + Stage: applyStage, + CreatedAt: time.Now().UnixMilli(), + }) completedApply, failed := r.buildWorkspace(ctx, applyStage, &sdkproto.Provision_Request{ Type: &sdkproto.Provision_Request_Apply{ Apply: &sdkproto.Provision_Apply{ Config: config, - Plan: completed.GetPlan(), + Plan: completedPlan.GetPlan(), }, }, }) if failed != nil { return nil, failed } + r.flushQueuedLogs(ctx) return &proto.CompletedJob{ JobId: r.job.JobId, diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index 258ab419557d8..90db4232ba53f 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -1092,7 +1092,7 @@ type Resource struct { Hide bool `protobuf:"varint,5,opt,name=hide,proto3" json:"hide,omitempty"` Icon string `protobuf:"bytes,6,opt,name=icon,proto3" json:"icon,omitempty"` InstanceType string `protobuf:"bytes,7,opt,name=instance_type,json=instanceType,proto3" json:"instance_type,omitempty"` - Cost int32 `protobuf:"varint,8,opt,name=cost,proto3" json:"cost,omitempty"` + DailyCost int32 `protobuf:"varint,8,opt,name=daily_cost,json=dailyCost,proto3" json:"daily_cost,omitempty"` } func (x *Resource) Reset() { @@ -1176,9 +1176,9 @@ func (x *Resource) GetInstanceType() string { return "" } -func (x *Resource) GetCost() int32 { +func (x *Resource) GetDailyCost() int32 { if x != nil { - return x.Cost + return x.DailyCost } return 0 } @@ -2201,7 +2201,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, - 0x68, 0x6f, 0x6c, 0x64, 0x22, 0xe6, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x68, 0x6f, 0x6c, 0x64, 0x22, 0xf1, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, @@ -2215,128 +2215,128 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x04, 0x68, 0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, - 0x73, 0x74, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, - 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, - 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xfc, 0x01, - 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, - 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, - 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xf0, 0x08, 0x0a, - 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xd1, 0x02, 0x0a, 0x08, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, - 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, - 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x1a, 0x79, - 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x85, 0x01, 0x0a, 0x04, 0x50, 0x6c, - 0x61, 0x6e, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, - 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x1a, 0x52, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, - 0xb3, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x04, 0x70, - 0x6c, 0x61, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x1a, 0x69, 0x0a, + 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, + 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xfc, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, + 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, + 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, + 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, + 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xf0, 0x08, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xd1, 0x02, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, + 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, + 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, + 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, + 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x1a, 0x79, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x34, - 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x1a, 0x85, 0x01, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x35, 0x0a, + 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x48, 0x00, 0x52, 0x05, 0x61, - 0x70, 0x70, 0x6c, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, - 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, 0x7f, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, - 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, - 0x6e, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, - 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, - 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, - 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, - 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, - 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, - 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, - 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, - 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, - 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, - 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, - 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, - 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x52, 0x0a, 0x05, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, + 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0xb3, 0x01, 0x0a, 0x07, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x6c, 0x61, + 0x6e, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x34, 0x0a, 0x05, 0x61, 0x70, 0x70, + 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, + 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, + 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x1a, 0x7f, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x33, 0x0a, 0x09, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, + 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, + 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, + 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, + 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, + 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, + 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, + 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, + 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, + 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, + 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, + 0x02, 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 468535402a27f..fd4a55e741f54 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -132,7 +132,7 @@ message Resource { bool hide = 5; string icon = 6; string instance_type = 7; - int32 cost = 8; + int32 daily_cost = 8; } // Parse consumes source-code from a directory to produce inputs. diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 3bc1709d99072..85115131052f2 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -21,6 +21,5 @@ export enum FeatureNames { UserLimit = "user_limit", BrowserOnly = "browser_only", SCIM = "scim", - WorkspaceQuota = "workspace_quota", TemplateRBAC = "template_rbac", } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7e0e10ded46f6..f6b71a0daf608 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -156,6 +156,7 @@ export interface CreateFirstUserResponse { export interface CreateGroupRequest { readonly name: string readonly avatar_url: string + readonly quota_allowance: number } // From codersdk/users.go @@ -301,7 +302,6 @@ export interface DeploymentConfig { readonly audit_logging: DeploymentConfigField readonly browser_only: DeploymentConfigField readonly scim_api_key: DeploymentConfigField - readonly user_workspace_quota: DeploymentConfigField readonly provisioner: ProvisionerConfig readonly api_rate_limit: DeploymentConfigField readonly experimental: DeploymentConfigField @@ -374,6 +374,7 @@ export interface Group { readonly organization_id: string readonly members: User[] readonly avatar_url: string + readonly quota_allowance: number } // From codersdk/workspaceapps.go @@ -502,6 +503,7 @@ export interface PatchGroupRequest { readonly remove_users: string[] readonly name: string readonly avatar_url?: string + readonly quota_allowance?: number } // From codersdk/deploymentconfig.go @@ -881,6 +883,7 @@ export interface WorkspaceBuild { readonly resources: WorkspaceResource[] readonly deadline?: string readonly status: WorkspaceStatus + readonly daily_cost: number } // From codersdk/workspaces.go @@ -899,10 +902,10 @@ export interface WorkspaceOptions { readonly include_deleted?: boolean } -// From codersdk/workspacequota.go +// From codersdk/quota.go export interface WorkspaceQuota { - readonly user_workspace_count: number - readonly user_workspace_limit: number + readonly credits_consumed: number + readonly budget: number } // From codersdk/workspacebuilds.go @@ -917,6 +920,7 @@ export interface WorkspaceResource { readonly icon: string readonly agents?: WorkspaceAgent[] readonly metadata?: WorkspaceResourceMetadata[] + readonly daily_cost: number } // From codersdk/workspacebuilds.go diff --git a/site/src/components/Logs/Logs.tsx b/site/src/components/Logs/Logs.tsx index d5fd60d037c6c..78b667654b51b 100644 --- a/site/src/components/Logs/Logs.tsx +++ b/site/src/components/Logs/Logs.tsx @@ -48,7 +48,8 @@ const useStyles = makeStyles((theme) => ({ overflowX: "auto", }, line: { - whiteSpace: "nowrap", + // Whitespace is significant in terminal output for alignment + whiteSpace: "pre", }, space: { userSelect: "none", diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 273a77c8ba2b6..a846a4cab45d5 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -53,6 +53,16 @@ export const ResourceCard: FC = ({ resource, agentRow }) => {
+ {resource.daily_cost > 0 && ( +
+
+ cost +
+
+ {resource.daily_cost} +
+
+ )} {visibleMetadata.map((meta) => { return (
diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 093c35e7e76c1..9247688be332e 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -55,6 +55,7 @@ export interface WorkspaceProps { buildInfo?: TypesGen.BuildInfoResponse applicationsHost?: string template?: TypesGen.Template + quota_budget?: number } /** @@ -77,6 +78,7 @@ export const Workspace: FC> = ({ buildInfo, applicationsHost, template, + quota_budget, }) => { const { t } = useTranslation("workspacePage") const styles = useStyles() @@ -187,7 +189,11 @@ export const Workspace: FC> = ({ handleClick={() => navigate(`/templates`)} /> - + {isTransitioning !== undefined && isTransitioning && ( = (args) => ( - -) - -export const Example = Template.bind({}) -Example.args = { - quota: { - user_workspace_count: 1, - user_workspace_limit: 3, - }, -} - -export const LimitOf1 = Template.bind({}) -LimitOf1.args = { - quota: { - user_workspace_count: 1, - user_workspace_limit: 1, - }, -} - -export const Loading = Template.bind({}) -Loading.args = { - quota: undefined, -} - -export const Error = Template.bind({}) -Error.args = { - quota: undefined, - error: { - response: { - data: { - message: "Failed to fetch workspace quotas!", - }, - }, - isAxiosError: true, - }, -} - -export const Disabled = Template.bind({}) -Disabled.args = { - quota: { - user_workspace_count: 1, - user_workspace_limit: 0, - }, -} diff --git a/site/src/components/WorkspaceQuota/WorkspaceQuota.tsx b/site/src/components/WorkspaceQuota/WorkspaceQuota.tsx deleted file mode 100644 index 442c262f2ee04..0000000000000 --- a/site/src/components/WorkspaceQuota/WorkspaceQuota.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import Box from "@material-ui/core/Box" -import LinearProgress from "@material-ui/core/LinearProgress" -import { makeStyles } from "@material-ui/core/styles" -import Skeleton from "@material-ui/lab/Skeleton" -import { AlertBanner } from "components/AlertBanner/AlertBanner" -import { Stack } from "components/Stack/Stack" -import { FC } from "react" -import * as TypesGen from "../../api/typesGenerated" - -export const Language = { - of: "of", - workspace: "workspace", - workspaces: "workspaces", -} - -export interface WorkspaceQuotaProps { - quota?: TypesGen.WorkspaceQuota - error: Error | unknown -} - -export const WorkspaceQuota: FC = ({ quota, error }) => { - const styles = useStyles() - - // error state - if (error !== undefined) { - return ( - - - Usage Quota - - - - ) - } - - // loading - if (quota === undefined) { - return ( - - - Usage quota - -
- -
-
-
- ) - } - - // don't show if limit is 0, this means the feature is disabled. - if (quota.user_workspace_limit === 0) { - return null - } - - let value = Math.round( - (quota.user_workspace_count / quota.user_workspace_limit) * 100, - ) - // we don't want to round down to zero if the count is > 0 - if (quota.user_workspace_count > 0 && value === 0) { - value = 1 - } - - return ( - - - - Usage Quota -
- - {quota.user_workspace_count} - {" "} - {Language.of}{" "} - - {quota.user_workspace_limit} - {" "} - {quota.user_workspace_limit === 1 - ? Language.workspace - : Language.workspaces} - {" used"} -
-
- = quota.user_workspace_limit - ? styles.maxProgress - : undefined - } - value={value} - variant="determinate" - /> -
-
- ) -} - -const useStyles = makeStyles((theme) => ({ - stack: { - paddingTop: theme.spacing(2.5), - }, - maxProgress: { - "& .MuiLinearProgress-colorPrimary": { - backgroundColor: theme.palette.error.main, - }, - "& .MuiLinearProgress-barColorPrimary": { - backgroundColor: theme.palette.error.main, - }, - }, - title: { - fontSize: 16, - }, - label: { - fontSize: 14, - display: "block", - color: theme.palette.text.secondary, - }, - labelHighlight: { - color: theme.palette.text.primary, - }, - skeleton: { - minWidth: "150px", - }, -})) diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.tsx index 8fac5bede3b4c..b68bb33431e87 100644 --- a/site/src/components/WorkspaceStats/WorkspaceStats.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceStats.tsx @@ -13,19 +13,22 @@ const Language = { templateLabel: "Template", statusLabel: "Workspace Status", versionLabel: "Version", - lastBuiltLabel: "Last Built", + lastBuiltLabel: "Last built", outdated: "Outdated", upToDate: "Up to date", byLabel: "Last built by", + costLabel: "Daily cost", } export interface WorkspaceStatsProps { workspace: Workspace + quota_budget?: number handleUpdate: () => void } export const WorkspaceStats: FC = ({ workspace, + quota_budget, handleUpdate, }) => { const styles = useStyles() @@ -74,6 +77,14 @@ export const WorkspaceStats: FC = ({ {Language.byLabel}: {initiatedBy}
+ {workspace.latest_build.daily_cost > 0 && ( +
+ {Language.costLabel}: + + {workspace.latest_build.daily_cost} / {quota_budget} + +
+ )}
) } diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index abb522209ba7a..5a6d790ed61aa 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -1,12 +1,10 @@ -import { shallowEqual, useActor, useMachine, useSelector } from "@xstate/react" -import { FeatureNames } from "api/types" +import { useActor, useMachine } from "@xstate/react" import { useOrganizationId } from "hooks/useOrganizationId" import { FC, useContext } from "react" import { Helmet } from "react-helmet-async" import { useNavigate, useParams } from "react-router-dom" import { pageTitle } from "util/page" import { createWorkspaceMachine } from "xServices/createWorkspace/createWorkspaceXService" -import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { XServiceContext } from "xServices/StateContext" import { CreateWorkspaceErrors, @@ -19,19 +17,12 @@ const CreateWorkspacePage: FC = () => { const { template } = useParams() const templateName = template ? template : "" const navigate = useNavigate() - const featureVisibility = useSelector( - xServices.entitlementsXService, - selectFeatureVisibility, - shallowEqual, - ) - const workspaceQuotaEnabled = featureVisibility[FeatureNames.WorkspaceQuota] const [authState] = useActor(xServices.authXService) const { me } = authState.context const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, { context: { organizationId, templateName, - workspaceQuotaEnabled, owner: me ?? null, }, actions: { @@ -49,8 +40,6 @@ const CreateWorkspacePage: FC = () => { getTemplatesError, createWorkspaceError, permissions, - workspaceQuota, - getWorkspaceQuotaError, owner, } = createWorkspaceState.context @@ -70,14 +59,11 @@ const CreateWorkspacePage: FC = () => { templates={templates} selectedTemplate={selectedTemplate} templateSchema={templateSchema} - workspaceQuota={workspaceQuota} createWorkspaceErrors={{ [CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError, [CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: getTemplateSchemaError, [CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: createWorkspaceError, - [CreateWorkspaceErrors.GET_WORKSPACE_QUOTA_ERROR]: - getWorkspaceQuotaError, }} canCreateForUser={permissions?.createWorkspaceForUser} owner={owner} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 3a5a78e3c1ad9..610603f5fb35a 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -4,7 +4,6 @@ import { FormFooter } from "components/FormFooter/FormFooter" import { ParameterInput } from "components/ParameterInput/ParameterInput" import { Stack } from "components/Stack/Stack" import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete" -import { WorkspaceQuota } from "components/WorkspaceQuota/WorkspaceQuota" import { FormikContextType, FormikTouched, useFormik } from "formik" import { i18n } from "i18n" import { FC, useState } from "react" @@ -20,7 +19,6 @@ export enum CreateWorkspaceErrors { GET_TEMPLATES_ERROR = "getTemplatesError", GET_TEMPLATE_SCHEMA_ERROR = "getTemplateSchemaError", CREATE_WORKSPACE_ERROR = "createWorkspaceError", - GET_WORKSPACE_QUOTA_ERROR = "getWorkspaceQuotaError", } export interface CreateWorkspacePageViewProps { @@ -32,7 +30,6 @@ export interface CreateWorkspacePageViewProps { templates?: TypesGen.Template[] selectedTemplate?: TypesGen.Template templateSchema?: TypesGen.ParameterSchema[] - workspaceQuota?: TypesGen.WorkspaceQuota createWorkspaceErrors: Partial> canCreateForUser?: boolean owner: TypesGen.User | null @@ -94,12 +91,6 @@ export const CreateWorkspacePageView: FC< }, }) - const canSubmit = - props.workspaceQuota && props.workspaceQuota.user_workspace_limit > 0 - ? props.workspaceQuota.user_workspace_count < - props.workspaceQuota.user_workspace_limit - : true - const isLoading = props.loadingTemplateSchema || props.loadingTemplates const getFieldHelpers = getFormHelpers( @@ -237,17 +228,6 @@ export const CreateWorkspacePageView: FC< inputMargin="dense" showAvatar /> - - {props.workspaceQuota && ( - - )}
)} @@ -289,7 +269,6 @@ export const CreateWorkspacePageView: FC< styles={formFooterStyles} onCancel={props.onCancel} isLoading={props.creatingWorkspace} - submitDisabled={!canSubmit} submitLabel={t("createWorkspace")} /> diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index 44e87e3d53b74..2af7ecc257158 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -29,6 +29,7 @@ export const CreateGroupPageView: React.FC = ({ initialValues: { name: "", avatar_url: "", + quota_allowance: 0, }, validationSchema, onSubmit, diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx index 6833ae6862194..3e9c33efb8fd7 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx @@ -22,10 +22,12 @@ import * as Yup from "yup" type FormData = { name: string avatar_url: string + quota_allowance: number } const validationSchema = Yup.object({ name: nameValidator("Name"), + quota_allowance: Yup.number().required().positive().integer(), }) const UpdateGroupForm: React.FC<{ @@ -40,6 +42,7 @@ const UpdateGroupForm: React.FC<{ initialValues: { name: group.name, avatar_url: group.avatar_url, + quota_allowance: group.quota_allowance, }, validationSchema, onSubmit, @@ -121,6 +124,20 @@ const UpdateGroupForm: React.FC<{ /> + + + This group gives {form.values.quota_allowance} quota credits to each + of its members. + + diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 46fb48d620137..a1882c4422c5e 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -8,6 +8,7 @@ import { Loader } from "components/Loader/Loader" import { firstOrItem } from "util/array" import { workspaceMachine } from "xServices/workspace/workspaceXService" import { WorkspaceReadyPage } from "./WorkspaceReadyPage" +import { quotaMachine } from "xServices/quotas/quotasXService" export const WorkspacePage: FC = () => { const { username: usernameQueryParam, workspace: workspaceQueryParam } = @@ -21,6 +22,8 @@ export const WorkspacePage: FC = () => { getTemplateWarning, checkPermissionsError, } = workspaceState.context + const [quotaState, quotaSend] = useMachine(quotaMachine) + const { getQuotaError } = quotaState.context const styles = useStyles() /** @@ -33,6 +36,10 @@ export const WorkspacePage: FC = () => { workspaceSend({ type: "GET_WORKSPACE", username, workspaceName }) }, [username, workspaceName, workspaceSend]) + useEffect(() => { + username && quotaSend({ type: "GET_QUOTA", username }) + }, [username, quotaSend]) + return ( @@ -46,11 +53,21 @@ export const WorkspacePage: FC = () => { {Boolean(checkPermissionsError) && ( )} + {Boolean(getQuotaError) && ( + + )} - + diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 772769312011a..ec0c96b402ca8 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -10,6 +10,7 @@ import { getMinDeadline, } from "util/schedule" import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" +import { quotaMachine } from "xServices/quotas/quotasXService" import { StateFrom } from "xstate" import { DeleteDialog } from "../../components/Dialogs/DeleteDialog/DeleteDialog" import { @@ -26,11 +27,13 @@ import { interface WorkspaceReadyPageProps { workspaceState: StateFrom + quotaState: StateFrom workspaceSend: (event: WorkspaceEvent) => void } export const WorkspaceReadyPage = ({ workspaceState, + quotaState, workspaceSend, }: WorkspaceReadyPageProps): JSX.Element => { const [bannerState, bannerSend] = useActor( @@ -124,6 +127,7 @@ export const WorkspaceReadyPage = ({ buildInfo={buildInfoState.context.buildInfo} applicationsHost={applicationsHost} template={template} + quota_budget={quotaState.context.quota?.budget} /> { @@ -243,4 +243,8 @@ export const handlers = [ rest.delete("/api/v2/groups/:groupId", (req, res, ctx) => { return res(ctx.status(204)) }), + + rest.get("/api/v2/workspace-quota/:userId", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(MockWorkspaceQuota)) + }), ] diff --git a/site/src/util/groups.ts b/site/src/util/groups.ts index a5140bb7b4762..2324a4a27fd60 100644 --- a/site/src/util/groups.ts +++ b/site/src/util/groups.ts @@ -6,6 +6,7 @@ export const everyOneGroup = (organizationId: string): Group => ({ organization_id: organizationId, members: [], avatar_url: "", + quota_allowance: 0, }) export const getGroupSubtitle = (group: Group): string => { diff --git a/site/src/xServices/createWorkspace/createWorkspaceXService.ts b/site/src/xServices/createWorkspace/createWorkspaceXService.ts index 0f4deb2890039..80b9ce6b6f42f 100644 --- a/site/src/xServices/createWorkspace/createWorkspaceXService.ts +++ b/site/src/xServices/createWorkspace/createWorkspaceXService.ts @@ -3,7 +3,6 @@ import { createWorkspace, getTemplates, getTemplateVersionSchema, - getWorkspaceQuota, } from "api/api" import { CreateWorkspaceRequest, @@ -11,7 +10,6 @@ import { Template, User, Workspace, - WorkspaceQuota, } from "api/typesGenerated" import { assign, createMachine } from "xstate" @@ -19,7 +17,6 @@ type CreateWorkspaceContext = { organizationId: string owner: User | null templateName: string - workspaceQuotaEnabled: boolean templates?: Template[] selectedTemplate?: Template templateSchema?: ParameterSchema[] @@ -30,8 +27,6 @@ type CreateWorkspaceContext = { getTemplateSchemaError?: Error | unknown permissions?: Record checkPermissionsError?: Error | unknown - workspaceQuota?: WorkspaceQuota - getWorkspaceQuotaError?: Error | unknown } type CreateWorkspaceEvent = { @@ -60,9 +55,6 @@ export const createWorkspaceMachine = createMachine( getTemplateSchema: { data: ParameterSchema[] } - getWorkspaceQuota: { - data: WorkspaceQuota - } createWorkspace: { data: Workspace } @@ -110,25 +102,11 @@ export const createWorkspaceMachine = createMachine( src: "checkPermissions", id: "checkPermissions", onDone: { - actions: ["assignPermissions"], - target: "gettingWorkspaceQuota", - }, - onError: { - actions: ["assignCheckPermissionsError"], - }, - }, - }, - gettingWorkspaceQuota: { - entry: "clearGetWorkspaceQuotaError", - invoke: { - src: "getWorkspaceQuota", - onDone: { - actions: ["assignWorkspaceQuota"], + actions: "assignPermissions", target: "fillingParams", }, onError: { - actions: ["assignGetWorkspaceQuotaError"], - target: "error", + actions: ["assignCheckPermissionsError"], }, }, }, @@ -140,7 +118,7 @@ export const createWorkspaceMachine = createMachine( }, SELECT_OWNER: { actions: ["assignOwner"], - target: "gettingWorkspaceQuota", + target: ["fillingParams"], }, }, }, @@ -212,17 +190,6 @@ export const createWorkspaceMachine = createMachine( createWorkspaceRequest, ) }, - getWorkspaceQuota: (context) => { - if (!context.workspaceQuotaEnabled) { - // resolving with a limit of 0 will disable the component - return Promise.resolve({ - user_workspace_count: 0, - user_workspace_limit: 0, - }) - } - - return getWorkspaceQuota(context.owner?.id ?? "me") - }, }, guards: { areTemplatesEmpty: (_, event) => event.data.length === 0, @@ -278,15 +245,6 @@ export const createWorkspaceMachine = createMachine( clearGetTemplateSchemaError: assign({ getTemplateSchemaError: (_) => undefined, }), - assignWorkspaceQuota: assign({ - workspaceQuota: (_, event) => event.data, - }), - assignGetWorkspaceQuotaError: assign({ - getWorkspaceQuotaError: (_, event) => event.data, - }), - clearGetWorkspaceQuotaError: assign({ - getWorkspaceQuotaError: (_) => undefined, - }), }, }, ) diff --git a/site/src/xServices/groups/editGroupXService.ts b/site/src/xServices/groups/editGroupXService.ts index ae06e937998b1..93c2311825b8f 100644 --- a/site/src/xServices/groups/editGroupXService.ts +++ b/site/src/xServices/groups/editGroupXService.ts @@ -29,7 +29,7 @@ export const editGroupMachine = createMachine( }, events: {} as { type: "UPDATE" - data: { name: string; avatar_url: string } + data: { name: string; avatar_url: string; quota_allowance: number } }, }, tsTypes: {} as import("./editGroupXService.typegen").Typegen0, diff --git a/site/src/xServices/quotas/quotasXService.ts b/site/src/xServices/quotas/quotasXService.ts new file mode 100644 index 0000000000000..ae9a9652f54ee --- /dev/null +++ b/site/src/xServices/quotas/quotasXService.ts @@ -0,0 +1,77 @@ +import { assign, createMachine } from "xstate" +import * as API from "../../api/api" +import { WorkspaceQuota } from "../../api/typesGenerated" + +export const Language = { + getQuotaError: "Failed to get Quota", +} + +export type QuotaContext = { + quota?: WorkspaceQuota + getQuotaError?: Error | unknown +} + +export type QuotaEvent = { + type: "GET_QUOTA" + username: string +} + +export const quotaMachine = createMachine( + { + id: "quotasMachine", + predictableActionArguments: true, + tsTypes: {} as import("./quotasXService.typegen").Typegen0, + schema: { + context: {} as QuotaContext, + events: {} as QuotaEvent, + services: { + getQuota: { + data: {} as WorkspaceQuota, + }, + }, + }, + context: {}, + initial: "idle", + states: { + idle: { + on: { + GET_QUOTA: "gettingQuotas", + }, + }, + gettingQuotas: { + entry: "clearGetQuotaError", + invoke: { + id: "getQuota", + src: "getQuota", + onDone: { + target: "success", + actions: ["assignQuota"], + }, + onError: { + target: "idle", + actions: ["assignGetQuotaError"], + }, + }, + }, + success: { + type: "final", + }, + }, + }, + { + actions: { + assignQuota: assign({ + quota: (_, event) => event.data, + }), + assignGetQuotaError: assign({ + getQuotaError: (_, event) => event.data, + }), + clearGetQuotaError: assign({ + getQuotaError: (_) => undefined, + }), + }, + services: { + getQuota: (context, event) => API.getWorkspaceQuota(event.username), + }, + }, +)