Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit69c73b2

Browse files
authored
feat: workspace quotas (#4184)
1 parentf9b7588 commit69c73b2

28 files changed

+712
-83
lines changed

‎coderd/coderd.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/coder/coder/coderd/rbac"
3636
"github.com/coder/coder/coderd/telemetry"
3737
"github.com/coder/coder/coderd/tracing"
38+
"github.com/coder/coder/coderd/workspacequota"
3839
"github.com/coder/coder/coderd/wsconncache"
3940
"github.com/coder/coder/codersdk"
4041
"github.com/coder/coder/site"
@@ -55,6 +56,7 @@ type Options struct {
5556
CacheDirstring
5657

5758
Auditor audit.Auditor
59+
WorkspaceQuotaEnforcer workspacequota.Enforcer
5860
AgentConnectionUpdateFrequency time.Duration
5961
AgentInactiveDisconnectTimeout time.Duration
6062
// APIRateLimit is the minutely throughput rate limit per user or ip.
@@ -120,6 +122,9 @@ func New(options *Options) *API {
120122
ifoptions.Auditor==nil {
121123
options.Auditor=audit.NewNop()
122124
}
125+
ifoptions.WorkspaceQuotaEnforcer==nil {
126+
options.WorkspaceQuotaEnforcer=workspacequota.NewNop()
127+
}
123128

124129
siteCacheDir:=options.CacheDir
125130
ifsiteCacheDir!="" {
@@ -145,10 +150,12 @@ func New(options *Options) *API {
145150
Authorizer:options.Authorizer,
146151
Logger:options.Logger,
147152
},
148-
metricsCache:metricsCache,
149-
Auditor: atomic.Pointer[audit.Auditor]{},
153+
metricsCache:metricsCache,
154+
Auditor: atomic.Pointer[audit.Auditor]{},
155+
WorkspaceQuotaEnforcer: atomic.Pointer[workspacequota.Enforcer]{},
150156
}
151157
api.Auditor.Store(&options.Auditor)
158+
api.WorkspaceQuotaEnforcer.Store(&options.WorkspaceQuotaEnforcer)
152159
api.workspaceAgentCache=wsconncache.New(api.dialWorkspaceAgentTailnet,0)
153160
api.derpServer=derp.NewServer(key.NewNode(),tailnet.Logger(options.Logger))
154161
oauthConfigs:=&httpmw.OAuth2Configs{
@@ -516,6 +523,7 @@ type API struct {
516523
*Options
517524
Auditor atomic.Pointer[audit.Auditor]
518525
WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter)bool]
526+
WorkspaceQuotaEnforcer atomic.Pointer[workspacequota.Enforcer]
519527
HTTPAuth*HTTPAuthorizer
520528

521529
// APIHandler serves "/api/v2"

‎coderd/database/databasefake/databasefake.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,22 @@ func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (da
698698
return database.WorkspaceBuild{},sql.ErrNoRows
699699
}
700700

701+
func (q*fakeQuerier)GetWorkspaceCountByUserID(_ context.Context,id uuid.UUID) (int64,error) {
702+
q.mutex.RLock()
703+
deferq.mutex.RUnlock()
704+
varcountint64
705+
for_,workspace:=rangeq.workspaces {
706+
ifworkspace.OwnerID.String()==id.String() {
707+
ifworkspace.Deleted {
708+
continue
709+
}
710+
711+
count++
712+
}
713+
}
714+
returncount,nil
715+
}
716+
701717
func (q*fakeQuerier)GetWorkspaceBuildByJobID(_ context.Context,jobID uuid.UUID) (database.WorkspaceBuild,error) {
702718
q.mutex.RLock()
703719
deferq.mutex.RUnlock()

‎coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/queries.sql.go

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/queries/workspaces.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ WHERE
7474
GROUP BY
7575
template_id;
7676

77+
-- name: GetWorkspaceCountByUserID :one
78+
SELECT
79+
COUNT(id)
80+
FROM
81+
workspaces
82+
WHERE
83+
owner_id= @owner_id
84+
-- Ignore deleted workspaces
85+
AND deleted!= true;
86+
7787
-- name: InsertWorkspace :one
7888
INSERT INTO
7989
workspaces (
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package workspacequota
2+
3+
typeEnforcerinterface {
4+
UserWorkspaceLimit()int
5+
CanCreateWorkspace(countint)bool
6+
}
7+
8+
typenopstruct{}
9+
10+
funcNewNop()Enforcer {
11+
return&nop{}
12+
}
13+
14+
func (*nop)UserWorkspaceLimit()int {
15+
return0
16+
}
17+
func (*nop)CanCreateWorkspace(_int)bool {
18+
returntrue
19+
}

‎coderd/workspaces.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,25 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
317317
return
318318
}
319319

320+
workspaceCount,err:=api.Database.GetWorkspaceCountByUserID(ctx,user.ID)
321+
iferr!=nil {
322+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
323+
Message:"Internal error fetching workspace count.",
324+
Detail:err.Error(),
325+
})
326+
return
327+
}
328+
329+
// make sure the user has not hit their quota limit
330+
e:=*api.WorkspaceQuotaEnforcer.Load()
331+
canCreate:=e.CanCreateWorkspace(int(workspaceCount))
332+
if!canCreate {
333+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
334+
Message:fmt.Sprintf("User workspace limit of %d is already reached.",e.UserWorkspaceLimit()),
335+
})
336+
return
337+
}
338+
320339
templateVersion,err:=api.Database.GetTemplateVersionByID(ctx,template.ActiveVersionID)
321340
iferr!=nil {
322341
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
@@ -352,8 +371,10 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
352371
return
353372
}
354373

