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

Commit0036916

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 parentd998c77 commit0036916

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

@@ -174,6 +174,91 @@ type Feature struct {
174174
Actual*int64`json:"actual,omitempty"`
175175
}
176176

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

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.
287+
// Compare the features, keep the one that is "better"
288+
comparison:=CompareFeatures(add,existing)
207289
ifcomparison>0 {
208290
e.Features[name]=add
209291
return
210292
}
211-
212-
// If they have the same entitlement, then we can compare the limits.
213-
ifcomparison==0 {
214-
ifadd.Limit!=nil {
215-
ifexisting.Limit==nil||*add.Limit>*existing.Limit {
216-
e.Features[name]=add
217-
return
218-
}
219-
}
220-
221-
// Enabled is better than disabled.
222-
ifadd.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-
ifadd.Actual!=nil {
229-
ifexisting.Actual==nil||*add.Actual>*existing.Actual {
230-
e.Features[name]=add
231-
return
232-
}
233-
}
234-
}
235293
}
236294

237295
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