mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08: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() {
|
||||
|
||||
Reference in New Issue
Block a user