Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a6fc1b5
feat: add the Share mode API fields to workspace pools, claims, and m…
ThomasK33 Oct 31, 2026
5207783
tests: fake Coder groups, a sharing-settings write tripwire, and ACL …
ThomasK33 Oct 31, 2026
202b636
feat: share a Share claim's workspace with its Coder user within the …
ThomasK33 Oct 31, 2026
bdcbcdc
docs: describe Share mode for workspace pools (#153 PR 14)
ThomasK33 Oct 31, 2026
76d855e
test: Kind phase P9 shares a workspace with a second user, and offlin…
ThomasK33 Oct 31, 2026
e1fe6d8
docs: a Share claim is Ready only while Coder shows the share
ThomasK33 Oct 31, 2026
afe0349
fix: remove a Share claim's share when the share policy no longer all…
ThomasK33 Oct 31, 2026
c11a4bf
test: P9 checks that withdrawn shareGroupIDs remove the share in Coder
ThomasK33 Oct 31, 2026
a3aa7bd
fix: check Share claims against the control plane without the cache, …
ThomasK33 Oct 31, 2026
53b58f9
fix: bound the Coder reads that refused Share claims cause (#153 PR 14)
ThomasK33 Oct 31, 2026
bae362c
test: Coder's 429 for one refused Share claim ends the user reads of …
ThomasK33 Oct 31, 2026
eb89c36
docs: refused Share claims and the claim quota
ThomasK33 Oct 31, 2026
a32d876
fix: Share mode keeps the policy on a changed role and drops demand w…
ThomasK33 Oct 31, 2026
851db2c
docs: shares stay while a control plane is being deleted
ThomasK33 Oct 31, 2026
58c21ec
tests: a shared member reads the ACL at every poll
ThomasK33 Nov 1, 2026
2924f36
docs: a removed share lasts until the workspace restarts or stops, an…
ThomasK33 Nov 1, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -683,9 +683,9 @@ jobs:
kubectl -n coder delete secret coder-tls
kubectl -n coder delete configmap coder-ca

# CoderWorkspacePool phases P1-P8 (#153). It runs before the lifecycle driver, which deletes namespace coder at
# CoderWorkspacePool phases P1-P9 (#153). It runs before the lifecycle driver, which deletes namespace coder at
# its end, imports its own template e2e-pool-agent from the agent fixture, and removes its objects when it passes.
- name: Workspace pool (create, scale, claim, operator restart, use, deletes)
- name: Workspace pool (create, scale, claim, operator restart, use, deletes, share)
if: env.E2E_FULL == 'true'
env:
E2E_WORKDIR: ${{ runner.temp }}/workspace-pool
Expand Down
28 changes: 26 additions & 2 deletions api/v1alpha1/coderworkspaceclaim_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const (
// a claim never binds again.
CoderWorkspaceClaimPhaseLost = "Lost"

// CoderWorkspaceClaimConditionReady is True when the claim is bound and
// its workspace is ready.
// CoderWorkspaceClaimConditionReady is True when the claim is bound, its
// workspace is ready, and, with spec.share, shared.
CoderWorkspaceClaimConditionReady = "Ready"
// CoderWorkspaceClaimConditionReconciling is True while the claim waits.
CoderWorkspaceClaimConditionReconciling = "Reconciling"
Expand All @@ -33,6 +33,22 @@ type CoderWorkspaceClaimSpec struct {
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
PoolName string `json:"poolName"`
// Share names the Coder user that gets the workspace. Required when the
// pool's claims.mode is Share, refused otherwise: such a claim never binds
// and is Stalled with ClaimModeMismatch.
// +optional
Share *CoderWorkspaceClaimShare `json:"share,omitempty"`
}

// CoderWorkspaceClaimShare names the Coder user of a Share claim.
type CoderWorkspaceClaimShare struct {
// User is a Coder username or user UUID (lower case with hyphens).
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=36
// +kubebuilder:validation:Pattern=`^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$`
// +kubebuilder:validation:XValidation:rule="self.size() <= 32 || self.matches('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')",message="user must be a Coder username of at most 32 characters or a lower-case UUID"
// +kubebuilder:validation:XValidation:rule="self != 'c42fdf75-3097-471c-8c33-fb52454d81c0'",message="user must not be the Coder prebuilds system user"
User string `json:"user"`
}

// CoderWorkspaceClaimStatus defines the observed state of a
Expand Down Expand Up @@ -60,6 +76,14 @@ type CoderWorkspaceClaimStatus struct {
// BoundTime is when a member took the claim.
// +optional
BoundTime *metav1.Time `json:"boundTime,omitempty"`
// SharedUserID is the Coder user that has a role on the workspace
// through this claim, as the member last read it from Coder. The
// controller grants `use`.
// +kubebuilder:validation:MaxLength=36
// +kubebuilder:validation:Pattern=`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`
// +kubebuilder:validation:XValidation:rule="self != 'c42fdf75-3097-471c-8c33-fb52454d81c0'",message="sharedUserID must not be the Coder prebuilds system user"
// +optional
SharedUserID string `json:"sharedUserID,omitempty"`
// Reason is the current wait reason or the reason for Lost.
// +optional
Reason string `json:"reason,omitempty"`
Expand Down
23 changes: 21 additions & 2 deletions api/v1alpha1/coderworkspacepool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const (
// The consumer connects with credentials it already holds for the pool
// owner.
CoderWorkspacePoolClaimModeReserve = "Reserve"
// CoderWorkspacePoolClaimModeShare binds a ready workspace to a claim and
// gives the claim's spec.share.user the Coder `use` role on it.
CoderWorkspacePoolClaimModeShare = "Share"

// CoderWorkspacePoolSelectorLabel is the label key of the pool's
// status.selector: <key>=<pool UID>.
Expand Down Expand Up @@ -76,6 +79,19 @@ type WorkspacePoolOwner struct {
// +kubebuilder:validation:Maximum=100
// +optional
MaxWorkspaces *int32 `json:"maxWorkspaces,omitempty"`
// ShareGroupIDs turn on Share mode for this owner: they are the Coder
// groups whose members a Share claim can name. Empty means Share mode is
// off for this owner. The template organization's ID names its Everyone
// group: every active member of that organization, without a license.
// Other groups need a Premium license (Coder answers 403 without one, and
// the share is refused). Every claimant can act as the pool owner if the
// template exposes the owner's session token (see the how-to).
// +kubebuilder:validation:MaxItems=16
// +kubebuilder:validation:items:MaxLength=36
// +kubebuilder:validation:items:Pattern=`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`
// +listType=set
// +optional
ShareGroupIDs []string `json:"shareGroupIDs,omitempty"`
}

// CoderWorkspacePoolVersion selects the template version of new members. Set
Expand Down Expand Up @@ -117,8 +133,11 @@ type CoderWorkspacePoolParameter struct {
// CoderWorkspacePoolClaims configures how the pool hands out workspaces.
type CoderWorkspacePoolClaims struct {
// Mode is how a claim gets its workspace. Reserve binds a ready workspace
// to the claim, and the workspace stays with the pool owner.
// +kubebuilder:validation:Enum=Reserve
// to the claim, and the workspace stays with the pool owner. Share also
// gives the claim's spec.share.user the Coder `use` role on the workspace
// (open apps, SSH, start, stop; never delete), where the organization's
// sharing setting and the owner's shareGroupIDs allow it.
// +kubebuilder:validation:Enum=Reserve;Share
// +kubebuilder:default=Reserve
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="claims.mode is immutable"
// +optional
Expand Down
22 changes: 21 additions & 1 deletion api/v1alpha1/coderworkspacepoolmember_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ const (
// CoderWorkspacePoolMemberConditionWorkspaceDeleted reports whether the
// member's workspace is gone, with the CoderTemplateTest reasons.
CoderWorkspacePoolMemberConditionWorkspaceDeleted = "WorkspaceDeleted"
// CoderWorkspacePoolMemberConditionShared is True while Coder's ACL gives
// status.sharedUserID a role on the workspace of a Share claim (use, or
// one that a person set later) and the share policy allows it. Reason
// ShareRefused: the policy or Coder refused the share. ShareRevoked: the
// policy no longer allowed the share, and the controller removed it
// (ShareRevoking until that worked).
// ShareRemoved: someone else removed the share. The controller does not
// share again after ShareRevoked or ShareRemoved.
CoderWorkspacePoolMemberConditionShared = "Shared"

// CoderWorkspacePoolMemberCleanupFinalizer keeps a member until its
// workspace is deleted.
Expand Down Expand Up @@ -177,6 +186,16 @@ type CoderWorkspacePoolMemberStatus struct {
// that the extension happened.
// +optional
BindExtendDeadline *metav1.Time `json:"bindExtendDeadline,omitempty"`
// SharedUserID is the Coder user that the controller shares the
// workspace with for a Share claim. The controller stores it before its
// first ACL write and never changes it, so a renamed or reused username
// never moves the share to another user. The Shared condition reports
// whether Coder's ACL gives this user a role.
// +kubebuilder:validation:MaxLength=36
// +kubebuilder:validation:Pattern=`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`
// +kubebuilder:validation:XValidation:rule="self != 'c42fdf75-3097-471c-8c33-fb52454d81c0'",message="sharedUserID must not be the Coder prebuilds system user"
// +optional
SharedUserID string `json:"sharedUserID,omitempty"`
// DeleteBuildID is the ID of the current delete build.
// +optional
DeleteBuildID string `json:"deleteBuildID,omitempty"`
Expand All @@ -190,7 +209,8 @@ type CoderWorkspacePoolMemberStatus struct {
// logs, parameter values, or tokens.
// +optional
Message string `json:"message,omitempty"`
// Conditions are Ready, Used, and WorkspaceDeleted.
// Conditions are Ready, Used, WorkspaceDeleted, and, for a Share claim,
// Shared.
// +listType=map
// +listMapKey=type
// +optional
Expand Down
28 changes: 27 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

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

16 changes: 16 additions & 0 deletions config/crd/bases/coder.com_codercontrolplanes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4390,6 +4390,22 @@ spec:
maximum: 100
minimum: 1
type: integer
shareGroupIDs:
description: |-
ShareGroupIDs turn on Share mode for this owner: they are the Coder
groups whose members a Share claim can name. Empty means Share mode is
off for this owner. The template organization's ID names its Everyone
group: every active member of that organization, without a license.
Other groups need a Premium license (Coder answers 403 without one, and
the share is refused). Every claimant can act as the pool owner if the
template exposes the owner's session token (see the how-to).
items:
maxLength: 36
pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
type: string
maxItems: 16
type: array
x-kubernetes-list-type: set
templates:
description: |-
Templates are the only Coder templates, as `<organization>.<template>`,
Expand Down
33 changes: 33 additions & 0 deletions config/crd/bases/coder.com_coderworkspaceclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ spec:
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
share:
description: |-
Share names the Coder user that gets the workspace. Required when the
pool's claims.mode is Share, refused otherwise: such a claim never binds
and is Stalled with ClaimModeMismatch.
properties:
user:
description: User is a Coder username or user UUID (lower case
with hyphens).
maxLength: 36
minLength: 1
pattern: ^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$
type: string
x-kubernetes-validations:
- message: user must be a Coder username of at most 32 characters
or a lower-case UUID
rule: self.size() <= 32 || self.matches('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')
- message: user must not be the Coder prebuilds system user
rule: self != 'c42fdf75-3097-471c-8c33-fb52454d81c0'
required:
- user
type: object
required:
- poolName
type: object
Expand Down Expand Up @@ -169,6 +191,17 @@ spec:
reason:
description: Reason is the current wait reason or the reason for Lost.
type: string
sharedUserID:
description: |-
SharedUserID is the Coder user that has a role on the workspace
through this claim, as the member last read it from Coder. The
controller grants `use`.
maxLength: 36
pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
type: string
x-kubernetes-validations:
- message: sharedUserID must not be the Coder prebuilds system user
rule: self != 'c42fdf75-3097-471c-8c33-fb52454d81c0'
workspaceID:
description: WorkspaceID is the Coder workspace ID.
type: string
Expand Down
17 changes: 16 additions & 1 deletion config/crd/bases/coder.com_coderworkspacepoolmembers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@ spec:
at bind.
type: boolean
conditions:
description: Conditions are Ready, Used, and WorkspaceDeleted.
description: |-
Conditions are Ready, Used, WorkspaceDeleted, and, for a Share claim,
Shared.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
Expand Down Expand Up @@ -347,6 +349,19 @@ spec:
reason:
description: Reason is the current wait reason or the failure reason.
type: string
sharedUserID:
description: |-
SharedUserID is the Coder user that the controller shares the
workspace with for a Share claim. The controller stores it before its
first ACL write and never changes it, so a renamed or reused username
never moves the share to another user. The Shared condition reports
whether Coder's ACL gives this user a role.
maxLength: 36
pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
type: string
x-kubernetes-validations:
- message: sharedUserID must not be the Coder prebuilds system user
rule: self != 'c42fdf75-3097-471c-8c33-fb52454d81c0'
startBuildID:
description: StartBuildID is the ID of the start build that the controller
requested.
Expand Down
6 changes: 5 additions & 1 deletion config/crd/bases/coder.com_coderworkspacepools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,13 @@ spec:
default: Reserve
description: |-
Mode is how a claim gets its workspace. Reserve binds a ready workspace
to the claim, and the workspace stays with the pool owner.
to the claim, and the workspace stays with the pool owner. Share also
gives the claim's spec.share.user the Coder `use` role on the workspace
(open apps, SSH, start, stop; never delete), where the organization's
sharing setting and the owner's shareGroupIDs allow it.
enum:
- Reserve
- Share
type: string
x-kubernetes-validations:
- message: claims.mode is immutable
Expand Down
Loading
Loading