mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
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>
This commit is contained in:
@ -55,6 +55,7 @@ func TestEntitlements(t *testing.T) {
|
|||||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||||
codersdk.FeatureWorkspaceProxy: 1,
|
codersdk.FeatureWorkspaceProxy: 1,
|
||||||
},
|
},
|
||||||
|
GraceAt: time.Now().Add(59 * 24 * time.Hour),
|
||||||
})
|
})
|
||||||
res, err := client.Entitlements(context.Background())
|
res, err := client.Entitlements(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
@ -70,6 +71,23 @@ func Entitlements(
|
|||||||
// LicenseExpires we must be in grace period.
|
// LicenseExpires we must be in grace period.
|
||||||
entitlement = codersdk.EntitlementGracePeriod
|
entitlement = codersdk.EntitlementGracePeriod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add warning if license is expiring soon
|
||||||
|
daysToExpire := int(math.Ceil(claims.LicenseExpires.Sub(now).Hours() / 24))
|
||||||
|
isTrial := entitlements.Trial
|
||||||
|
showWarningDays := 30
|
||||||
|
if isTrial {
|
||||||
|
showWarningDays = 7
|
||||||
|
}
|
||||||
|
isExpiringSoon := daysToExpire > 0 && daysToExpire < showWarningDays
|
||||||
|
if isExpiringSoon {
|
||||||
|
day := "day"
|
||||||
|
if daysToExpire > 1 {
|
||||||
|
day = "days"
|
||||||
|
}
|
||||||
|
entitlements.Warnings = append(entitlements.Warnings, fmt.Sprintf("Your license expires in %d %s.", daysToExpire, day))
|
||||||
|
}
|
||||||
|
|
||||||
for featureName, featureValue := range claims.Features {
|
for featureName, featureValue := range claims.Features {
|
||||||
// Can this be negative?
|
// Can this be negative?
|
||||||
if featureValue <= 0 {
|
if featureValue <= 0 {
|
||||||
|
@ -102,6 +102,123 @@ func TestEntitlements(t *testing.T) {
|
|||||||
fmt.Sprintf("%s is enabled but your license for this feature is expired.", codersdk.FeatureAuditLog.Humanize()),
|
fmt.Sprintf("%s is enabled but your license for this feature is expired.", codersdk.FeatureAuditLog.Humanize()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
t.Run("Expiration warning", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
db := dbfake.New()
|
||||||
|
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureUserLimit: 100,
|
||||||
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
GraceAt: time.Now().AddDate(0, 0, 2),
|
||||||
|
ExpiresAt: time.Now().AddDate(0, 0, 5),
|
||||||
|
}),
|
||||||
|
Exp: time.Now().AddDate(0, 0, 5),
|
||||||
|
})
|
||||||
|
|
||||||
|
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, entitlements.HasLicense)
|
||||||
|
require.False(t, entitlements.Trial)
|
||||||
|
|
||||||
|
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
|
||||||
|
require.Contains(
|
||||||
|
t, entitlements.Warnings,
|
||||||
|
"Your license expires in 2 days.",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Expiration warning for license expiring in 1 day", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
db := dbfake.New()
|
||||||
|
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureUserLimit: 100,
|
||||||
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
GraceAt: time.Now().AddDate(0, 0, 1),
|
||||||
|
ExpiresAt: time.Now().AddDate(0, 0, 5),
|
||||||
|
}),
|
||||||
|
Exp: time.Now().AddDate(0, 0, 5),
|
||||||
|
})
|
||||||
|
|
||||||
|
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, entitlements.HasLicense)
|
||||||
|
require.False(t, entitlements.Trial)
|
||||||
|
|
||||||
|
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
|
||||||
|
require.Contains(
|
||||||
|
t, entitlements.Warnings,
|
||||||
|
"Your license expires in 1 day.",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Expiration warning for trials", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
db := dbfake.New()
|
||||||
|
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureUserLimit: 100,
|
||||||
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
Trial: true,
|
||||||
|
GraceAt: time.Now().AddDate(0, 0, 8),
|
||||||
|
ExpiresAt: time.Now().AddDate(0, 0, 5),
|
||||||
|
}),
|
||||||
|
Exp: time.Now().AddDate(0, 0, 5),
|
||||||
|
})
|
||||||
|
|
||||||
|
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, entitlements.HasLicense)
|
||||||
|
require.True(t, entitlements.Trial)
|
||||||
|
|
||||||
|
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
|
||||||
|
require.NotContains( // it should not contain a warning since it is a trial license
|
||||||
|
t, entitlements.Warnings,
|
||||||
|
"Your license expires in 8 days.",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Expiration warning for non trials", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
db := dbfake.New()
|
||||||
|
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureUserLimit: 100,
|
||||||
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
GraceAt: time.Now().AddDate(0, 0, 30),
|
||||||
|
ExpiresAt: time.Now().AddDate(0, 0, 5),
|
||||||
|
}),
|
||||||
|
Exp: time.Now().AddDate(0, 0, 5),
|
||||||
|
})
|
||||||
|
|
||||||
|
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, entitlements.HasLicense)
|
||||||
|
require.False(t, entitlements.Trial)
|
||||||
|
|
||||||
|
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
|
||||||
|
require.NotContains( // it should not contain a warning since it is a trial license
|
||||||
|
t, entitlements.Warnings,
|
||||||
|
"Your license expires in 30 days.",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("SingleLicenseNotEntitled", func(t *testing.T) {
|
t.Run("SingleLicenseNotEntitled", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
db := dbfake.New()
|
db := dbfake.New()
|
||||||
@ -164,16 +281,18 @@ func TestEntitlements(t *testing.T) {
|
|||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 10,
|
codersdk.FeatureUserLimit: 10,
|
||||||
},
|
},
|
||||||
|
GraceAt: time.Now().Add(59 * 24 * time.Hour),
|
||||||
}),
|
}),
|
||||||
Exp: time.Now().Add(time.Hour),
|
Exp: time.Now().Add(60 * 24 * time.Hour),
|
||||||
})
|
})
|
||||||
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 1,
|
codersdk.FeatureUserLimit: 1,
|
||||||
},
|
},
|
||||||
|
GraceAt: time.Now().Add(59 * 24 * time.Hour),
|
||||||
}),
|
}),
|
||||||
Exp: time.Now().Add(time.Hour),
|
Exp: time.Now().Add(60 * 24 * time.Hour),
|
||||||
})
|
})
|
||||||
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty)
|
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -25,10 +25,12 @@ export const DashboardLayout: FC = () => {
|
|||||||
})
|
})
|
||||||
const { error: updateCheckError, updateCheck } = updateCheckState.context
|
const { error: updateCheckError, updateCheck } = updateCheckState.context
|
||||||
|
|
||||||
|
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<ServiceBanner />
|
<ServiceBanner />
|
||||||
<LicenseBanner />
|
{canViewDeployment && <LicenseBanner />}
|
||||||
|
|
||||||
<div className={styles.site}>
|
<div className={styles.site}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
Reference in New Issue
Block a user