mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(enterprise/coderd): add soft warning for AI Bridge GA transition (#21675)
## Summary AI Bridge is moving to General Availability in v2.30 and will require the AI Governance Add-On license in future versions. This adds a soft warning for deployments using AI Bridge via Premium/Enterprise FeatureSet without an explicit AI Bridge add-on license. Relates to: https://github.com/coder/internal/issues/1226 ## Changes - Track whether AI Bridge was explicitly granted via license Features (add-on) vs inherited from FeatureSet - Show soft warning when AI Bridge is enabled and entitled via FeatureSet but not via explicit add-on - Changed AI Bridge enablement from hardcoded `true` to check `CODER_AIBRIDGE_ENABLED` deployment config ## Behavior Change AI Bridge is now only marked as "enabled" in entitlements when `CODER_AIBRIDGE_ENABLED=true` is set in the deployment config. Previously, it was always enabled for Premium/Enterprise licenses regardless of the config setting. This change ensures that users who do not use AI Bridge will not see the soft warning about the upcoming license requirement. ## Warning Message > 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. ## Behavior | Condition | Warning Shown | |-----------|---------------| | AI Bridge disabled | ❌ No | | AI Bridge enabled + explicit add-on license | ❌ No | | AI Bridge enabled + Premium/Enterprise FeatureSet (no add-on) | ✅ Yes | ## Screenshots ### 1. No license <img width="1708" height="577" alt="image" src="https://github.com/user-attachments/assets/cbdbfd4d-55de-4d70-8abf-2665f458e96f" /> ### 2. No license + CODER_AIBRIDGE_ENABLED=true <img width="1716" height="513" alt="image" src="https://github.com/user-attachments/assets/344aae76-7703-485f-b568-1f13a1efa48f" /> ### 3. Premium license + CODER_AIBRIDGE_ENABLED=false <img width="1687" height="389" alt="image" src="https://github.com/user-attachments/assets/c2be12b0-1c0f-438d-a293-f9ec9fe6a736" /> ### 4. Premium license + CODER_AIBRIDGE_ENABLED=true <img width="1707" height="525" alt="image" src="https://github.com/user-attachments/assets/1a4640e1-e656-4f9b-bed0-9390cb5d6a84" /> ## Notes - TODO comments added to mark code that should be removed when AI Bridge enforcement is added - Feature continues to work - this is just a transitional warning (soft enforcement)
This commit is contained in:
@@ -27,6 +27,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = true
|
||||||
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -77,6 +78,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = true
|
||||||
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -162,6 +164,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = true
|
||||||
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
"github.com/coder/serpent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAIBridgeListInterceptions(t *testing.T) {
|
func TestAIBridgeListInterceptions(t *testing.T) {
|
||||||
@@ -51,6 +52,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Run("EmptyDB", func(t *testing.T) {
|
t.Run("EmptyDB", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -71,6 +73,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Run("OK", func(t *testing.T) {
|
t.Run("OK", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -189,6 +192,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -304,6 +308,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Run("InflightInterceptions", func(t *testing.T) {
|
t.Run("InflightInterceptions", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -337,6 +342,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Run("Authorized", func(t *testing.T) {
|
t.Run("Authorized", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
adminClient, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
adminClient, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -381,6 +387,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Run("Filter", func(t *testing.T) {
|
t.Run("Filter", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -561,6 +568,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
|||||||
t.Run("FilterErrors", func(t *testing.T) {
|
t.Run("FilterErrors", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -643,6 +651,7 @@ func TestAIBridgeRouting(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -703,6 +712,7 @@ func TestAIBridgeRateLimiting(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
// Set a low rate limit for testing.
|
// Set a low rate limit for testing.
|
||||||
dv.AI.BridgeConfig.RateLimit = 2
|
dv.AI.BridgeConfig.RateLimit = 2
|
||||||
|
|
||||||
@@ -758,6 +768,7 @@ func TestAIBridgeConcurrencyLimiting(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
// Set a low concurrency limit for testing.
|
// Set a low concurrency limit for testing.
|
||||||
dv.AI.BridgeConfig.MaxConcurrency = 1
|
dv.AI.BridgeConfig.MaxConcurrency = 1
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
"github.com/coder/serpent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
||||||
@@ -20,6 +21,7 @@ func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
// Proxy is disabled by default, so we don't need to set it explicitly.
|
// Proxy is disabled by default, so we don't need to set it explicitly.
|
||||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
@@ -50,6 +52,7 @@ func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
@@ -78,6 +81,7 @@ func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dv := coderdtest.DeploymentValues(t)
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
Options: &coderdtest.Options{
|
Options: &coderdtest.Options{
|
||||||
DeploymentValues: dv,
|
DeploymentValues: dv,
|
||||||
|
|||||||
@@ -769,7 +769,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
|||||||
codersdk.FeatureUserRoleManagement: true,
|
codersdk.FeatureUserRoleManagement: true,
|
||||||
codersdk.FeatureAccessControl: true,
|
codersdk.FeatureAccessControl: true,
|
||||||
codersdk.FeatureControlSharedPorts: true,
|
codersdk.FeatureControlSharedPorts: true,
|
||||||
codersdk.FeatureAIBridge: true,
|
codersdk.FeatureAIBridge: api.DeploymentValues.AI.BridgeConfig.Enabled.Value(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return codersdk.Entitlements{}, err
|
return codersdk.Entitlements{}, err
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ func LicensesEntitlements(
|
|||||||
keys map[string]ed25519.PublicKey,
|
keys map[string]ed25519.PublicKey,
|
||||||
featureArguments FeatureArguments,
|
featureArguments FeatureArguments,
|
||||||
) (codersdk.Entitlements, error) {
|
) (codersdk.Entitlements, error) {
|
||||||
|
// TODO: Remove this tracking once AI Bridge is enforced as an add-on license.
|
||||||
|
// Track if AI Bridge was explicitly granted via license Features (add-on)
|
||||||
|
// vs inherited from FeatureSet (Premium). Only explicit grants should
|
||||||
|
// suppress the soft warning for AI Bridge GA.
|
||||||
|
hasExplicitAIBridgeEntitlement := false
|
||||||
|
|
||||||
// Default all entitlements to be disabled.
|
// Default all entitlements to be disabled.
|
||||||
entitlements := codersdk.Entitlements{
|
entitlements := codersdk.Entitlements{
|
||||||
Features: map[codersdk.FeatureName]codersdk.Feature{
|
Features: map[codersdk.FeatureName]codersdk.Feature{
|
||||||
@@ -335,6 +341,12 @@ func LicensesEntitlements(
|
|||||||
continue
|
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.
|
// Special handling for grouped (e.g. usage period) features.
|
||||||
if grouping, ok := featureGrouping[featureName]; ok {
|
if grouping, ok := featureGrouping[featureName]; ok {
|
||||||
ul := uncommittedUsageFeatures[grouping.sdkFeature]
|
ul := uncommittedUsageFeatures[grouping.sdkFeature]
|
||||||
@@ -583,6 +595,17 @@ func LicensesEntitlements(
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this soft warning block once AI Bridge is enforced as an add-on license.
|
||||||
|
// AI Bridge soft warning: Show warning when AI Bridge is enabled and
|
||||||
|
// entitled via Premium FeatureSet but not via explicit add-on license.
|
||||||
|
// This is a transitional warning as AI Bridge moves to GA and will
|
||||||
|
// require a separate add-on license in future versions.
|
||||||
|
aiBridgeFeature := entitlements.Features[codersdk.FeatureAIBridge]
|
||||||
|
if aiBridgeFeature.Enabled && aiBridgeFeature.Entitlement.Entitled() && !hasExplicitAIBridgeEntitlement {
|
||||||
|
entitlements.Warnings = append(entitlements.Warnings,
|
||||||
|
"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.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap up by disabling all features that are not entitled.
|
// Wrap up by disabling all features that are not entitled.
|
||||||
|
|||||||
@@ -188,9 +188,11 @@ func TestEntitlements(t *testing.T) {
|
|||||||
graceDate := dbtime.Now().AddDate(0, 0, 1)
|
graceDate := dbtime.Now().AddDate(0, 0, 1)
|
||||||
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 100,
|
codersdk.FeatureUserLimit: 100,
|
||||||
codersdk.FeatureAuditLog: 1,
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||||
},
|
},
|
||||||
|
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
@@ -214,9 +216,11 @@ func TestEntitlements(t *testing.T) {
|
|||||||
// license expires.
|
// license expires.
|
||||||
_, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
_, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 100,
|
codersdk.FeatureUserLimit: 100,
|
||||||
codersdk.FeatureAuditLog: 1,
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||||
},
|
},
|
||||||
|
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
@@ -245,9 +249,11 @@ func TestEntitlements(t *testing.T) {
|
|||||||
graceDate := dbtime.Now().AddDate(0, 0, 1)
|
graceDate := dbtime.Now().AddDate(0, 0, 1)
|
||||||
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 100,
|
codersdk.FeatureUserLimit: 100,
|
||||||
codersdk.FeatureAuditLog: 1,
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||||
},
|
},
|
||||||
|
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
@@ -271,9 +277,11 @@ func TestEntitlements(t *testing.T) {
|
|||||||
// license expires (e.g. there's a gap)
|
// license expires (e.g. there's a gap)
|
||||||
_, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
_, err = db.InsertLicense(context.Background(), database.InsertLicenseParams{
|
||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 100,
|
codersdk.FeatureUserLimit: 100,
|
||||||
codersdk.FeatureAuditLog: 1,
|
codersdk.FeatureAuditLog: 1,
|
||||||
|
codersdk.FeatureAIBridge: 1, // Explicit AI Bridge to avoid soft warning
|
||||||
},
|
},
|
||||||
|
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
@@ -806,12 +814,17 @@ func TestEntitlements(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
mDB := dbmock.NewMockStore(ctrl)
|
mDB := dbmock.NewMockStore(ctrl)
|
||||||
|
|
||||||
|
// TODO: Remove explicit FeatureAIBridge once AI Bridge is enforced as an add-on license.
|
||||||
licenseOpts := (&coderdenttest.LicenseOptions{
|
licenseOpts := (&coderdenttest.LicenseOptions{
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
IssuedAt: dbtime.Now().Add(-2 * time.Hour).Truncate(time.Second),
|
IssuedAt: dbtime.Now().Add(-2 * time.Hour).Truncate(time.Second),
|
||||||
NotBefore: dbtime.Now().Add(-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
|
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
|
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
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureAIBridge: 1,
|
||||||
|
},
|
||||||
}).
|
}).
|
||||||
UserLimit(100).
|
UserLimit(100).
|
||||||
ManagedAgentLimit(100, 200)
|
ManagedAgentLimit(100, 200)
|
||||||
@@ -895,6 +908,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
codersdk.FeatureAIBridge: true,
|
codersdk.FeatureAIBridge: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove explicit FeatureAIBridge from these license helpers once AI Bridge is enforced as an add-on license.
|
||||||
legacyLicense := func() *coderdenttest.LicenseOptions {
|
legacyLicense := func() *coderdenttest.LicenseOptions {
|
||||||
return (&coderdenttest.LicenseOptions{
|
return (&coderdenttest.LicenseOptions{
|
||||||
AccountType: "salesforce",
|
AccountType: "salesforce",
|
||||||
@@ -902,6 +916,10 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
Trial: false,
|
Trial: false,
|
||||||
// Use the legacy boolean
|
// Use the legacy boolean
|
||||||
AllFeatures: true,
|
AllFeatures: true,
|
||||||
|
// Explicit AI Bridge to avoid soft warning in tests
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureAIBridge: 1,
|
||||||
|
},
|
||||||
}).Valid(time.Now())
|
}).Valid(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,6 +931,10 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
Trial: false,
|
Trial: false,
|
||||||
FeatureSet: codersdk.FeatureSetEnterprise,
|
FeatureSet: codersdk.FeatureSetEnterprise,
|
||||||
AllFeatures: true,
|
AllFeatures: true,
|
||||||
|
// Explicit AI Bridge to avoid soft warning in tests
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureAIBridge: 1,
|
||||||
|
},
|
||||||
}).Valid(time.Now())
|
}).Valid(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,6 +946,10 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
Trial: false,
|
Trial: false,
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
AllFeatures: true,
|
AllFeatures: true,
|
||||||
|
// Explicit AI Bridge to avoid soft warning in tests
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureAIBridge: 1,
|
||||||
|
},
|
||||||
}).Valid(time.Now())
|
}).Valid(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,6 +1311,164 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAIBridgeSoftWarning(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
aiBridgeEnabledEnablements := map[codersdk.FeatureName]bool{
|
||||||
|
codersdk.FeatureAIBridge: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
aiBridgeDisabledEnablements := map[codersdk.FeatureName]bool{
|
||||||
|
codersdk.FeatureAIBridge: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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.
|
||||||
|
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, 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.Parallel()
|
||||||
|
// Premium license PLUS explicit AI Bridge add-on should NOT show warning.
|
||||||
|
lo := (&coderdenttest.LicenseOptions{
|
||||||
|
AccountType: "salesforce",
|
||||||
|
AccountID: "test",
|
||||||
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
|
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("NoLicenseWithAIBridgeEnabled", 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestUsageLimitFeatures(t *testing.T) {
|
func TestUsageLimitFeatures(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user