Skip to content

Commit c3fe251

Browse files
rodrimaiamtojek
andauthored
feat: add license expiration warning (#7264)
* wip: add expiration warning * Use GraceAt * show expiration warning for trial accounts * fix test * only show license banner for users with deployment permission --------- Co-authored-by: Marcin Tojek <marcin@coder.com>
1 parent 3eb7f06 commit c3fe251

File tree

4 files changed

+143
-3
lines changed

4 files changed

+143
-3
lines changed

enterprise/coderd/coderd_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func TestEntitlements(t *testing.T) {
5555
codersdk.FeatureAdvancedTemplateScheduling: 1,
5656
codersdk.FeatureWorkspaceProxy: 1,
5757
},
58+
GraceAt: time.Now().Add(59 * 24 * time.Hour),
5859
})
5960
res, err := client.Entitlements(context.Background())
6061
require.NoError(t, err)

enterprise/coderd/license/license.go

+18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/ed25519"
66
"fmt"
7+
"math"
78
"time"
89

910
"github.com/golang-jwt/jwt/v4"
@@ -70,6 +71,23 @@ func Entitlements(
7071
// LicenseExpires we must be in grace period.
7172
entitlement = codersdk.EntitlementGracePeriod
7273
}
74+
75+
// Add warning if license is expiring soon
76+
daysToExpire := int(math.Ceil(claims.LicenseExpires.Sub(now).Hours() / 24))
77+
isTrial := entitlements.Trial
78+
showWarningDays := 30
79+
if isTrial {
80+
showWarningDays = 7
81+
}
82+
isExpiringSoon := daysToExpire > 0 && daysToExpire < showWarningDays
83+
if isExpiringSoon {
84+
day := "day"
85+
if daysToExpire > 1 {
86+
day = "days"
87+
}
88+
entitlements.Warnings = append(entitlements.Warnings, fmt.Sprintf("Your license expires in %d %s.", daysToExpire, day))
89+
}
90+
7391
for featureName, featureValue := range claims.Features {
7492
// Can this be negative?
7593
if featureValue <= 0 {

enterprise/coderd/license/license_test.go

+121-2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,123 @@ func TestEntitlements(t *testing.T) {
102102
fmt.Sprintf("%s is enabled but your license for this feature is expired.", codersdk.FeatureAuditLog.Humanize()),
103103
)
104104
})
105+
t.Run("Expiration warning", func(t *testing.T) {
106+
t.Parallel()
107+
db := dbfake.New()
108+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
109+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
110+
Features: license.Features{
111+
codersdk.FeatureUserLimit: 100,
112+
codersdk.FeatureAuditLog: 1,
113+
},
114+
115+
GraceAt: time.Now().AddDate(0, 0, 2),
116+
ExpiresAt: time.Now().AddDate(0, 0, 5),
117+
}),
118+
Exp: time.Now().AddDate(0, 0, 5),
119+
})
120+
121+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
122+
123+
require.NoError(t, err)
124+
require.True(t, entitlements.HasLicense)
125+
require.False(t, entitlements.Trial)
126+
127+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
128+
require.Contains(
129+
t, entitlements.Warnings,
130+
"Your license expires in 2 days.",
131+
)
132+
})
133+
134+
t.Run("Expiration warning for license expiring in 1 day", func(t *testing.T) {
135+
t.Parallel()
136+
db := dbfake.New()
137+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
138+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
139+
Features: license.Features{
140+
codersdk.FeatureUserLimit: 100,
141+
codersdk.FeatureAuditLog: 1,
142+
},
143+
144+
GraceAt: time.Now().AddDate(0, 0, 1),
145+
ExpiresAt: time.Now().AddDate(0, 0, 5),
146+
}),
147+
Exp: time.Now().AddDate(0, 0, 5),
148+
})
149+
150+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
151+
152+
require.NoError(t, err)
153+
require.True(t, entitlements.HasLicense)
154+
require.False(t, entitlements.Trial)
155+
156+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
157+
require.Contains(
158+
t, entitlements.Warnings,
159+
"Your license expires in 1 day.",
160+
)
161+
})
162+
163+
t.Run("Expiration warning for trials", func(t *testing.T) {
164+
t.Parallel()
165+
db := dbfake.New()
166+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
167+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
168+
Features: license.Features{
169+
codersdk.FeatureUserLimit: 100,
170+
codersdk.FeatureAuditLog: 1,
171+
},
172+
173+
Trial: true,
174+
GraceAt: time.Now().AddDate(0, 0, 8),
175+
ExpiresAt: time.Now().AddDate(0, 0, 5),
176+
}),
177+
Exp: time.Now().AddDate(0, 0, 5),
178+
})
179+
180+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
181+
182+
require.NoError(t, err)
183+
require.True(t, entitlements.HasLicense)
184+
require.True(t, entitlements.Trial)
185+
186+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
187+
require.NotContains( // it should not contain a warning since it is a trial license
188+
t, entitlements.Warnings,
189+
"Your license expires in 8 days.",
190+
)
191+
})
192+
193+
t.Run("Expiration warning for non trials", func(t *testing.T) {
194+
t.Parallel()
195+
db := dbfake.New()
196+
db.InsertLicense(context.Background(), database.InsertLicenseParams{
197+
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
198+
Features: license.Features{
199+
codersdk.FeatureUserLimit: 100,
200+
codersdk.FeatureAuditLog: 1,
201+
},
202+
203+
GraceAt: time.Now().AddDate(0, 0, 30),
204+
ExpiresAt: time.Now().AddDate(0, 0, 5),
205+
}),
206+
Exp: time.Now().AddDate(0, 0, 5),
207+
})
208+
209+
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
210+
211+
require.NoError(t, err)
212+
require.True(t, entitlements.HasLicense)
213+
require.False(t, entitlements.Trial)
214+
215+
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
216+
require.NotContains( // it should not contain a warning since it is a trial license
217+
t, entitlements.Warnings,
218+
"Your license expires in 30 days.",
219+
)
220+
})
221+
105222
t.Run("SingleLicenseNotEntitled", func(t *testing.T) {
106223
t.Parallel()
107224
db := dbfake.New()
@@ -164,16 +281,18 @@ func TestEntitlements(t *testing.T) {
164281
Features: license.Features{
165282
codersdk.FeatureUserLimit: 10,
166283
},
284+
GraceAt: time.Now().Add(59 * 24 * time.Hour),
167285
}),
168-
Exp: time.Now().Add(time.Hour),
286+
Exp: time.Now().Add(60 * 24 * time.Hour),
169287
})
170288
db.InsertLicense(context.Background(), database.InsertLicenseParams{
171289
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
172290
Features: license.Features{
173291
codersdk.FeatureUserLimit: 1,
174292
},
293+
GraceAt: time.Now().Add(59 * 24 * time.Hour),
175294
}),
176-
Exp: time.Now().Add(time.Hour),
295+
Exp: time.Now().Add(60 * 24 * time.Hour),
177296
})
178297
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty)
179298
require.NoError(t, err)

site/src/components/Dashboard/DashboardLayout.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ export const DashboardLayout: FC = () => {
2525
})
2626
const { error: updateCheckError, updateCheck } = updateCheckState.context
2727

28+
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
29+
2830
return (
2931
<DashboardProvider>
3032
<ServiceBanner />
31-
<LicenseBanner />
33+
{canViewDeployment && <LicenseBanner />}
3234

3335
<div className={styles.site}>
3436
<Navbar />

0 commit comments

Comments
 (0)