355-
varprovisionerJob database.ProvisionerJob
356-
varworkspaceBuild database.WorkspaceBuild
374+
var (
375+
provisionerJob database.ProvisionerJob
376+
workspaceBuild database.WorkspaceBuild
377+
)
357378
err=api.Database.InTx(func(db database.Store)error {
358379
now:=database.Now()
359380
workspaceBuildID:=uuid.New()

‎codersdk/features.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,20 @@ const (
1515
)
1616

1717
const (
18-
FeatureUserLimit="user_limit"
19-
FeatureAuditLog="audit_log"
20-
FeatureBrowserOnly="browser_only"
21-
FeatureSCIM="scim"
18+
FeatureUserLimit="user_limit"
19+
FeatureAuditLog="audit_log"
20+
FeatureBrowserOnly="browser_only"
21+
FeatureSCIM="scim"
22+
FeatureWorkspaceQuota="workspace_quota"
2223
)
2324

24-
varFeatureNames= []string{FeatureUserLimit,FeatureAuditLog,FeatureBrowserOnly,FeatureSCIM}
25+
varFeatureNames= []string{
26+
FeatureUserLimit,
27+
FeatureAuditLog,
28+
FeatureBrowserOnly,
29+
FeatureSCIM,
30+
FeatureWorkspaceQuota,
31+
}
2532

2633
typeFeaturestruct {
2734
EntitlementEntitlement`json:"entitlement"`

‎codersdk/workspacequota.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package codersdk
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
)
9+
10+
typeWorkspaceQuotastruct {
11+
UserWorkspaceCountint`json:"user_workspace_count"`
12+
UserWorkspaceLimitint`json:"user_workspace_limit"`
13+
}
14+
15+
func (c*Client)WorkspaceQuota(ctx context.Context,userIDstring) (WorkspaceQuota,error) {
16+
res,err:=c.Request(ctx,http.MethodGet,fmt.Sprintf("/api/v2/workspace-quota/%s",userID),nil)
17+
iferr!=nil {
18+
returnWorkspaceQuota{},err
19+
}
20+
deferres.Body.Close()
21+
ifres.StatusCode!=http.StatusOK {
22+
returnWorkspaceQuota{},readBodyAsError(res)
23+
}
24+
varquotaWorkspaceQuota
25+
returnquota,json.NewDecoder(res.Body).Decode(&quota)
26+
}

‎enterprise/cli/features_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,16 @@ func TestFeaturesList(t *testing.T) {
5757
varentitlements codersdk.Entitlements
5858
err:=json.Unmarshal(buf.Bytes(),&entitlements)
5959
require.NoError(t,err,"unmarshal JSON output")
60-
assert.Len(t,entitlements.Features,3)
60+
assert.Len(t,entitlements.Features,4)
6161
assert.Empty(t,entitlements.Warnings)
6262
assert.Equal(t,codersdk.EntitlementNotEntitled,
6363
entitlements.Features[codersdk.FeatureUserLimit].Entitlement)
6464
assert.Equal(t,codersdk.EntitlementNotEntitled,
6565
entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
6666
assert.Equal(t,codersdk.EntitlementNotEntitled,
6767
entitlements.Features[codersdk.FeatureBrowserOnly].Entitlement)
68+
assert.Equal(t,codersdk.EntitlementNotEntitled,
69+
entitlements.Features[codersdk.FeatureWorkspaceQuota].Entitlement)
6870
assert.False(t,entitlements.HasLicense)
6971
})
7072
}

‎enterprise/cli/server.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@ import (
1515

1616
funcserver()*cobra.Command {
1717
var (
18-
auditLoggingbool
19-
browserOnlybool
20-
scimAuthHeaderstring
18+
auditLoggingbool
19+
browserOnlybool
20+
scimAuthHeaderstring
21+
userWorkspaceQuotaint
2122
)
2223
cmd:=agpl.Server(func(ctx context.Context,options*agplcoderd.Options) (*agplcoderd.API,error) {
2324
api,err:=coderd.New(ctx,&coderd.Options{
24-
AuditLogging:auditLogging,
25-
BrowserOnly:browserOnly,
26-
SCIMAPIKey: []byte(scimAuthHeader),
27-
Options:options,
25+
AuditLogging:auditLogging,
26+
BrowserOnly:browserOnly,
27+
SCIMAPIKey: []byte(scimAuthHeader),
28+
UserWorkspaceQuota:userWorkspaceQuota,
29+
Options:options,
2830
})
2931
iferr!=nil {
3032
returnnil,err
@@ -39,6 +41,8 @@ func server() *cobra.Command {
3941
"Whether Coder only allows connections to workspaces via the browser. "+enterpriseOnly)
4042
cliflag.StringVarP(cmd.Flags(),&scimAuthHeader,"scim-auth-header","","CODER_SCIM_API_KEY","",
4143
"Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication. "+enterpriseOnly)
44+
cliflag.IntVarP(cmd.Flags(),&userWorkspaceQuota,"user-workspace-quota","","CODER_USER_WORKSPACE_QUOTA",0,
45+
"A positive number applies a limit on how many workspaces each user can create. "+enterpriseOnly)
4246

4347
returncmd
4448
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp