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

Commit26f7b66

Browse files
committed
feat: implement premium vs enterprise licenses
Refactor license logic to keep license features atomic (no merging).Refactor logic to be more unit testable.
1 parentcae444b commit26f7b66

File tree

5 files changed

+441
-40
lines changed

5 files changed

+441
-40
lines changed

‎codersdk/deployment.go‎

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ func entitlementWeight(e Entitlement) int {
4444
caseEntitlementGracePeriod:
4545
return1
4646
caseEntitlementNotEntitled:
47-
return0
48-
default:
4947
return-1
48+
default:
49+
return-2
5050
}
5151
}
5252

@@ -176,6 +176,91 @@ type Feature struct {
176176
Actual*int64`json:"actual,omitempty"`
177177
}
178178

179+
// CompareFeatures compares two features and returns an integer representing
180+
// if the first feature is greater than, equal to, or less than the second feature.
181+
// "Greater than" means the first feature has more functionality than the
182+
// second feature. It is assumed the features are for the same FeatureName.
183+
//
184+
// A feature is considered greater than another feature if:
185+
// - Graceful & capable > Entitled & not capable
186+
// - The entitlement is greater
187+
// - The limit is greater
188+
// - Enabled is greater than disabled
189+
// - The actual is greater
190+
funcCompareFeatures(a,bFeature)int {
191+
if!a.Capable()||!b.Capable() {
192+
// If either is incapable, then it is possible a grace period
193+
// feature can be "greater" than an entitled.
194+
// If either is "NotEntitled" then we can defer to a strict entitlement
195+
// check.
196+
ifentitlementWeight(a.Entitlement)>=0&&entitlementWeight(b.Entitlement)>=0 {
197+
ifa.Capable()&&!b.Capable() {
198+
return1
199+
}
200+
ifb.Capable()&&!a.Capable() {
201+
return-1
202+
}
203+
}
204+
}
205+
206+
entitlement:=CompareEntitlements(a.Entitlement,b.Entitlement)
207+
ifentitlement>0 {
208+
return1
209+
}
210+
ifentitlement<0 {
211+
return-1
212+
}
213+
214+
// If the entitlement is the same, then we can compare the limits.
215+
ifa.Limit==nil&&b.Limit!=nil {
216+
return-1
217+
}
218+
ifa.Limit!=nil&&b.Limit==nil {
219+
return1
220+
}
221+
ifa.Limit!=nil&&b.Limit!=nil {
222+
difference:=*a.Limit-*b.Limit
223+
if*a.Limit-*b.Limit!=0 {
224+
returnint(difference)
225+
}
226+
}
227+
228+
// Enabled is better than disabled.
229+
ifa.Enabled&&!b.Enabled {
230+
return1
231+
}
232+
if!a.Enabled&&b.Enabled {
233+
return-1
234+
}
235+
236+
// Higher actual is better
237+
ifa.Actual==nil&&b.Actual!=nil {
238+
return-1
239+
}
240+
ifa.Actual!=nil&&b.Actual==nil {
241+
return1
242+
}
243+
ifa.Actual!=nil&&b.Actual!=nil {
244+
difference:=*a.Actual-*b.Actual
245+
if*a.Actual-*b.Actual!=0 {
246+
returnint(difference)
247+
}
248+
}
249+
250+
return0
251+
}
252+
253+
// Capable is a helper function that returns if a given feature has a limit
254+
// that is greater than or equal to the actual.
255+
// If this condition is not true, then the feature is not capable of being used
256+
// since the limit is not high enough.
257+
func (fFeature)Capable()bool {
258+
iff.Limit!=nil&&f.Actual!=nil {
259+
return*f.Limit>=*f.Actual
260+
}
261+
returntrue
262+
}
263+
179264
typeEntitlementsstruct {
180265
Featuresmap[FeatureName]Feature`json:"features"`
181266
Warnings []string`json:"warnings"`
@@ -192,48 +277,21 @@ type Entitlements struct {
192277
//
193278
// All features should be added as atomic items, and not merged in any way.
194279
// Merging entitlements could lead to unexpected behavior, like a larger user
195-
// limit in grace period merging with a smaller one ina grace period. This could
196-
// lead to the larger limit being extended as "entitled", which is not correct.
280+
// limit in grace period merging with a smaller one inan "entitled" state. This
281+
//couldlead to the larger limit being extended as "entitled", which is not correct.
197282
func (e*Entitlements)AddFeature(nameFeatureName,addFeature) {
198283
existing,ok:=e.Features[name]
199284
if!ok {
200285
e.Features[name]=add
201286
return
202287
}
203288

204-
comparison:=CompareEntitlements(add.Entitlement,existing.Entitlement)
205-
// If the new entitlement is greater than the existing entitlement, replace it.
206-
// The edge case is if the previous entitlement is in a grace period with a
207-
// higher value.
208-
// TODO: Address the edge case.
289+
// Compare the features, keep the one that is "better"
290+
comparison:=CompareFeatures(add,existing)
209291
ifcomparison>0 {
210292
e.Features[name]=add
211293
return
212294
}
213-
214-
// If they have the same entitlement, then we can compare the limits.
215-
ifcomparison==0 {
216-
ifadd.Limit!=nil {
217-
ifexisting.Limit==nil||*add.Limit>*existing.Limit {
218-
e.Features[name]=add
219-
return
220-
}
221-
}
222-
223-
// Enabled is better than disabled.
224-
ifadd.Enabled&&!existing.Enabled {
225-
e.Features[name]=add
226-
return
227-
}
228-
229-
// If the actual value is greater than the existing actual value, replace it.
230-
ifadd.Actual!=nil {
231-
ifexisting.Actual==nil||*add.Actual>*existing.Actual {
232-
e.Features[name]=add
233-
return
234-
}
235-
}
236-
}
237295
}
238296

239297
func (c*Client)Entitlements(ctx context.Context) (Entitlements,error) {

‎enterprise/coderd/coderdenttest/coderdenttest.go‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,55 @@ func NewWithAPI(t *testing.T, options *Options) (
146146
returnclient,provisionerCloser,coderAPI,user
147147
}
148148

149+
// LicenseOptions is used to generate a license for testing.
150+
// It supports the builder pattern for easy customization.
149151
typeLicenseOptionsstruct {
150152
AccountTypestring
151153
AccountIDstring
152154
DeploymentIDs []string
153155
Trialbool
156+
FeatureSet codersdk.FeatureSet
154157
AllFeaturesbool
155-
GraceAt time.Time
156-
ExpiresAt time.Time
157-
Features license.Features
158+
// GraceAt is the time at which the license will enter the grace period.
159+
GraceAt time.Time
160+
// ExpiresAt is the time at which the license will hard expire.
161+
// ExpiresAt should always be greater then GraceAt.
162+
ExpiresAt time.Time
163+
Features license.Features
164+
}
165+
166+
func (opts*LicenseOptions)Expired(now time.Time)*LicenseOptions {
167+
opts.ExpiresAt=now.Add(time.Hour*24*-2)
168+
opts.GraceAt=now.Add(time.Hour*24*-3)
169+
returnopts
170+
}
171+
172+
func (opts*LicenseOptions)GracePeriod(now time.Time)*LicenseOptions {
173+
opts.ExpiresAt=now.Add(time.Hour*24)
174+
opts.GraceAt=now.Add(time.Hour*24*-1)
175+
returnopts
176+
}
177+
178+
func (opts*LicenseOptions)Valid(now time.Time)*LicenseOptions {
179+
opts.ExpiresAt=now.Add(time.Hour*24*60)
180+
opts.GraceAt=now.Add(time.Hour*24*53)
181+
returnopts
182+
}
183+
184+
func (opts*LicenseOptions)UserLimit(limitint64)*LicenseOptions {
185+
returnopts.Feature(codersdk.FeatureUserLimit,limit)
186+
}
187+
188+
func (opts*LicenseOptions)Feature(name codersdk.FeatureName,valueint64)*LicenseOptions {
189+
ifopts.Features==nil {
190+
opts.Features= license.Features{}
191+
}
192+
opts.Features[name]=value
193+
returnopts
194+
}
195+
196+
func (opts*LicenseOptions)Generate(t*testing.T)string {
197+
returnGenerateLicense(t,*opts)
158198
}
159199

160200
// AddFullLicense generates a license with all features enabled.
@@ -195,6 +235,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
195235
Trial:options.Trial,
196236
Version:license.CurrentVersion,
197237
AllFeatures:options.AllFeatures,
238+
FeatureSet:options.FeatureSet,
198239
Features:options.Features,
199240
}
200241
tok:=jwt.NewWithClaims(jwt.SigningMethodEdDSA,c)

‎enterprise/coderd/license/license.go‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ func LicensesEntitlements(
135135

136136
// Add all features from the feature set defined.
137137
for_,featureName:=rangeclaims.FeatureSet.Features() {
138+
iffeatureName==codersdk.FeatureUserLimit {
139+
// FeatureUserLimit is unique in that it must be specifically defined
140+
// in the license. There is no default meaning if no "limit" is set.
141+
continue
142+
}
138143
entitlements.AddFeature(featureName, codersdk.Feature{
139144
Entitlement:entitlement,
140145
Enabled:enablements[featureName]||featureName.AlwaysEnable(),
@@ -212,11 +217,15 @@ func LicensesEntitlements(
212217
}
213218

214219
ifentitlements.HasLicense {
215-
userLimit:=entitlements.Features[codersdk.FeatureUserLimit].Limit
216-
ifuserLimit!=nil&&featureArguments.ActiveUserCount>*userLimit {
220+
userLimit:=entitlements.Features[codersdk.FeatureUserLimit]
221+
ifuserLimit.Limit!=nil&&featureArguments.ActiveUserCount>*userLimit.Limit {
217222
entitlements.Warnings=append(entitlements.Warnings,fmt.Sprintf(
218223
"Your deployment has %d active users but is only licensed for %d.",
219-
featureArguments.ActiveUserCount,*userLimit))
224+
featureArguments.ActiveUserCount,*userLimit.Limit))
225+
}elseifuserLimit.Limit!=nil&&userLimit.Entitlement==codersdk.EntitlementGracePeriod {
226+
entitlements.Warnings=append(entitlements.Warnings,fmt.Sprintf(
227+
"Your deployment has %d active users but the license with the limit %d is expired.",
228+
featureArguments.ActiveUserCount,*userLimit.Limit))
220229
}
221230

222231
// Add a warning for every feature that is enabled but not entitled or
@@ -298,7 +307,7 @@ type Claims struct {
298307
FeatureSet codersdk.FeatureSet`json:"feature_set"`
299308
// AllFeatures represents 'FeatureSet = FeatureSetEnterprise'
300309
// Deprecated: AllFeatures is deprecated in favor of FeatureSet.
301-
AllFeaturesbool`json:"all_features"`
310+
AllFeaturesbool`json:"all_features,omitempty"`
302311
Versionuint64`json:"version"`
303312
FeaturesFeatures`json:"features"`
304313
RequireTelemetrybool`json:"require_telemetry,omitempty"`

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp