mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: display the addon license UI (#22948)
<img width="1052" height="234" alt="Screenshot 2026-03-18 at 21 58 57" src="https://github.com/user-attachments/assets/136ccb1f-e47a-44fd-804d-859301161435" /> --------- Co-authored-by: Steven Masley <stevenmasley@gmail.com>
This commit is contained in:
co-authored by
Steven Masley
parent
89eaf6ad74
commit
6f244cddde
@@ -46,6 +46,12 @@ func Entitlements(
|
||||
return codersdk.Entitlements{}, xerrors.Errorf("query active user count: %w", err)
|
||||
}
|
||||
|
||||
// nolint:gocritic // Getting active AI seat count is a system function.
|
||||
activeAISeatCount, err := db.GetActiveAISeatCount(dbauthz.AsSystemRestricted(ctx))
|
||||
if err != nil {
|
||||
return codersdk.Entitlements{}, xerrors.Errorf("query active AI seat count: %w", err)
|
||||
}
|
||||
|
||||
// nolint:gocritic // Getting external templates is a system function.
|
||||
externalTemplates, err := db.GetTemplatesWithFilter(dbauthz.AsSystemRestricted(ctx), database.GetTemplatesWithFilterParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
@@ -59,6 +65,7 @@ func Entitlements(
|
||||
|
||||
entitlements, err := LicensesEntitlements(ctx, now, licenses, enablements, keys, FeatureArguments{
|
||||
ActiveUserCount: activeUserCount,
|
||||
ActiveAISeatCount: activeAISeatCount,
|
||||
ReplicaCount: replicaCount,
|
||||
ExternalAuthCount: externalAuthCount,
|
||||
ExternalTemplateCount: int64(len(externalTemplates)),
|
||||
@@ -88,6 +95,7 @@ func Entitlements(
|
||||
|
||||
type FeatureArguments struct {
|
||||
ActiveUserCount int64
|
||||
ActiveAISeatCount int64
|
||||
ReplicaCount int
|
||||
ExternalAuthCount int
|
||||
ExternalTemplateCount int64
|
||||
@@ -326,6 +334,9 @@ func LicensesEntitlements(
|
||||
if featureName == codersdk.FeatureUserLimit {
|
||||
actual = &featureArguments.ActiveUserCount
|
||||
}
|
||||
if featureName == codersdk.FeatureAIGovernanceUserLimit {
|
||||
actual = &featureArguments.ActiveAISeatCount
|
||||
}
|
||||
|
||||
entitlements.AddFeature(featureName, codersdk.Feature{
|
||||
Enabled: true,
|
||||
@@ -478,6 +489,24 @@ func LicensesEntitlements(
|
||||
"Your deployment has %d active users but the license with the limit %d is expired.",
|
||||
featureArguments.ActiveUserCount, *userLimit.Limit))
|
||||
}
|
||||
if featureArguments.ActiveAISeatCount > 0 {
|
||||
feature := entitlements.Features[codersdk.FeatureAIGovernanceUserLimit]
|
||||
switch {
|
||||
case feature.Entitlement == codersdk.EntitlementNotEntitled:
|
||||
// If the limit is not set
|
||||
entitlements.Errors = append(entitlements.Errors,
|
||||
fmt.Sprintf("Your deployment has %d active AI governance seats but the license is not entitled to this feature.", featureArguments.ActiveAISeatCount))
|
||||
case feature.Entitlement == codersdk.EntitlementGracePeriod && feature.Limit != nil:
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
fmt.Sprintf(
|
||||
"Your deployment has %d active AI governance seats but the license with the limit %d is expired.",
|
||||
featureArguments.ActiveAISeatCount, *feature.Limit))
|
||||
case feature.Limit != nil && featureArguments.ActiveAISeatCount > *feature.Limit:
|
||||
entitlements.Warnings = append(entitlements.Warnings, fmt.Sprintf(
|
||||
"Your deployment has %d active AI governance seats but is only licensed for %d.",
|
||||
featureArguments.ActiveAISeatCount, *feature.Limit))
|
||||
}
|
||||
}
|
||||
|
||||
// Add a warning for every feature that is enabled but not entitled or
|
||||
// is in a grace period.
|
||||
@@ -486,6 +515,9 @@ func LicensesEntitlements(
|
||||
if featureName == codersdk.FeatureUserLimit {
|
||||
continue
|
||||
}
|
||||
if featureName == codersdk.FeatureAIGovernanceUserLimit {
|
||||
continue
|
||||
}
|
||||
// High availability has it's own warnings based on replica count!
|
||||
if featureName == codersdk.FeatureHighAvailability {
|
||||
continue
|
||||
|
||||
@@ -851,6 +851,9 @@ func TestEntitlements(t *testing.T) {
|
||||
mDB.EXPECT().
|
||||
GetActiveUserCount(gomock.Any(), false).
|
||||
Return(int64(1), nil)
|
||||
mDB.EXPECT().
|
||||
GetActiveAISeatCount(gomock.Any()).
|
||||
Return(int64(27), nil)
|
||||
mDB.EXPECT().
|
||||
GetTotalUsageDCManagedAgentsV1(gomock.Any(), gomock.Cond(func(params database.GetTotalUsageDCManagedAgentsV1Params) bool {
|
||||
// gomock doesn't seem to compare times very nicely, so check
|
||||
@@ -885,10 +888,72 @@ func TestEntitlements(t *testing.T) {
|
||||
require.NotNil(t, managedAgentLimit.Actual)
|
||||
require.EqualValues(t, 175, *managedAgentLimit.Actual)
|
||||
|
||||
aiGovernanceSeatLimit, ok := entitlements.Features[codersdk.FeatureAIGovernanceUserLimit]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, aiGovernanceSeatLimit.Actual)
|
||||
require.EqualValues(t, 27, *aiGovernanceSeatLimit.Actual)
|
||||
require.NotNil(t, aiGovernanceSeatLimit.Limit)
|
||||
require.EqualValues(t, 100, *aiGovernanceSeatLimit.Limit)
|
||||
|
||||
// Usage exceeds the limit, so an exceeded warning should be present.
|
||||
require.Len(t, entitlements.Warnings, 1)
|
||||
require.Equal(t, codersdk.LicenseManagedAgentLimitExceededWarningText, entitlements.Warnings[0])
|
||||
})
|
||||
|
||||
t.Run("AIGovernanceSeatLimitExceededWarning", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
|
||||
licenseOpts := (&coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
NotBefore: dbtime.Now().Add(-time.Hour).Truncate(time.Second),
|
||||
GraceAt: dbtime.Now().Add(time.Hour * 24 * 60).Truncate(time.Second),
|
||||
ExpiresAt: dbtime.Now().Add(time.Hour * 24 * 90).Truncate(time.Second),
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
}).
|
||||
UserLimit(100)
|
||||
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
JWT: coderdenttest.GenerateLicense(t, *licenseOpts),
|
||||
Exp: licenseOpts.ExpiresAt,
|
||||
}
|
||||
|
||||
mDB.EXPECT().
|
||||
GetUnexpiredLicenses(gomock.Any()).
|
||||
Return([]database.License{lic}, nil)
|
||||
mDB.EXPECT().
|
||||
GetActiveUserCount(gomock.Any(), false).
|
||||
Return(int64(1), nil)
|
||||
mDB.EXPECT().
|
||||
GetActiveAISeatCount(gomock.Any()).
|
||||
Return(int64(127), nil)
|
||||
mDB.EXPECT().
|
||||
GetTotalUsageDCManagedAgentsV1(gomock.Any(), gomock.Any()).
|
||||
Return(int64(0), nil)
|
||||
mDB.EXPECT().
|
||||
GetTemplatesWithFilter(gomock.Any(), gomock.Any()).
|
||||
Return([]database.Template{}, nil)
|
||||
|
||||
entitlements, err := license.Entitlements(context.Background(), mDB, 1, 0, coderdenttest.Keys, all)
|
||||
require.NoError(t, err)
|
||||
require.True(t, entitlements.HasLicense)
|
||||
|
||||
aiGovernanceSeatLimit, ok := entitlements.Features[codersdk.FeatureAIGovernanceUserLimit]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, aiGovernanceSeatLimit.Actual)
|
||||
require.EqualValues(t, 127, *aiGovernanceSeatLimit.Actual)
|
||||
require.NotNil(t, aiGovernanceSeatLimit.Limit)
|
||||
require.EqualValues(t, 100, *aiGovernanceSeatLimit.Limit)
|
||||
|
||||
require.Len(t, entitlements.Warnings, 1)
|
||||
require.Equal(t, "Your deployment has 127 active AI governance seats but is only licensed for 100.", entitlements.Warnings[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestLicenseEntitlements(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user