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:
Jaayden Halko
2026-03-20 16:34:17 +00:00
committed by GitHub
co-authored by Steven Masley
parent 89eaf6ad74
commit 6f244cddde
12 changed files with 977 additions and 134 deletions
+32
View File
@@ -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
+65
View File
@@ -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) {