mirror of https://github.com/coder/coder.git
Implement Quotas v3 (#5012)
* provisioner/terraform: add cost to resource_metadata * provisionerd/runner: use Options struct * Complete provisionerd implementation * Add quota_allowance to groups * Combine Quota and RBAC licenses * Add Opts to InTx
This commit is contained in:
parent
3fb7892c07
commit
97dbd4dc5d
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ func TestInTx(t *testing.T) {
|
|||
})
|
||||
assert.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
}, nil)
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
var nums []int
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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 *;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 != "",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
||||
<img src="../images/admin/quotas.png"/>
|
||||
## Establishing Budgets
|
||||
|
||||
## Enabling this feature
|
||||
Each group has a configurable Quota Allowance. A user's budget is calculated as
|
||||
the sum of their allowances.
|
||||
|
||||
This feature is only available with an enterprise license. [Learn more](../enterprise.md)
|
||||
![group-settings](../images/admin/quota-groups.png)
|
||||
|
||||
For example:
|
||||
|
||||
| Group Name | Quota Allowance |
|
||||
| ---------- | --------------- |
|
||||
| Frontend | 100 |
|
||||
| Backend | 200 |
|
||||
| Data | 300 |
|
||||
|
||||
<br/>
|
||||
|
||||
| Username | Groups | Effective Budget |
|
||||
| -------- | ----------------- | ---------------- |
|
||||
| jill | Frontend, Backend | 300 |
|
||||
| jack | Backend, Data | 500 |
|
||||
| sam | Data | 300 |
|
||||
| alex | Frontend | 100 |
|
||||
|
||||
## Quota Enforcement
|
||||
|
||||
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.
|
||||
|
||||
![build-log](../images/admin/quota-buildlog.png)
|
||||
|
||||
## Up next
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 859 KiB |
Binary file not shown.
After Width: | Height: | Size: 594 KiB |
Binary file not shown.
Before Width: | Height: | Size: 791 KiB |
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// TODO: Do we care about validating this?
|
||||
if req.AvatarURL != nil {
|
||||
group.AvatarURL = *req.AvatarURL
|
||||
}
|
||||
if req.Name != "" {
|
||||
group.Name = req.Name
|
||||
updateGroupParams := database.UpdateGroupByIDParams{
|
||||
ID: group.ID,
|
||||
AvatarURL: group.AvatarURL,
|
||||
Name: group.Name,
|
||||
QuotaAllowance: group.QuotaAllowance,
|
||||
}
|
||||
|
||||
group, err = tx.UpdateGroupByID(ctx, database.UpdateGroupByIDParams{
|
||||
ID: group.ID,
|
||||
Name: group.Name,
|
||||
AvatarURL: group.AvatarURL,
|
||||
})
|
||||
// TODO: Do we care about validating this?
|
||||
if req.AvatarURL != nil {
|
||||
updateGroupParams.AvatarURL = *req.AvatarURL
|
||||
}
|
||||
if req.Name != "" {
|
||||
updateGroupParams.Name = req.Name
|
||||
}
|
||||
if req.QuotaAllowance != nil {
|
||||
updateGroupParams.QuotaAllowance = int32(*req.QuotaAllowance)
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (e *enforcer) UserWorkspaceLimit() int {
|
||||
return e.userWorkspaceLimit
|
||||
}
|
||||
|
||||
func (e *enforcer) CanCreateWorkspace(count int) bool {
|
||||
if e.userWorkspaceLimit == 0 {
|
||||
return true
|
||||
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
|
||||
}
|
||||
|
||||
return count < e.userWorkspaceLimit
|
||||
build, err := c.Database.GetWorkspaceBuildByJobID(ctx, jobID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workspace, err := c.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
consumed int64
|
||||
budget int64
|
||||
permit bool
|
||||
)
|
||||
err = c.Database.InTx(func(s database.Store) error {
|
||||
var err error
|
||||
consumed, err = s.GetQuotaConsumedForUser(ctx, workspace.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
budget, err = s.GetQuotaAllowanceForUser(ctx, workspace.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the new build will reduce overall quota consumption, then we
|
||||
// allow it even if the user is over quota.
|
||||
netIncrease := true
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 TestWorkspaceQuota(t *testing.T) {
|
||||
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)
|
||||
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()
|
||||
|
||||
// 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,
|
||||
})
|
||||
q1, err := client.WorkspaceQuota(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, q1.UserWorkspaceCount, 0)
|
||||
require.EqualValues(t, q1.UserWorkspaceLimit, max)
|
||||
|
||||
group2, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "test-2",
|
||||
QuotaAllowance: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, 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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"format_version": "1.1",
|
||||
"terraform_version": "1.3.4",
|
||||
"terraform_version": "1.3.3",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
|
|
|
@ -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": {}
|
||||
|
|
2
provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json
generated
vendored
2
provisioner/terraform/testdata/chaining-resources/chaining-resources.tfplan.json
generated
vendored
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"format_version": "1.1",
|
||||
"terraform_version": "1.3.4",
|
||||
"terraform_version": "1.3.3",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
|
|
6
provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json
generated
vendored
6
provisioner/terraform/testdata/chaining-resources/chaining-resources.tfstate.json
generated
vendored
|
@ -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": {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"format_version": "1.1",
|
||||
"terraform_version": "1.3.4",
|
||||
"terraform_version": "1.3.3",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
|
|
10
provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json
generated
vendored
10
provisioner/terraform/testdata/conflicting-resources/conflicting-resources.tfstate.json
generated
vendored
|
@ -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": {},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"format_version": "1.1",
|
||||
"terraform_version": "1.3.4",
|
||||
"terraform_version": "1.3.3",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
|
|
|
@ -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": {},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"format_version": "1.1",
|
||||
"terraform_version": "1.3.4",
|
||||
"terraform_version": "1.3.3",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
|
|
|
@ -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": {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"format_version": "1.1",
|
||||
"terraform_version": "1.3.4",
|
||||
"terraform_version": "1.3.3",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
9
provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json
generated
vendored
9
provisioner/terraform/testdata/resource-metadata/resource-metadata.tfstate.json
generated
vendored
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package proto
|
||||
|
||||
import context "context"
|
||||
|
||||
type QuotaCommitter interface {
|
||||
CommitQuota(ctx context.Context, request *CommitQuotaRequest) (*CommitQuotaResponse, error)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -21,6 +21,5 @@ export enum FeatureNames {
|
|||
UserLimit = "user_limit",
|
||||
BrowserOnly = "browser_only",
|
||||
SCIM = "scim",
|
||||
WorkspaceQuota = "workspace_quota",
|
||||
TemplateRBAC = "template_rbac",
|
||||
}
|
||||
|
|
|
@ -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<boolean>
|
||||
readonly browser_only: DeploymentConfigField<boolean>
|
||||
readonly scim_api_key: DeploymentConfigField<string>
|
||||
readonly user_workspace_quota: DeploymentConfigField<number>
|
||||
readonly provisioner: ProvisionerConfig
|
||||
readonly api_rate_limit: DeploymentConfigField<number>
|
||||
readonly experimental: DeploymentConfigField<boolean>
|
||||
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -53,6 +53,16 @@ export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
|
|||
|
||||
<Stack alignItems="flex-start" direction="row" spacing={5}>
|
||||
<div className={styles.metadataHeader}>
|
||||
{resource.daily_cost > 0 && (
|
||||
<div className={styles.metadata}>
|
||||
<div className={styles.metadataLabel}>
|
||||
<b>cost</b>
|
||||
</div>
|
||||
<div className={styles.metadataValue}>
|
||||
{resource.daily_cost}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{visibleMetadata.map((meta) => {
|
||||
return (
|
||||
<div className={styles.metadata} key={meta.key}>
|
||||
|
|
|
@ -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<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
buildInfo,
|
||||
applicationsHost,
|
||||
template,
|
||||
quota_budget,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const styles = useStyles()
|
||||
|
@ -187,7 +189,11 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
handleClick={() => navigate(`/templates`)}
|
||||
/>
|
||||
|
||||
<WorkspaceStats workspace={workspace} handleUpdate={handleUpdate} />
|
||||
<WorkspaceStats
|
||||
workspace={workspace}
|
||||
quota_budget={quota_budget}
|
||||
handleUpdate={handleUpdate}
|
||||
/>
|
||||
|
||||
{isTransitioning !== undefined && isTransitioning && (
|
||||
<WorkspaceBuildProgress
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { WorkspaceQuota, WorkspaceQuotaProps } from "./WorkspaceQuota"
|
||||
|
||||
export default {
|
||||
title: "components/WorkspaceQuota",
|
||||
component: WorkspaceQuota,
|
||||
}
|
||||
|
||||
const Template: Story<WorkspaceQuotaProps> = (args) => (
|
||||
<WorkspaceQuota {...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,
|
||||
},
|
||||
}
|
|
@ -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<WorkspaceQuotaProps> = ({ quota, error }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
// error state
|
||||
if (error !== undefined) {
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={1} className={styles.stack}>
|
||||
<span className={styles.title}>Usage Quota</span>
|
||||
<AlertBanner severity="error" error={error} />
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// loading
|
||||
if (quota === undefined) {
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={1} className={styles.stack}>
|
||||
<span className={styles.title}>Usage quota</span>
|
||||
<LinearProgress color="primary" />
|
||||
<div className={styles.label}>
|
||||
<Skeleton className={styles.skeleton} />
|
||||
</div>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Box>
|
||||
<Stack spacing={1.5} className={styles.stack}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<span className={styles.title}>Usage Quota</span>
|
||||
<div className={styles.label}>
|
||||
<span className={styles.labelHighlight}>
|
||||
{quota.user_workspace_count}
|
||||
</span>{" "}
|
||||
{Language.of}{" "}
|
||||
<span className={styles.labelHighlight}>
|
||||
{quota.user_workspace_limit}
|
||||
</span>{" "}
|
||||
{quota.user_workspace_limit === 1
|
||||
? Language.workspace
|
||||
: Language.workspaces}
|
||||
{" used"}
|
||||
</div>
|
||||
</Stack>
|
||||
<LinearProgress
|
||||
className={
|
||||
quota.user_workspace_count >= quota.user_workspace_limit
|
||||
? styles.maxProgress
|
||||
: undefined
|
||||
}
|
||||
value={value}
|
||||
variant="determinate"
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
}))
|
|
@ -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<WorkspaceStatsProps> = ({
|
||||
workspace,
|
||||
quota_budget,
|
||||
handleUpdate,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
@ -74,6 +77,14 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
|||
<span className={styles.statsLabel}>{Language.byLabel}:</span>
|
||||
<span className={styles.statsValue}>{initiatedBy}</span>
|
||||
</div>
|
||||
{workspace.latest_build.daily_cost > 0 && (
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statsLabel}>{Language.costLabel}:</span>
|
||||
<span className={styles.statsValue}>
|
||||
{workspace.latest_build.daily_cost} / {quota_budget}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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<Record<CreateWorkspaceErrors, Error | unknown>>
|
||||
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<TypesGen.CreateWorkspaceRequest>(
|
||||
|
@ -237,17 +228,6 @@ export const CreateWorkspacePageView: FC<
|
|||
inputMargin="dense"
|
||||
showAvatar
|
||||
/>
|
||||
|
||||
{props.workspaceQuota && (
|
||||
<WorkspaceQuota
|
||||
quota={props.workspaceQuota}
|
||||
error={
|
||||
props.createWorkspaceErrors[
|
||||
CreateWorkspaceErrors.GET_WORKSPACE_QUOTA_ERROR
|
||||
]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
|
@ -289,7 +269,6 @@ export const CreateWorkspacePageView: FC<
|
|||
styles={formFooterStyles}
|
||||
onCancel={props.onCancel}
|
||||
isLoading={props.creatingWorkspace}
|
||||
submitDisabled={!canSubmit}
|
||||
submitLabel={t("createWorkspace")}
|
||||
/>
|
||||
</Stack>
|
||||
|
|
|
@ -29,6 +29,7 @@ export const CreateGroupPageView: React.FC<CreateGroupPageViewProps> = ({
|
|||
initialValues: {
|
||||
name: "",
|
||||
avatar_url: "",
|
||||
quota_allowance: 0,
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
|
|
|
@ -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<{
|
|||
/>
|
||||
</Popover>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("quota_allowance")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
fullWidth
|
||||
type="number"
|
||||
label="Quota Allowance"
|
||||
variant="outlined"
|
||||
/>
|
||||
<span>
|
||||
This group gives {form.values.quota_allowance} quota credits to each
|
||||
of its members.
|
||||
</span>
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
</form>
|
||||
</FullPageForm>
|
||||
|
|
|
@ -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 (
|
||||
<ChooseOne>
|
||||
<Cond condition={workspaceState.matches("error")}>
|
||||
|
@ -46,11 +53,21 @@ export const WorkspacePage: FC = () => {
|
|||
{Boolean(checkPermissionsError) && (
|
||||
<AlertBanner severity="error" error={checkPermissionsError} />
|
||||
)}
|
||||
{Boolean(getQuotaError) && (
|
||||
<AlertBanner severity="error" error={getQuotaError} />
|
||||
)}
|
||||
</div>
|
||||
</Cond>
|
||||
<Cond condition={Boolean(workspace) && workspaceState.matches("ready")}>
|
||||
<Cond
|
||||
condition={
|
||||
Boolean(workspace) &&
|
||||
workspaceState.matches("ready") &&
|
||||
quotaState.matches("success")
|
||||
}
|
||||
>
|
||||
<WorkspaceReadyPage
|
||||
workspaceState={workspaceState}
|
||||
quotaState={quotaState}
|
||||
workspaceSend={workspaceSend}
|
||||
/>
|
||||
</Cond>
|
||||
|
|
|
@ -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<typeof workspaceMachine>
|
||||
quotaState: StateFrom<typeof quotaMachine>
|
||||
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}
|
||||
/>
|
||||
<DeleteDialog
|
||||
entity="workspace"
|
||||
|
|
|
@ -311,6 +311,7 @@ export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
|
|||
{ key: "type", value: "a-workspace-resource", sensitive: false },
|
||||
{ key: "api_key", value: "12345678", sensitive: true },
|
||||
],
|
||||
daily_cost: 10,
|
||||
}
|
||||
|
||||
export const MockWorkspaceResource2: TypesGen.WorkspaceResource = {
|
||||
|
@ -331,6 +332,7 @@ export const MockWorkspaceResource2: TypesGen.WorkspaceResource = {
|
|||
{ key: "type", value: "google_compute_disk", sensitive: false },
|
||||
{ key: "size", value: "32GB", sensitive: false },
|
||||
],
|
||||
daily_cost: 10,
|
||||
}
|
||||
|
||||
export const MockWorkspaceResource3: TypesGen.WorkspaceResource = {
|
||||
|
@ -351,6 +353,7 @@ export const MockWorkspaceResource3: TypesGen.WorkspaceResource = {
|
|||
{ key: "type", value: "google_compute_disk", sensitive: false },
|
||||
{ key: "size", value: "32GB", sensitive: false },
|
||||
],
|
||||
daily_cost: 20,
|
||||
}
|
||||
|
||||
export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest =
|
||||
|
@ -383,6 +386,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = {
|
|||
reason: "initiator",
|
||||
resources: [MockWorkspaceResource],
|
||||
status: "running",
|
||||
daily_cost: 20,
|
||||
}
|
||||
|
||||
export const MockFailedWorkspaceBuild = (
|
||||
|
@ -405,6 +409,7 @@ export const MockFailedWorkspaceBuild = (
|
|||
reason: "initiator",
|
||||
resources: [],
|
||||
status: "running",
|
||||
daily_cost: 20,
|
||||
})
|
||||
|
||||
export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = {
|
||||
|
@ -988,8 +993,8 @@ export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = {
|
|||
}
|
||||
|
||||
export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
|
||||
user_workspace_count: 0,
|
||||
user_workspace_limit: 100,
|
||||
credits_consumed: 0,
|
||||
budget: 100,
|
||||
}
|
||||
|
||||
export const MockGroup: TypesGen.Group = {
|
||||
|
@ -998,6 +1003,7 @@ export const MockGroup: TypesGen.Group = {
|
|||
avatar_url: "https://example.com",
|
||||
organization_id: MockOrganization.id,
|
||||
members: [MockUser, MockUser2],
|
||||
quota_allowance: 5,
|
||||
}
|
||||
|
||||
export const MockTemplateACL: TypesGen.TemplateACL = {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { WorkspaceBuildTransition } from "../api/types"
|
|||
import { CreateWorkspaceBuildRequest } from "../api/typesGenerated"
|
||||
import { permissionsToCheck } from "../xServices/auth/authXService"
|
||||
import * as M from "./entities"
|
||||
import { MockGroup } from "./entities"
|
||||
import { MockGroup, MockWorkspaceQuota } from "./entities"
|
||||
|
||||
export const handlers = [
|
||||
rest.get("/api/v2/templates/:templateId/daus", async (req, res, ctx) => {
|
||||
|
@ -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))
|
||||
}),
|
||||
]
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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<string, boolean>
|
||||
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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue