Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a0a8246
initial mock
f0ssel Aug 30, 2022
b19ce57
move to navbar
f0ssel Aug 31, 2022
06f3a1a
fix infinity size
f0ssel Aug 31, 2022
8342dcb
add storybook
f0ssel Aug 31, 2022
03d1def
connect entitlement
f0ssel Sep 23, 2022
cce7c45
wip
f0ssel Sep 23, 2022
27346fd
demoable
f0ssel Sep 26, 2022
d3f7aa7
only on create page
f0ssel Sep 26, 2022
07b807c
wip
f0ssel Sep 26, 2022
082a04f
add enforcers
f0ssel Sep 27, 2022
1faa7c7
add quota enforcer to routes
f0ssel Sep 27, 2022
347cfa7
remove unused changes
f0ssel Sep 27, 2022
f712ba8
remove unused changes
f0ssel Sep 27, 2022
e464126
make gen
f0ssel Sep 27, 2022
cbe7882
lint
f0ssel Sep 27, 2022
65a20a9
fix tests
f0ssel Sep 27, 2022
bdce03a
add state
f0ssel Sep 27, 2022
c3c7261
fmt
f0ssel Sep 27, 2022
347c834
cleanup names
f0ssel Sep 27, 2022
76db339
options
f0ssel Sep 27, 2022
7f4a245
remove enforcer
f0ssel Sep 27, 2022
d357edf
enttest options
f0ssel Sep 27, 2022
63eed86
fix option usage
f0ssel Sep 29, 2022
9d3ab76
add tests for flags
f0ssel Sep 29, 2022
6527c43
add more tests
f0ssel Sep 29, 2022
5e66175
add more tests
f0ssel Sep 29, 2022
3f09989
pr review
f0ssel Sep 29, 2022
4955b4f
fmt
f0ssel Sep 29, 2022
08b6891
fmt
f0ssel Sep 29, 2022
4c30857
fix xstate merge
f0ssel Sep 29, 2022
bf7f462
support selecting users
f0ssel Sep 29, 2022
d962f1b
fmt
f0ssel Sep 29, 2022
0145ee8
only fetch if enabled
f0ssel Sep 30, 2022
308a9e5
fmt
f0ssel Sep 30, 2022
cd2c263
fix promise
f0ssel Sep 30, 2022
aad4f41
fix return again
f0ssel Sep 30, 2022
e2ae55e
move quota route to ent
f0ssel Sep 30, 2022
1c2d7d6
fix ent route
f0ssel Sep 30, 2022
94f6abc
fix ent route in site
f0ssel Sep 30, 2022
aa3fd9c
replace loop with count query
f0ssel Sep 30, 2022
725b3ee
missing returns
f0ssel Sep 30, 2022
d679ef5
lint
f0ssel Sep 30, 2022
ca672f8
fix page view
f0ssel Sep 30, 2022
01239c1
fix db mock
f0ssel Sep 30, 2022
1129264
fmt
f0ssel Sep 30, 2022
706fabd
fix logic
f0ssel Sep 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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/site"
Expand All @@ -55,6 +56,7 @@ 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.
Expand Down Expand Up @@ -120,6 +122,9 @@ 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 != "" {
Expand All @@ -145,10 +150,12 @@ func New(options *Options) *API {
Authorizer: options.Authorizer,
Logger: options.Logger,
},
metricsCache: metricsCache,
Auditor: atomic.Pointer[audit.Auditor]{},
metricsCache: metricsCache,
Auditor: atomic.Pointer[audit.Auditor]{},
WorkspaceQuotaEnforcer: atomic.Pointer[workspacequota.Enforcer]{},
}
api.Auditor.Store(&options.Auditor)
api.WorkspaceQuotaEnforcer.Store(&options.WorkspaceQuotaEnforcer)
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0)
api.derpServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger))
oauthConfigs := &httpmw.OAuth2Configs{
Expand Down Expand Up @@ -516,6 +523,7 @@ type API struct {
*Options
Auditor atomic.Pointer[audit.Auditor]
WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool]
WorkspaceQuotaEnforcer atomic.Pointer[workspacequota.Enforcer]
HTTPAuth *HTTPAuthorizer

// APIHandler serves "/api/v2"
Expand Down
16 changes: 16 additions & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,22 @@ func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (da
return database.WorkspaceBuild{}, sql.ErrNoRows
}

func (q *fakeQuerier) GetWorkspaceCountByUserID(_ context.Context, id uuid.UUID) (int64, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var count int64
for _, workspace := range q.workspaces {
if workspace.OwnerID.String() == id.String() {
if workspace.Deleted {
continue
}

count++
}
}
return count, nil
}

func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
Expand Down
1 change: 1 addition & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions coderd/database/queries/workspaces.sql
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ WHERE
GROUP BY
template_id;

-- name: GetWorkspaceCountByUserID :one
SELECT
COUNT(id)
FROM
workspaces
WHERE
owner_id = @owner_id
-- Ignore deleted workspaces
AND deleted != true;

-- name: InsertWorkspace :one
INSERT INTO
workspaces (
Expand Down
19 changes: 19 additions & 0 deletions coderd/workspacequota/workspacequota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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
}
25 changes: 23 additions & 2 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,25 @@ 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{
Expand Down Expand Up @@ -352,8 +371,10 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
return
}

var provisionerJob database.ProvisionerJob
var workspaceBuild database.WorkspaceBuild
var (
provisionerJob database.ProvisionerJob
workspaceBuild database.WorkspaceBuild
)
err = api.Database.InTx(func(db database.Store) error {
now := database.Now()
workspaceBuildID := uuid.New()
Expand Down
17 changes: 12 additions & 5 deletions codersdk/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ const (
)

const (
FeatureUserLimit = "user_limit"
FeatureAuditLog = "audit_log"
FeatureBrowserOnly = "browser_only"
FeatureSCIM = "scim"
FeatureUserLimit = "user_limit"
FeatureAuditLog = "audit_log"
FeatureBrowserOnly = "browser_only"
FeatureSCIM = "scim"
FeatureWorkspaceQuota = "workspace_quota"
)

var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog, FeatureBrowserOnly, FeatureSCIM}
var FeatureNames = []string{
FeatureUserLimit,
FeatureAuditLog,
FeatureBrowserOnly,
FeatureSCIM,
FeatureWorkspaceQuota,
}

type Feature struct {
Entitlement Entitlement `json:"entitlement"`
Expand Down
26 changes: 26 additions & 0 deletions codersdk/workspacequota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package codersdk

import (
"context"
"encoding/json"
"fmt"
"net/http"
)

type WorkspaceQuota struct {
UserWorkspaceCount int `json:"user_workspace_count"`
UserWorkspaceLimit int `json:"user_workspace_limit"`
}

func (c *Client) WorkspaceQuota(ctx context.Context, userID string) (WorkspaceQuota, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspace-quota/%s", userID), nil)
if err != nil {
return WorkspaceQuota{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return WorkspaceQuota{}, readBodyAsError(res)
}
var quota WorkspaceQuota
return quota, json.NewDecoder(res.Body).Decode(&quota)
}
4 changes: 3 additions & 1 deletion enterprise/cli/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,16 @@ func TestFeaturesList(t *testing.T) {
var entitlements codersdk.Entitlements
err := json.Unmarshal(buf.Bytes(), &entitlements)
require.NoError(t, err, "unmarshal JSON output")
assert.Len(t, entitlements.Features, 3)
assert.Len(t, entitlements.Features, 4)
assert.Empty(t, entitlements.Warnings)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureUserLimit].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureBrowserOnly].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureWorkspaceQuota].Entitlement)
assert.False(t, entitlements.HasLicense)
})
}
18 changes: 11 additions & 7 deletions enterprise/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@ import (

func server() *cobra.Command {
var (
auditLogging bool
browserOnly bool
scimAuthHeader string
auditLogging bool
browserOnly bool
scimAuthHeader string
userWorkspaceQuota int
)
cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, error) {
api, err := coderd.New(ctx, &coderd.Options{
AuditLogging: auditLogging,
BrowserOnly: browserOnly,
SCIMAPIKey: []byte(scimAuthHeader),
Options: options,
AuditLogging: auditLogging,
BrowserOnly: browserOnly,
SCIMAPIKey: []byte(scimAuthHeader),
UserWorkspaceQuota: userWorkspaceQuota,
Options: options,
})
if err != nil {
return nil, err
Expand All @@ -39,6 +41,8 @@ func server() *cobra.Command {
"Whether Coder only allows connections to workspaces via the browser. "+enterpriseOnly)
cliflag.StringVarP(cmd.Flags(), &scimAuthHeader, "scim-auth-header", "", "CODER_SCIM_API_KEY", "",
"Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication. "+enterpriseOnly)
cliflag.IntVarP(cmd.Flags(), &userWorkspaceQuota, "user-workspace-quota", "", "CODER_USER_WORKSPACE_QUOTA", 0,
"A positive number applies a limit on how many workspaces each user can create. "+enterpriseOnly)

return cmd
}
Loading