mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: extend premium license for aigovernance (#21499)
Closes [#1227](https://github.com/coder/internal/issues/1227) Added support for license addons, starting with AI Governance, to enable dynamic feature grouping without requiring license reissuance. ### What changed? - Introduced a new `Addon` type to represent groupings of features that can be added to licenses - Created the first addon `AddonAIGovernance` which includes AI Bridge and Boundary features - Added validation for addon dependencies to ensure required features are present - Added new features: `FeatureBoundary` and `FeatureAIGovernanceUserLimit` - Updated license entitlement logic to handle addons and their features - Added helper methods to check if features belong to addons - Updated tests to verify addon functionality ### Why make this change? This change introduces a more flexible licensing model that allows features to be grouped into addons that can be added to licenses without requiring reissuance when new features are added to an addon. This is particularly useful for specialized feature sets like AI Governance, where related features can be bundled together and sold as a separate SKU. The addon approach allows for better organization of features and more granular control over entitlements.
This commit is contained in:
+128
-2
@@ -57,6 +57,111 @@ func (e Entitlement) Weight() int {
|
||||
}
|
||||
}
|
||||
|
||||
// Addon represents a grouping of features used for additional license SKUs.
|
||||
// It is complementary to FeatureSet and similar in implementation, allowing
|
||||
// features to be grouped together dynamically. Unlike FeatureSet, licenses
|
||||
// can have multiple addons. This also means that entitlements don't require
|
||||
// reissuing when new features are added to an addon.
|
||||
type Addon string
|
||||
|
||||
const (
|
||||
AddonAIGovernance Addon = "ai_governance"
|
||||
)
|
||||
|
||||
var (
|
||||
// AddonsNames must be kept in-sync with the Addon enum above.
|
||||
AddonsNames = []Addon{
|
||||
AddonAIGovernance,
|
||||
}
|
||||
|
||||
// AddonsMap is a map of all addon names for quick lookups.
|
||||
AddonsMap = func() map[Addon]struct{} {
|
||||
addonsMap := make(map[Addon]struct{}, len(AddonsNames))
|
||||
for _, addon := range AddonsNames {
|
||||
addonsMap[addon] = struct{}{}
|
||||
}
|
||||
return addonsMap
|
||||
}()
|
||||
)
|
||||
|
||||
// Features returns all the features that are part of the addon.
|
||||
func (a Addon) Features() []FeatureName {
|
||||
switch a {
|
||||
case AddonAIGovernance:
|
||||
// Return all AI governance features.
|
||||
var features []FeatureName
|
||||
for _, featureName := range FeatureNames {
|
||||
if featureName.IsAIGovernanceAddon() {
|
||||
features = append(features, featureName)
|
||||
}
|
||||
}
|
||||
return features
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateDependencies validates the dependencies of the addon
|
||||
// and returns a list of errors for the missing dependencies.
|
||||
func (a Addon) ValidateDependencies(features map[FeatureName]Feature) []string {
|
||||
errors := []string{}
|
||||
|
||||
// Candidate for a switch statement once we have more addons.
|
||||
if a == AddonAIGovernance {
|
||||
requiredFeatures := []FeatureName{
|
||||
FeatureAIGovernanceUserLimit,
|
||||
}
|
||||
|
||||
for _, featureName := range requiredFeatures {
|
||||
feature, ok := features[featureName]
|
||||
if !ok {
|
||||
errors = append(errors,
|
||||
fmt.Sprintf(
|
||||
"Feature %s must be set when using the %s addon.",
|
||||
featureName.Humanize(),
|
||||
a.Humanize(),
|
||||
),
|
||||
)
|
||||
continue
|
||||
}
|
||||
// For limit features, check if the Limit is set (not nil).
|
||||
// For usage period features, check if the Limit is set.
|
||||
if featureName.UsesLimit() || featureName.UsesUsagePeriod() {
|
||||
if feature.Limit == nil {
|
||||
errors = append(errors,
|
||||
fmt.Sprintf(
|
||||
"Feature %s must be set when using the %s addon.",
|
||||
featureName.Humanize(),
|
||||
a.Humanize(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if feature.Entitlement == EntitlementNotEntitled {
|
||||
// For non-limit features, check if the feature is entitled.
|
||||
errors = append(errors,
|
||||
fmt.Sprintf(
|
||||
"Feature %s must be set when using the %s addon.",
|
||||
featureName.Humanize(),
|
||||
a.Humanize(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// Humanize returns the addon name in a human-readable format.
|
||||
func (a Addon) Humanize() string {
|
||||
switch a {
|
||||
case AddonAIGovernance:
|
||||
return "AI Governance"
|
||||
default:
|
||||
return strings.Title(strings.ReplaceAll(string(a), "_", " "))
|
||||
}
|
||||
}
|
||||
|
||||
// FeatureName represents the internal name of a feature.
|
||||
// To add a new feature, add it to this set of enums as well as the FeatureNames
|
||||
// array below.
|
||||
@@ -91,6 +196,7 @@ const (
|
||||
FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent"
|
||||
FeatureAIBridge FeatureName = "aibridge"
|
||||
FeatureBoundary FeatureName = "boundary"
|
||||
FeatureAIGovernanceUserLimit FeatureName = "ai_governance_user_limit"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -121,6 +227,7 @@ var (
|
||||
FeatureWorkspaceExternalAgent,
|
||||
FeatureAIBridge,
|
||||
FeatureBoundary,
|
||||
FeatureAIGovernanceUserLimit,
|
||||
}
|
||||
|
||||
// FeatureNamesMap is a map of all feature names for quick lookups.
|
||||
@@ -142,6 +249,8 @@ func (n FeatureName) Humanize() string {
|
||||
return "SCIM"
|
||||
case FeatureAIBridge:
|
||||
return "AI Bridge"
|
||||
case FeatureAIGovernanceUserLimit:
|
||||
return "AI Governance User Limit"
|
||||
default:
|
||||
return strings.Title(strings.ReplaceAll(string(n), "_", " "))
|
||||
}
|
||||
@@ -184,8 +293,9 @@ func (n FeatureName) Enterprise() bool {
|
||||
// be included in any feature sets (as they are not boolean features).
|
||||
func (n FeatureName) UsesLimit() bool {
|
||||
return map[FeatureName]bool{
|
||||
FeatureUserLimit: true,
|
||||
FeatureManagedAgentLimit: true,
|
||||
FeatureUserLimit: true,
|
||||
FeatureManagedAgentLimit: true,
|
||||
FeatureAIGovernanceUserLimit: true,
|
||||
}[n]
|
||||
}
|
||||
|
||||
@@ -196,6 +306,20 @@ func (n FeatureName) UsesUsagePeriod() bool {
|
||||
}[n]
|
||||
}
|
||||
|
||||
// IsAIGovernanceAddon returns true if the feature is an AI governance addon feature.
|
||||
func (n FeatureName) IsAIGovernanceAddon() bool {
|
||||
return n == FeatureAIBridge || n == FeatureBoundary
|
||||
}
|
||||
|
||||
// IsAddon returns true if the feature is an addon feature.
|
||||
func (n FeatureName) IsAddonFeature() bool {
|
||||
features := []FeatureName{}
|
||||
for addon := range AddonsMap {
|
||||
features = append(features, addon.Features()...)
|
||||
}
|
||||
return slices.Contains(features, n)
|
||||
}
|
||||
|
||||
// FeatureSet represents a grouping of features. Rather than manually
|
||||
// assigning features al-la-carte when making a license, a set can be specified.
|
||||
// Sets are dynamic in the sense a feature can be added to a set, granting the
|
||||
@@ -220,6 +344,7 @@ func (set FeatureSet) Features() []FeatureName {
|
||||
copy(enterpriseFeatures, FeatureNames)
|
||||
// Remove the selection
|
||||
enterpriseFeatures = slices.DeleteFunc(enterpriseFeatures, func(f FeatureName) bool {
|
||||
// TODO: In future release, restore the f.IsAddonFeature() check.
|
||||
return !f.Enterprise() || f.UsesLimit()
|
||||
})
|
||||
|
||||
@@ -229,6 +354,7 @@ func (set FeatureSet) Features() []FeatureName {
|
||||
copy(premiumFeatures, FeatureNames)
|
||||
// Remove the selection
|
||||
premiumFeatures = slices.DeleteFunc(premiumFeatures, func(f FeatureName) bool {
|
||||
// TODO: In future release, restore the f.IsAddonFeature() check.
|
||||
return f.UsesLimit()
|
||||
})
|
||||
// FeatureSetPremium is just all features.
|
||||
|
||||
@@ -623,7 +623,8 @@ func TestPremiumSuperSet(t *testing.T) {
|
||||
// Premium ⊃ Enterprise
|
||||
require.Subset(t, premium.Features(), enterprise.Features(), "premium should be a superset of enterprise. If this fails, update the premium feature set to include all enterprise features.")
|
||||
|
||||
// Premium = All Features EXCEPT usage limit features
|
||||
// Premium = All Features EXCEPT limit-based features.
|
||||
// TODO: In future release, also exclude addon features (f.IsAddonFeature()).
|
||||
expectedPremiumFeatures := []codersdk.FeatureName{}
|
||||
for _, feature := range codersdk.FeatureNames {
|
||||
if feature.UsesLimit() {
|
||||
|
||||
@@ -185,6 +185,7 @@ type LicenseOptions struct {
|
||||
// past.
|
||||
IssuedAt time.Time
|
||||
Features license.Features
|
||||
Addons []codersdk.Addon
|
||||
|
||||
AllowEmpty bool
|
||||
}
|
||||
@@ -225,6 +226,11 @@ func (opts *LicenseOptions) UserLimit(limit int64) *LicenseOptions {
|
||||
return opts.Feature(codersdk.FeatureUserLimit, limit)
|
||||
}
|
||||
|
||||
func (opts *LicenseOptions) AIGovernanceAddon(limit int64) *LicenseOptions {
|
||||
opts.Addons = append(opts.Addons, codersdk.AddonAIGovernance)
|
||||
return opts.Feature(codersdk.FeatureAIGovernanceUserLimit, limit)
|
||||
}
|
||||
|
||||
func (opts *LicenseOptions) ManagedAgentLimit(soft int64, hard int64) *LicenseOptions {
|
||||
// These don't use named or exported feature names, see
|
||||
// enterprise/coderd/license/license.go.
|
||||
@@ -301,6 +307,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
|
||||
AllFeatures: options.AllFeatures,
|
||||
FeatureSet: options.FeatureSet,
|
||||
Features: options.Features,
|
||||
Addons: options.Addons,
|
||||
PublishUsageData: options.PublishUsageData,
|
||||
}
|
||||
return GenerateLicenseRaw(t, c)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
@@ -298,11 +299,19 @@ func LicensesEntitlements(
|
||||
})
|
||||
}
|
||||
|
||||
// Add all features from the feature set defined.
|
||||
// TODO: Remove this tracking once AI Bridge is enforced as an add-on license.
|
||||
// Track explicit AI Bridge entitlement (add-on license). This is checked
|
||||
// at the license level since AI Bridge may come from the FeatureSet
|
||||
// (Premium) rather than being explicitly listed in claims.Features.
|
||||
// Only having the AI Governance addon should suppress the soft warning.
|
||||
if slices.Contains(claims.Addons, codersdk.AddonAIGovernance) {
|
||||
hasExplicitAIBridgeEntitlement = true
|
||||
}
|
||||
|
||||
// Add all features from the feature set.
|
||||
for _, featureName := range claims.FeatureSet.Features() {
|
||||
if _, ok := licenseForbiddenFeatures[featureName]; ok {
|
||||
// Ignore any FeatureSet features that are forbidden to be set
|
||||
// in a license.
|
||||
// Ignore any FeatureSet features that are forbidden to be set in a license.
|
||||
continue
|
||||
}
|
||||
if _, ok := featureGrouping[featureName]; ok {
|
||||
@@ -310,8 +319,8 @@ func LicensesEntitlements(
|
||||
// multiple feature values into a single SDK feature.
|
||||
continue
|
||||
}
|
||||
if featureName == codersdk.FeatureUserLimit || featureName.UsesUsagePeriod() {
|
||||
// FeatureUserLimit and usage period features are handled below.
|
||||
if featureName.UsesLimit() || featureName.UsesUsagePeriod() {
|
||||
// Limit and usage period features are handled below.
|
||||
// They don't provide default values as they are always enabled
|
||||
// and require a limit to be specified in the license to have
|
||||
// any effect.
|
||||
@@ -341,12 +350,6 @@ func LicensesEntitlements(
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Remove this tracking once AI Bridge is enforced as an add-on license.
|
||||
// Track explicit AI Bridge entitlement (add-on license).
|
||||
if featureName == codersdk.FeatureAIBridge && featureValue > 0 {
|
||||
hasExplicitAIBridgeEntitlement = true
|
||||
}
|
||||
|
||||
// Special handling for grouped (e.g. usage period) features.
|
||||
if grouping, ok := featureGrouping[featureName]; ok {
|
||||
ul := uncommittedUsageFeatures[grouping.sdkFeature]
|
||||
@@ -367,18 +370,25 @@ func LicensesEntitlements(
|
||||
continue
|
||||
}
|
||||
|
||||
// Handling for non-grouped features.
|
||||
switch featureName {
|
||||
case codersdk.FeatureUserLimit:
|
||||
// Handling for limit features.
|
||||
switch {
|
||||
case featureName.UsesLimit():
|
||||
if featureValue <= 0 {
|
||||
// 0 user count doesn't make sense, so we skip it.
|
||||
// 0 limit value or less doesn't make sense, so we skip it.
|
||||
continue
|
||||
}
|
||||
entitlements.AddFeature(codersdk.FeatureUserLimit, codersdk.Feature{
|
||||
|
||||
// When we have a limit feature, we need to set the actual value (if available).
|
||||
var actual *int64
|
||||
if featureName == codersdk.FeatureUserLimit {
|
||||
actual = &featureArguments.ActiveUserCount
|
||||
}
|
||||
|
||||
entitlements.AddFeature(featureName, codersdk.Feature{
|
||||
Enabled: true,
|
||||
Entitlement: entitlement,
|
||||
Limit: &featureValue,
|
||||
Actual: &featureArguments.ActiveUserCount,
|
||||
Actual: actual,
|
||||
})
|
||||
default:
|
||||
if featureValue <= 0 {
|
||||
@@ -431,6 +441,35 @@ func LicensesEntitlements(
|
||||
}
|
||||
entitlements.AddFeature(featureName, feature)
|
||||
}
|
||||
|
||||
addonFeatures := make(map[codersdk.FeatureName]codersdk.Feature)
|
||||
|
||||
// Finally, add all features from the addons. We do this last so that
|
||||
// any dependencies of an addon are validated against the calculated
|
||||
// found entitlements. This is to stop a race condition with how we
|
||||
// calculate entitlements in tests.
|
||||
for _, addon := range claims.Addons {
|
||||
validationErrors := addon.ValidateDependencies(entitlements.Features)
|
||||
if len(validationErrors) > 0 {
|
||||
entitlements.Errors = append(
|
||||
entitlements.Errors,
|
||||
validationErrors...,
|
||||
)
|
||||
// Ignore the addon and don't add any features.
|
||||
continue
|
||||
}
|
||||
for _, featureName := range addon.Features() {
|
||||
if _, exists := addonFeatures[featureName]; !exists {
|
||||
addonFeatures[featureName] = codersdk.Feature{
|
||||
Entitlement: entitlement,
|
||||
Enabled: enablements[featureName] || featureName.AlwaysEnable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for featureName, feature := range addonFeatures {
|
||||
entitlements.AddFeature(featureName, feature)
|
||||
}
|
||||
}
|
||||
|
||||
// Now the license specific warnings and errors are added to the entitlements.
|
||||
@@ -666,11 +705,12 @@ type Claims struct {
|
||||
FeatureSet codersdk.FeatureSet `json:"feature_set"`
|
||||
// AllFeatures represents 'FeatureSet = FeatureSetEnterprise'
|
||||
// Deprecated: AllFeatures is deprecated in favor of FeatureSet.
|
||||
AllFeatures bool `json:"all_features,omitempty"`
|
||||
Version uint64 `json:"version"`
|
||||
Features Features `json:"features"`
|
||||
RequireTelemetry bool `json:"require_telemetry,omitempty"`
|
||||
PublishUsageData bool `json:"publish_usage_data,omitempty"`
|
||||
AllFeatures bool `json:"all_features,omitempty"`
|
||||
Version uint64 `json:"version"`
|
||||
Features Features `json:"features"`
|
||||
Addons []codersdk.Addon `json:"addons,omitempty"`
|
||||
RequireTelemetry bool `json:"require_telemetry,omitempty"`
|
||||
PublishUsageData bool `json:"publish_usage_data,omitempty"`
|
||||
}
|
||||
|
||||
var _ jwt.Claims = &Claims{}
|
||||
|
||||
@@ -188,16 +188,15 @@ func TestEntitlements(t *testing.T) {
|
||||
graceDate := dbtime.Now().AddDate(0, 0, 1)
|
||||
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||
Features: license.Features{
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
GraceAt: graceDate,
|
||||
ExpiresAt: dbtime.Now().AddDate(0, 0, 5),
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
}),
|
||||
Exp: time.Now().AddDate(0, 0, 5),
|
||||
})
|
||||
@@ -216,17 +215,16 @@ func TestEntitlements(t *testing.T) {
|
||||
// license expires.
|
||||
_, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||
Features: license.Features{
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
NotBefore: graceDate.Add(-time.Hour), // contiguous, and also in the future
|
||||
GraceAt: dbtime.Now().AddDate(1, 0, 0),
|
||||
ExpiresAt: dbtime.Now().AddDate(1, 0, 5),
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
}),
|
||||
Exp: dbtime.Now().AddDate(1, 0, 5),
|
||||
})
|
||||
@@ -249,16 +247,15 @@ func TestEntitlements(t *testing.T) {
|
||||
graceDate := dbtime.Now().AddDate(0, 0, 1)
|
||||
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||
Features: license.Features{
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
GraceAt: graceDate,
|
||||
ExpiresAt: dbtime.Now().AddDate(0, 0, 5),
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
}),
|
||||
Exp: time.Now().AddDate(0, 0, 5),
|
||||
})
|
||||
@@ -277,17 +274,16 @@ func TestEntitlements(t *testing.T) {
|
||||
// license expires (e.g. there's a gap)
|
||||
_, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||
Features: license.Features{
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
NotBefore: graceDate.Add(time.Minute), // gap of 1 second!
|
||||
GraceAt: dbtime.Now().AddDate(1, 0, 0),
|
||||
ExpiresAt: dbtime.Now().AddDate(1, 0, 5),
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
}),
|
||||
Exp: dbtime.Now().AddDate(1, 0, 5),
|
||||
})
|
||||
@@ -374,9 +370,15 @@ func TestEntitlements(t *testing.T) {
|
||||
require.True(t, entitlements.HasLicense)
|
||||
require.False(t, entitlements.Trial)
|
||||
for _, featureName := range codersdk.FeatureNames {
|
||||
if featureName == codersdk.FeatureUserLimit || featureName == codersdk.FeatureHighAvailability || featureName == codersdk.FeatureMultipleExternalAuth || featureName == codersdk.FeatureManagedAgentLimit {
|
||||
if featureName == codersdk.FeatureUserLimit ||
|
||||
featureName == codersdk.FeatureHighAvailability ||
|
||||
featureName == codersdk.FeatureMultipleExternalAuth ||
|
||||
featureName == codersdk.FeatureManagedAgentLimit ||
|
||||
featureName == codersdk.FeatureAIGovernanceUserLimit ||
|
||||
featureName == codersdk.FeatureBoundary {
|
||||
// These fields don't generate warnings when not entitled unless
|
||||
// a limit is breached.
|
||||
// a limit is breached, or in the case of AI Governance features,
|
||||
// they require the AI Governance addon.
|
||||
continue
|
||||
}
|
||||
niceName := featureName.Humanize()
|
||||
@@ -515,6 +517,9 @@ func TestEntitlements(t *testing.T) {
|
||||
// Enterprise licenses don't get any agents by default.
|
||||
continue
|
||||
}
|
||||
if featureName.IsAddonFeature() {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(enterpriseFeatures, featureName) {
|
||||
require.True(t, entitlements.Features[featureName].Enabled, featureName)
|
||||
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[featureName].Entitlement)
|
||||
@@ -574,6 +579,9 @@ func TestEntitlements(t *testing.T) {
|
||||
require.WithinDuration(t, agentUsagePeriodEnd, agentEntitlement.UsagePeriod.End, time.Second)
|
||||
continue
|
||||
}
|
||||
if featureName.IsAddonFeature() {
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(enterpriseFeatures, featureName) {
|
||||
require.True(t, entitlements.Features[featureName].Enabled, featureName)
|
||||
@@ -627,6 +635,9 @@ func TestEntitlements(t *testing.T) {
|
||||
if featureName.UsesLimit() {
|
||||
continue
|
||||
}
|
||||
if featureName.IsAddonFeature() {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(enterpriseFeatures, featureName) {
|
||||
require.True(t, entitlements.Features[featureName].Enabled, featureName)
|
||||
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[featureName].Entitlement)
|
||||
@@ -690,6 +701,9 @@ func TestEntitlements(t *testing.T) {
|
||||
if featureName == codersdk.FeatureUserLimit {
|
||||
continue
|
||||
}
|
||||
if featureName.IsAddonFeature() {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(enterpriseFeatures, featureName) {
|
||||
require.True(t, entitlements.Features[featureName].Enabled, featureName)
|
||||
require.Equal(t, codersdk.EntitlementGracePeriod, entitlements.Features[featureName].Entitlement)
|
||||
@@ -814,16 +828,15 @@ func TestEntitlements(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
|
||||
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||
licenseOpts := (&coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
IssuedAt: dbtime.Now().Add(-2 * time.Hour).Truncate(time.Second),
|
||||
NotBefore: dbtime.Now().Add(-time.Hour).Truncate(time.Second),
|
||||
GraceAt: dbtime.Now().Add(time.Hour * 24 * 60).Truncate(time.Second), // 60 days to remove warning
|
||||
ExpiresAt: dbtime.Now().Add(time.Hour * 24 * 90).Truncate(time.Second), // 90 days to remove warning
|
||||
// Explicit AI Bridge to avoid soft warning in tests
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
}).
|
||||
UserLimit(100).
|
||||
@@ -868,6 +881,7 @@ func TestEntitlements(t *testing.T) {
|
||||
|
||||
managedAgentLimit, ok := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
require.True(t, ok)
|
||||
|
||||
require.NotNil(t, managedAgentLimit.SoftLimit)
|
||||
require.EqualValues(t, 100, *managedAgentLimit.SoftLimit)
|
||||
require.NotNil(t, managedAgentLimit.Limit)
|
||||
@@ -906,9 +920,9 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
codersdk.FeatureControlSharedPorts: true,
|
||||
codersdk.FeatureWorkspaceExternalAgent: true,
|
||||
codersdk.FeatureAIBridge: true,
|
||||
codersdk.FeatureBoundary: true,
|
||||
}
|
||||
|
||||
// TODO: Remove explicit FeatureAIBridge from these license helpers once AI Bridge is enforced as an add-on license.
|
||||
legacyLicense := func() *coderdenttest.LicenseOptions {
|
||||
return (&coderdenttest.LicenseOptions{
|
||||
AccountType: "salesforce",
|
||||
@@ -916,9 +930,9 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
Trial: false,
|
||||
// Use the legacy boolean
|
||||
AllFeatures: true,
|
||||
// Explicit AI Bridge to avoid soft warning in tests
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
}).Valid(time.Now())
|
||||
}
|
||||
@@ -931,9 +945,9 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
Trial: false,
|
||||
FeatureSet: codersdk.FeatureSetEnterprise,
|
||||
AllFeatures: true,
|
||||
// Explicit AI Bridge to avoid soft warning in tests
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
}).Valid(time.Now())
|
||||
}
|
||||
@@ -946,9 +960,9 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
Trial: false,
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
AllFeatures: true,
|
||||
// Explicit AI Bridge to avoid soft warning in tests
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
}).Valid(time.Now())
|
||||
}
|
||||
@@ -1324,74 +1338,9 @@ func TestAIBridgeSoftWarning(t *testing.T) {
|
||||
|
||||
aiBridgeWarningMessage := "AI Bridge is now Generally Available in v2.30. In a future Coder version, your deployment will require the AI Governance Add-On to continue using this feature. Please reach out to your account team or sales@coder.com to learn more."
|
||||
|
||||
t.Run("PremiumLicenseWithAIBridgeEnabled", func(t *testing.T) {
|
||||
t.Run("NoAddon_AIBridgeOff", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Premium license with AI Bridge enabled should show soft warning
|
||||
// because AI Bridge is not explicitly granted via add-on.
|
||||
lo := (&coderdenttest.LicenseOptions{
|
||||
AccountType: "salesforce",
|
||||
AccountID: "test",
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
}).Valid(time.Now())
|
||||
|
||||
generatedLicenses := []database.License{
|
||||
{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now().Add(time.Hour * -1),
|
||||
JWT: lo.Generate(t),
|
||||
Exp: lo.GraceAt,
|
||||
UUID: uuid.New(),
|
||||
},
|
||||
}
|
||||
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), generatedLicenses, aiBridgeEnabledEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// AI Bridge should be enabled and entitled.
|
||||
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
assert.True(t, aiBridgeFeature.Enabled)
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, aiBridgeFeature.Entitlement)
|
||||
|
||||
// Should have the soft warning.
|
||||
require.Contains(t, entitlements.Warnings, aiBridgeWarningMessage)
|
||||
})
|
||||
|
||||
t.Run("ExplicitAIBridgeLicense", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// License with explicit AI Bridge feature (add-on) should NOT show warning.
|
||||
lo := (&coderdenttest.LicenseOptions{
|
||||
AccountType: "salesforce",
|
||||
AccountID: "test",
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
}).Valid(time.Now())
|
||||
|
||||
generatedLicenses := []database.License{
|
||||
{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now().Add(time.Hour * -1),
|
||||
JWT: lo.Generate(t),
|
||||
Exp: lo.GraceAt,
|
||||
UUID: uuid.New(),
|
||||
},
|
||||
}
|
||||
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), generatedLicenses, aiBridgeEnabledEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// AI Bridge should be enabled and entitled.
|
||||
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
assert.True(t, aiBridgeFeature.Enabled)
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, aiBridgeFeature.Entitlement)
|
||||
|
||||
// Should NOT have the soft warning.
|
||||
require.NotContains(t, entitlements.Warnings, aiBridgeWarningMessage)
|
||||
})
|
||||
|
||||
t.Run("PremiumLicenseWithAIBridgeDisabled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Premium license with AI Bridge NOT enabled should NOT show warning.
|
||||
// License without addon and AI Bridge disabled should NOT show warning.
|
||||
lo := (&coderdenttest.LicenseOptions{
|
||||
AccountType: "salesforce",
|
||||
AccountID: "test",
|
||||
@@ -1411,23 +1360,80 @@ func TestAIBridgeSoftWarning(t *testing.T) {
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), generatedLicenses, aiBridgeDisabledEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// AI Bridge should NOT be enabled.
|
||||
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
assert.False(t, aiBridgeFeature.Enabled)
|
||||
|
||||
// Should NOT have the soft warning.
|
||||
require.NotContains(t, entitlements.Warnings, aiBridgeWarningMessage)
|
||||
})
|
||||
|
||||
t.Run("PremiumPlusExplicitAIBridge", func(t *testing.T) {
|
||||
t.Run("NoAddon_AIBridgeOn", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Premium license PLUS explicit AI Bridge add-on should NOT show warning.
|
||||
// License without addon and AI Bridge enabled SHOULD show warning.
|
||||
lo := (&coderdenttest.LicenseOptions{
|
||||
AccountType: "salesforce",
|
||||
AccountID: "test",
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
}).Valid(time.Now())
|
||||
|
||||
generatedLicenses := []database.License{
|
||||
{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now().Add(time.Hour * -1),
|
||||
JWT: lo.Generate(t),
|
||||
Exp: lo.GraceAt,
|
||||
UUID: uuid.New(),
|
||||
},
|
||||
}
|
||||
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), generatedLicenses, aiBridgeEnabledEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
||||
require.NoError(t, err)
|
||||
|
||||
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
assert.True(t, aiBridgeFeature.Enabled)
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, aiBridgeFeature.Entitlement)
|
||||
require.Contains(t, entitlements.Warnings, aiBridgeWarningMessage)
|
||||
})
|
||||
|
||||
t.Run("Addon_AIBridgeOff", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// License with addon and AI Bridge disabled should NOT show warning.
|
||||
lo := (&coderdenttest.LicenseOptions{
|
||||
AccountType: "salesforce",
|
||||
AccountID: "test",
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
}).Valid(time.Now())
|
||||
|
||||
generatedLicenses := []database.License{
|
||||
{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now().Add(time.Hour * -1),
|
||||
JWT: lo.Generate(t),
|
||||
Exp: lo.GraceAt,
|
||||
UUID: uuid.New(),
|
||||
},
|
||||
}
|
||||
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), generatedLicenses, aiBridgeDisabledEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
||||
require.NoError(t, err)
|
||||
|
||||
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
assert.False(t, aiBridgeFeature.Enabled)
|
||||
require.NotContains(t, entitlements.Warnings, aiBridgeWarningMessage)
|
||||
})
|
||||
|
||||
t.Run("Addon_AIBridgeOn", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// License with addon and AI Bridge enabled should NOT show warning.
|
||||
lo := (&coderdenttest.LicenseOptions{
|
||||
AccountType: "salesforce",
|
||||
AccountID: "test",
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIGovernanceUserLimit: 100,
|
||||
},
|
||||
}).Valid(time.Now())
|
||||
|
||||
@@ -1444,27 +1450,21 @@ func TestAIBridgeSoftWarning(t *testing.T) {
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), generatedLicenses, aiBridgeEnabledEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// AI Bridge should be enabled and entitled.
|
||||
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
assert.True(t, aiBridgeFeature.Enabled)
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, aiBridgeFeature.Entitlement)
|
||||
|
||||
// Should NOT have the soft warning.
|
||||
require.NotContains(t, entitlements.Warnings, aiBridgeWarningMessage)
|
||||
})
|
||||
|
||||
t.Run("NoLicenseWithAIBridgeEnabled", func(t *testing.T) {
|
||||
t.Run("NoLicense_AIBridgeOn", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// No license with AI Bridge enabled should NOT show the soft warning
|
||||
// (it will show the generic "not entitled" warning instead).
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), []database.License{}, aiBridgeEnabledEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// AI Bridge should NOT be entitled.
|
||||
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
assert.Equal(t, codersdk.EntitlementNotEntitled, aiBridgeFeature.Entitlement)
|
||||
|
||||
// Should NOT have the soft warning (the feature is not entitled so it won't be enabled).
|
||||
require.NotContains(t, entitlements.Warnings, aiBridgeWarningMessage)
|
||||
})
|
||||
}
|
||||
@@ -1829,6 +1829,186 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAIGovernanceAddon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
empty := map[codersdk.FeatureName]bool{}
|
||||
|
||||
t.Run("AIGovernanceAddon enables AI governance features when enablements are set", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIGovernanceUserLimit: 1000,
|
||||
codersdk.FeatureManagedAgentLimit: 1000,
|
||||
},
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
}),
|
||||
Exp: dbtime.Now().Add(time.Hour),
|
||||
})
|
||||
|
||||
// Enable AI governance features in enablements.
|
||||
enablements := map[codersdk.FeatureName]bool{
|
||||
codersdk.FeatureAIBridge: true,
|
||||
codersdk.FeatureBoundary: true,
|
||||
}
|
||||
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, enablements)
|
||||
require.NoError(t, err)
|
||||
require.True(t, entitlements.HasLicense)
|
||||
|
||||
// AI Bridge should be enabled without warning when addon is present.
|
||||
aibridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
require.True(t, aibridgeFeature.Enabled, "AI Bridge should be enabled when addon is present and enablements are set")
|
||||
aiBridgeWarningMessage := "AI Bridge is now Generally Available in v2.30. In a future Coder version, your deployment will require the AI Governance Add-On to continue using this feature. Please reach out to your account team or sales@coder.com to learn more."
|
||||
require.NotContains(t, entitlements.Warnings, aiBridgeWarningMessage, "AI Bridge warning should not appear when AI Governance addon is present")
|
||||
|
||||
// require.Equal(t, codersdk.EntitlementEntitled, aibridgeFeature.Entitlement, "AI Bridge should be entitled when addon is present")
|
||||
|
||||
// TODO: Readd this test once Boundary is enforced as an add-on license.
|
||||
// boundaryFeature := entitlements.Features[codersdk.FeatureBoundary]
|
||||
// require.True(t, boundaryFeature.Enabled, "Boundary should be enabled when addon is present and enablements are set")
|
||||
// require.Equal(t, codersdk.EntitlementEntitled, boundaryFeature.Entitlement, "Boundary should be entitled when addon is present")
|
||||
})
|
||||
|
||||
t.Run("AIGovernanceAddon not present disables AI governance features", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
}),
|
||||
Exp: dbtime.Now().Add(time.Hour),
|
||||
})
|
||||
|
||||
enablements := map[codersdk.FeatureName]bool{
|
||||
codersdk.FeatureAIBridge: true,
|
||||
codersdk.FeatureBoundary: true,
|
||||
}
|
||||
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, enablements)
|
||||
require.NoError(t, err)
|
||||
require.True(t, entitlements.HasLicense)
|
||||
|
||||
// TODO: Readd this test once AI Bridge is enforced as an add-on license.
|
||||
// AI Bridge should not be entitled.
|
||||
// aibridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
// require.False(t, aibridgeFeature.Enabled, "AI Bridge should not be enabled when addon is absent")
|
||||
// require.Equal(t, codersdk.EntitlementNotEntitled, aibridgeFeature.Entitlement, "AI Bridge should not be entitled when addon is absent")
|
||||
|
||||
// TODO: Readd this test once Boundary is enforced as an add-on license.
|
||||
// boundaryFeature := entitlements.Features[codersdk.FeatureBoundary]
|
||||
// require.False(t, boundaryFeature.Enabled, "Boundary should not be enabled when addon is absent")
|
||||
// require.Equal(t, codersdk.EntitlementNotEntitled, boundaryFeature.Entitlement, "Boundary should not be entitled when addon is absent")
|
||||
})
|
||||
|
||||
t.Run("AIGovernanceAddon respects grace period entitlement", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIGovernanceUserLimit: 1000,
|
||||
codersdk.FeatureManagedAgentLimit: 1000,
|
||||
},
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
NotBefore: dbtime.Now().Add(-time.Hour * 2),
|
||||
GraceAt: dbtime.Now().Add(-time.Hour),
|
||||
ExpiresAt: dbtime.Now().Add(time.Hour),
|
||||
}),
|
||||
Exp: dbtime.Now().Add(time.Hour),
|
||||
})
|
||||
|
||||
enablements := map[codersdk.FeatureName]bool{
|
||||
codersdk.FeatureAIBridge: true,
|
||||
codersdk.FeatureBoundary: true,
|
||||
}
|
||||
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, enablements)
|
||||
require.NoError(t, err)
|
||||
require.True(t, entitlements.HasLicense)
|
||||
|
||||
// TODO: Readd this test once AI Bridge is enforced as an add-on license.
|
||||
// AI governance features should be enabled but in grace period.
|
||||
// aibridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
// require.True(t, aibridgeFeature.Enabled, "AI Bridge should be enabled during grace period")
|
||||
// require.Equal(t, codersdk.EntitlementGracePeriod, aibridgeFeature.Entitlement, "AI Bridge should be in grace period")
|
||||
|
||||
// TODO: Readd this test once Boundary is enforced as an add-on license.
|
||||
// boundaryFeature := entitlements.Features[codersdk.FeatureBoundary]
|
||||
// require.True(t, boundaryFeature.Enabled, "Boundary should be enabled during grace period")
|
||||
// require.Equal(t, codersdk.EntitlementGracePeriod, boundaryFeature.Entitlement, "Boundary should be in grace period")
|
||||
})
|
||||
|
||||
t.Run("AIGovernanceAddon requires enablements to enable features", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIGovernanceUserLimit: 1000,
|
||||
codersdk.FeatureManagedAgentLimit: 1000,
|
||||
},
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
}),
|
||||
Exp: dbtime.Now().Add(time.Hour),
|
||||
})
|
||||
|
||||
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, empty)
|
||||
require.NoError(t, err)
|
||||
require.True(t, entitlements.HasLicense)
|
||||
|
||||
// TODO: Readd this test once AI Bridge is enforced as an add-on license.
|
||||
// aibridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
// require.False(t, aibridgeFeature.Enabled, "AI Bridge should not be enabled without enablements")
|
||||
// require.Equal(t, codersdk.EntitlementEntitled, aibridgeFeature.Entitlement, "AI Bridge should still be entitled")
|
||||
|
||||
// TODO: Readd this test once Boundary is enforced as an add-on license.
|
||||
// boundaryFeature := entitlements.Features[codersdk.FeatureBoundary]
|
||||
// require.False(t, boundaryFeature.Enabled, "Boundary should not be enabled without enablements")
|
||||
// require.Equal(t, codersdk.EntitlementEntitled, boundaryFeature.Entitlement, "Boundary should still be entitled")
|
||||
})
|
||||
|
||||
t.Run("AIGovernanceAddon missing dependencies", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
// Use Enterprise so ManagedAgentLimit doesn't get default value, and
|
||||
// don't set either dependency.
|
||||
db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetEnterprise,
|
||||
Features: license.Features{},
|
||||
Addons: []codersdk.Addon{codersdk.AddonAIGovernance},
|
||||
}),
|
||||
Exp: dbtime.Now().Add(time.Hour),
|
||||
})
|
||||
|
||||
enablements := map[codersdk.FeatureName]bool{
|
||||
codersdk.FeatureAIBridge: true,
|
||||
codersdk.FeatureBoundary: true,
|
||||
}
|
||||
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, enablements)
|
||||
require.NoError(t, err)
|
||||
require.True(t, entitlements.HasLicense)
|
||||
|
||||
// Should have validation error for missing AI Governance User Limit.
|
||||
require.Len(t, entitlements.Errors, 1)
|
||||
require.Equal(t, "Feature AI Governance User Limit must be set when using the AI Governance addon.", entitlements.Errors[0])
|
||||
|
||||
// TODO: Readd this test once AI Bridge is enforced as an add-on license.
|
||||
// AI governance features should not be entitled when validation fails.
|
||||
// aibridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||
// require.False(t, aibridgeFeature.Enabled, "AI Bridge should not be enabled when addon validation fails")
|
||||
// require.Equal(t, codersdk.EntitlementNotEntitled, aibridgeFeature.Entitlement, "AI Bridge should not be entitled when addon validation fails")
|
||||
|
||||
// TODO: Readd this test once Boundary is enforced as an add-on license.
|
||||
// boundaryFeature := entitlements.Features[codersdk.FeatureBoundary]
|
||||
// require.False(t, boundaryFeature.Enabled, "Boundary should not be enabled when addon validation fails")
|
||||
// require.Equal(t, codersdk.EntitlementNotEntitled, boundaryFeature.Entitlement, "Boundary should not be entitled when addon validation fails")
|
||||
})
|
||||
}
|
||||
|
||||
func assertNoErrors(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
t.Helper()
|
||||
assert.Empty(t, entitlements.Errors, "no errors")
|
||||
|
||||
Generated
+7
@@ -585,6 +585,11 @@ export interface AddLicenseRequest {
|
||||
readonly license: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type Addon = "ai_governance";
|
||||
|
||||
export const Addons: Addon[] = ["ai_governance"];
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export interface AgentConnectionTiming {
|
||||
readonly started_at: string;
|
||||
@@ -2107,6 +2112,7 @@ export interface Feature {
|
||||
// From codersdk/deployment.go
|
||||
export type FeatureName =
|
||||
| "aibridge"
|
||||
| "ai_governance_user_limit"
|
||||
| "access_control"
|
||||
| "advanced_template_scheduling"
|
||||
| "appearance"
|
||||
@@ -2134,6 +2140,7 @@ export type FeatureName =
|
||||
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
"aibridge",
|
||||
"ai_governance_user_limit",
|
||||
"access_control",
|
||||
"advanced_template_scheduling",
|
||||
"appearance",
|
||||
|
||||
Reference in New Issue
Block a user