Skip to content

Commit 855e8b9

Browse files
authored
Merge branch 'main' into cj/18776/workaround-sidebar-app-id-fk
2 parents d92df3e + b200fc8 commit 855e8b9

File tree

8 files changed

+524
-103
lines changed

8 files changed

+524
-103
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,16 @@ var (
485485
rbac.ResourceFile.Type: {
486486
policy.ActionRead,
487487
},
488+
// Needs to be able to add the prebuilds system user to the "prebuilds" group in each organization that needs prebuilt workspaces
489+
// so that prebuilt workspaces can be scheduled and owned in those organizations.
490+
rbac.ResourceGroup.Type: {
491+
policy.ActionRead,
492+
policy.ActionCreate,
493+
policy.ActionUpdate,
494+
},
495+
rbac.ResourceGroupMember.Type: {
496+
policy.ActionRead,
497+
},
488498
}),
489499
},
490500
}),

coderd/database/queries.sql.go

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/organizationmembers.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ WHERE
8989
organization_id = @organization_id
9090
ELSE true
9191
END
92+
-- Filter by system type
93+
AND CASE WHEN @include_system::bool THEN TRUE ELSE is_system = false END
9294
ORDER BY
9395
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
9496
LOWER(username) ASC OFFSET @offset_opt

coderd/members.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) {
203203

204204
paginatedMemberRows, err := api.Database.PaginatedOrganizationMembers(ctx, database.PaginatedOrganizationMembersParams{
205205
OrganizationID: organization.ID,
206+
IncludeSystem: false,
206207
// #nosec G115 - Pagination limits are small and fit in int32
207208
LimitOpt: int32(paginationParams.Limit),
208209
// #nosec G115 - Pagination offsets are small and fit in int32

docs/admin/templates/extending-templates/prebuilt-workspaces.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,18 @@ The system always maintains the desired number of prebuilt workspaces for the ac
235235

236236
### Managing resource quotas
237237

238-
Prebuilt workspaces can be used in conjunction with [resource quotas](../../users/quotas.md).
238+
To help prevent unexpected infrastructure costs, prebuilt workspaces can be used in conjunction with [resource quotas](../../users/quotas.md).
239239
Because unclaimed prebuilt workspaces are owned by the `prebuilds` user, you can:
240240

241241
1. Configure quotas for any group that includes this user.
242242
1. Set appropriate limits to balance prebuilt workspace availability with resource constraints.
243243

244+
When prebuilt workspaces are configured for an organization, Coder creates a "prebuilds" group in that organization and adds the prebuilds user to it. This group has a default quota allowance of 0, which you should adjust based on your needs:
245+
246+
- **Set a quota allowance** on the "prebuilds" group to control how many prebuilt workspaces can be provisioned
247+
- **Monitor usage** to ensure the quota is appropriate for your desired number of prebuilt instances
248+
- **Adjust as needed** based on your template costs and desired prebuilt workspace pool size
249+
244250
If a quota is exceeded, the prebuilt workspace will fail provisioning the same way other workspaces do.
245251

246252
### Template configuration best practices

enterprise/coderd/prebuilds/membership.go

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import (
1212
"github.com/coder/quartz"
1313
)
1414

15+
const (
16+
PrebuiltWorkspacesGroupName = "coder_prebuilt_workspaces"
17+
PrebuiltWorkspacesGroupDisplayName = "Prebuilt Workspaces"
18+
)
19+
1520
// StoreMembershipReconciler encapsulates the responsibility of ensuring that the prebuilds system user is a member of all
1621
// organizations for which prebuilt workspaces are requested. This is necessary because our data model requires that such
1722
// prebuilt workspaces belong to a member of the organization of their eventual claimant.
@@ -27,11 +32,16 @@ func NewStoreMembershipReconciler(store database.Store, clock quartz.Clock) Stor
2732
}
2833
}
2934

30-
// ReconcileAll compares the current membership of a user to the membership required in order to create prebuilt workspaces.
31-
// If the user in question is not yet a member of an organization that needs prebuilt workspaces, ReconcileAll will create
32-
// the membership required.
35+
// ReconcileAll compares the current organization and group memberships of a user to the memberships required
36+
// in order to create prebuilt workspaces. If the user in question is not yet a member of an organization that
37+
// needs prebuilt workspaces, ReconcileAll will create the membership required.
3338
//
34-
// This method does not have an opinion on transaction or lock management. These responsibilities are left to the caller.
39+
// To facilitate quota management, ReconcileAll will ensure:
40+
// * the existence of a group (defined by PrebuiltWorkspacesGroupName) in each organization that needs prebuilt workspaces
41+
// * that the prebuilds system user belongs to the group in each organization that needs prebuilt workspaces
42+
// * that the group has a quota of 0 by default, which users can adjust based on their needs.
43+
//
44+
// ReconcileAll does not have an opinion on transaction or lock management. These responsibilities are left to the caller.
3545
func (s StoreMembershipReconciler) ReconcileAll(ctx context.Context, userID uuid.UUID, presets []database.GetTemplatePresetsWithPrebuildsRow) error {
3646
organizationMemberships, err := s.store.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{
3747
UserID: userID,
@@ -44,37 +54,74 @@ func (s StoreMembershipReconciler) ReconcileAll(ctx context.Context, userID uuid
4454
return xerrors.Errorf("determine prebuild organization membership: %w", err)
4555
}
4656

47-
systemUserMemberships := make(map[uuid.UUID]struct{}, 0)
57+
orgMemberships := make(map[uuid.UUID]struct{}, 0)
4858
defaultOrg, err := s.store.GetDefaultOrganization(ctx)
4959
if err != nil {
5060
return xerrors.Errorf("get default organization: %w", err)
5161
}
52-
systemUserMemberships[defaultOrg.ID] = struct{}{}
62+
orgMemberships[defaultOrg.ID] = struct{}{}
5363
for _, o := range organizationMemberships {
54-
systemUserMemberships[o.ID] = struct{}{}
64+
orgMemberships[o.ID] = struct{}{}
5565
}
5666

5767
var membershipInsertionErrors error
5868
for _, preset := range presets {
59-
_, alreadyMember := systemUserMemberships[preset.OrganizationID]
60-
if alreadyMember {
61-
continue
69+
_, alreadyOrgMember := orgMemberships[preset.OrganizationID]
70+
if !alreadyOrgMember {
71+
// Add the organization to our list of memberships regardless of potential failure below
72+
// to avoid a retry that will probably be doomed anyway.
73+
orgMemberships[preset.OrganizationID] = struct{}{}
74+
75+
// Insert the missing membership
76+
_, err = s.store.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
77+
OrganizationID: preset.OrganizationID,
78+
UserID: userID,
79+
CreatedAt: s.clock.Now(),
80+
UpdatedAt: s.clock.Now(),
81+
Roles: []string{},
82+
})
83+
if err != nil {
84+
membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("insert membership for prebuilt workspaces: %w", err))
85+
continue
86+
}
6287
}
63-
// Add the organization to our list of memberships regardless of potential failure below
64-
// to avoid a retry that will probably be doomed anyway.
65-
systemUserMemberships[preset.OrganizationID] = struct{}{}
6688

67-
// Insert the missing membership
68-
_, err = s.store.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
89+
// Create a "prebuilds" group in the organization and add the system user to it
90+
// This group will have a quota of 0 by default, which users can adjust based on their needs
91+
prebuildsGroup, err := s.store.InsertGroup(ctx, database.InsertGroupParams{
92+
ID: uuid.New(),
93+
Name: PrebuiltWorkspacesGroupName,
94+
DisplayName: PrebuiltWorkspacesGroupDisplayName,
6995
OrganizationID: preset.OrganizationID,
70-
UserID: userID,
71-
CreatedAt: s.clock.Now(),
72-
UpdatedAt: s.clock.Now(),
73-
Roles: []string{},
96+
AvatarURL: "",
97+
QuotaAllowance: 0, // Default quota of 0, users should set this based on their needs
98+
})
99+
if err != nil {
100+
// If the group already exists, try to get it
101+
if !database.IsUniqueViolation(err) {
102+
membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("create prebuilds group: %w", err))
103+
continue
104+
}
105+
prebuildsGroup, err = s.store.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
106+
OrganizationID: preset.OrganizationID,
107+
Name: PrebuiltWorkspacesGroupName,
108+
})
109+
if err != nil {
110+
membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("get existing prebuilds group: %w", err))
111+
continue
112+
}
113+
}
114+
115+
// Add the system user to the prebuilds group
116+
err = s.store.InsertGroupMember(ctx, database.InsertGroupMemberParams{
117+
GroupID: prebuildsGroup.ID,
118+
UserID: userID,
74119
})
75120
if err != nil {
76-
membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("insert membership for prebuilt workspaces: %w", err))
77-
continue
121+
// Ignore unique violation errors as the user might already be in the group
122+
if !database.IsUniqueViolation(err) {
123+
membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("add system user to prebuilds group: %w", err))
124+
}
78125
}
79126
}
80127
return membershipInsertionErrors

0 commit comments

Comments
 (0)