Skip to content

Commit d998c77

Browse files
committed
chore: implement feature sets to licenses
Implement different sets of licensed features. Refactor the license logic to fix up some edge cases
1 parent aaf295b commit d998c77

File tree

3 files changed

+274
-104
lines changed

3 files changed

+274
-104
lines changed

codersdk/deployment.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ const (
3333
EntitlementNotEntitled Entitlement = "not_entitled"
3434
)
3535

36+
func CompareEntitlements(a, b Entitlement) int {
37+
return entitlementWeight(a) - entitlementWeight(b)
38+
}
39+
40+
func entitlementWeight(e Entitlement) int {
41+
switch e {
42+
case EntitlementEntitled:
43+
return 2
44+
case EntitlementGracePeriod:
45+
return 1
46+
case EntitlementNotEntitled:
47+
return 0
48+
default:
49+
return -1
50+
}
51+
}
52+
3653
// FeatureName represents the internal name of a feature.
3754
// To add a new feature, add it to this set of enums as well as the FeatureNames
3855
// array below.
@@ -105,6 +122,51 @@ func (n FeatureName) AlwaysEnable() bool {
105122
}[n]
106123
}
107124

125+
// FeatureSet represents a grouping of features. This is easier
126+
// than manually assigning features al-la-carte when making a license.
127+
// These sets are dynamic in the sense a feature can be added to an existing
128+
// set to grant an additional feature to an existing license.
129+
// If features were granted al-la-carte, we would need to reissue the license
130+
// to include the new feature.
131+
type FeatureSet string
132+
133+
const (
134+
FeatureSetNone FeatureSet = ""
135+
FeatureSetEnterprise FeatureSet = "enterprise"
136+
FeatureSetPremium FeatureSet = "premium"
137+
)
138+
139+
func (set FeatureSet) Features() []FeatureName {
140+
switch FeatureSet(strings.ToLower(string(set))) {
141+
case FeatureSetEnterprise:
142+
// List all features that should be included in the Enterprise feature set.
143+
return []FeatureName{
144+
FeatureUserLimit,
145+
FeatureAuditLog,
146+
FeatureBrowserOnly,
147+
FeatureSCIM,
148+
FeatureTemplateRBAC,
149+
FeatureHighAvailability,
150+
FeatureMultipleExternalAuth,
151+
FeatureExternalProvisionerDaemons,
152+
FeatureAppearance,
153+
FeatureAdvancedTemplateScheduling,
154+
FeatureWorkspaceProxy,
155+
FeatureUserRoleManagement,
156+
FeatureExternalTokenEncryption,
157+
FeatureWorkspaceBatchActions,
158+
FeatureAccessControl,
159+
FeatureControlSharedPorts,
160+
FeatureCustomRoles,
161+
}
162+
case FeatureSetPremium:
163+
// FeatureSetPremium is a superset of Enterprise
164+
return append(FeatureSetEnterprise.Features())
165+
}
166+
// By default, return an empty set.
167+
return []FeatureName{}
168+
}
169+
108170
type Feature struct {
109171
Entitlement Entitlement `json:"entitlement"`
110172
Enabled bool `json:"enabled"`
@@ -122,6 +184,56 @@ type Entitlements struct {
122184
RefreshedAt time.Time `json:"refreshed_at" format:"date-time"`
123185
}
124186

187+
// AddFeature will add the feature to the entitlements iff it expands
188+
// the set of features granted by the entitlements. If it does not, it will
189+
// be ignored and the existing feature with the same name will remain.
190+
//
191+
// All features should be added as atomic items, and not merged in any way.
192+
// Merging entitlements could lead to unexpected behavior, like a larger user
193+
// limit in grace period merging with a smaller one in a grace period. This could
194+
// lead to the larger limit being extended as "entitled", which is not correct.
195+
func (e *Entitlements) AddFeature(name FeatureName, add Feature) {
196+
existing, ok := e.Features[name]
197+
if !ok {
198+
e.Features[name] = add
199+
return
200+
}
201+
202+
comparison := CompareEntitlements(add.Entitlement, existing.Entitlement)
203+
// If the new entitlement is greater than the existing entitlement, replace it.
204+
// The edge case is if the previous entitlement is in a grace period with a
205+
// higher value.
206+
// TODO: Address the edge case.
207+
if comparison > 0 {
208+
e.Features[name] = add
209+
return
210+
}
211+
212+
// If they have the same entitlement, then we can compare the limits.
213+
if comparison == 0 {
214+
if add.Limit != nil {
215+
if existing.Limit == nil || *add.Limit > *existing.Limit {
216+
e.Features[name] = add
217+
return
218+
}
219+
}
220+
221+
// Enabled is better than disabled.
222+
if add.Enabled && !existing.Enabled {
223+
e.Features[name] = add
224+
return
225+
}
226+
227+
// If the actual value is greater than the existing actual value, replace it.
228+
if add.Actual != nil {
229+
if existing.Actual == nil || *add.Actual > *existing.Actual {
230+
e.Features[name] = add
231+
return
232+
}
233+
}
234+
}
235+
}
236+
125237
func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
126238
res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil)
127239
if err != nil {

0 commit comments

Comments
 (0)