diff --git a/enterprise/coderd/coderd_test.go b/enterprise/coderd/coderd_test.go index 0dad01620b..27aa2cb4c3 100644 --- a/enterprise/coderd/coderd_test.go +++ b/enterprise/coderd/coderd_test.go @@ -55,6 +55,7 @@ func TestEntitlements(t *testing.T) { codersdk.FeatureAdvancedTemplateScheduling: 1, codersdk.FeatureWorkspaceProxy: 1, }, + GraceAt: time.Now().Add(59 * 24 * time.Hour), }) res, err := client.Entitlements(context.Background()) require.NoError(t, err) diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index d29dad402e..fa2f1a9fcf 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ed25519" "fmt" + "math" "time" "github.com/golang-jwt/jwt/v4" @@ -70,6 +71,23 @@ func Entitlements( // LicenseExpires we must be in grace period. 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 { // Can this be negative? if featureValue <= 0 { diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go index 9cd56c6787..953a14c169 100644 --- a/enterprise/coderd/license/license_test.go +++ b/enterprise/coderd/license/license_test.go @@ -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()), ) }) + 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.Parallel() db := dbfake.New() @@ -164,16 +281,18 @@ func TestEntitlements(t *testing.T) { Features: license.Features{ 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{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ Features: license.Features{ 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) require.NoError(t, err) diff --git a/site/src/components/Dashboard/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx index 843cf77e04..55b8d00b55 100644 --- a/site/src/components/Dashboard/DashboardLayout.tsx +++ b/site/src/components/Dashboard/DashboardLayout.tsx @@ -25,10 +25,12 @@ export const DashboardLayout: FC = () => { }) const { error: updateCheckError, updateCheck } = updateCheckState.context + const canViewDeployment = Boolean(permissions.viewDeploymentValues) + return ( - + {canViewDeployment && }