mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: convert soft_limit to limit (#22048)
In relation to [`internal#1281`](https://github.com/coder/internal/issues/1281) Remove the `soft_limit` field from the `Feature` type and simplify license limit handling. This change: - Removes the `soft_limit` field from the API and SDK - Uses the soft limit value as the single `limit` value in the UI and API - Simplifies warning logic to only show warnings when the limit is exceeded - Updates tests to reflect the new behavior - Updates the UI to use the single limit value for display
This commit is contained in:
Generated
-4
@@ -15368,10 +15368,6 @@ const docTemplate = `{
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"soft_limit": {
|
||||
"description": "SoftLimit is the soft limit of the feature, and is only used for showing\nincluded limits in the dashboard. No license validation or warnings are\ngenerated from this value.",
|
||||
"type": "integer"
|
||||
},
|
||||
"usage_period": {
|
||||
"description": "UsagePeriod denotes that the usage is a counter that accumulates over\nthis period (and most likely resets with the issuance of the next\nlicense).\n\nThese dates are determined from the license that this entitlement comes\nfrom, see enterprise/coderd/license/license.go.\n\nOnly certain features set these fields:\n- FeatureManagedAgentLimit",
|
||||
"allOf": [
|
||||
|
||||
Generated
-4
@@ -13895,10 +13895,6 @@
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"soft_limit": {
|
||||
"description": "SoftLimit is the soft limit of the feature, and is only used for showing\nincluded limits in the dashboard. No license validation or warnings are\ngenerated from this value.",
|
||||
"type": "integer"
|
||||
},
|
||||
"usage_period": {
|
||||
"description": "UsagePeriod denotes that the usage is a counter that accumulates over\nthis period (and most likely resets with the issuance of the next\nlicense).\n\nThese dates are determined from the license that this entitlement comes\nfrom, see enterprise/coderd/license/license.go.\n\nOnly certain features set these fields:\n- FeatureManagedAgentLimit",
|
||||
"allOf": [
|
||||
|
||||
@@ -372,10 +372,6 @@ type Feature struct {
|
||||
|
||||
// Below is only for features that use usage periods.
|
||||
|
||||
// SoftLimit is the soft limit of the feature, and is only used for showing
|
||||
// included limits in the dashboard. No license validation or warnings are
|
||||
// generated from this value.
|
||||
SoftLimit *int64 `json:"soft_limit,omitempty"`
|
||||
// UsagePeriod denotes that the usage is a counter that accumulates over
|
||||
// this period (and most likely resets with the issuance of the next
|
||||
// license).
|
||||
|
||||
Generated
-2
@@ -329,7 +329,6 @@ curl -X GET http://coder-server:8080/api/v2/entitlements \
|
||||
"enabled": true,
|
||||
"entitlement": "entitled",
|
||||
"limit": 0,
|
||||
"soft_limit": 0,
|
||||
"usage_period": {
|
||||
"end": "2019-08-24T14:15:22Z",
|
||||
"issued_at": "2019-08-24T14:15:22Z",
|
||||
@@ -341,7 +340,6 @@ curl -X GET http://coder-server:8080/api/v2/entitlements \
|
||||
"enabled": true,
|
||||
"entitlement": "entitled",
|
||||
"limit": 0,
|
||||
"soft_limit": 0,
|
||||
"usage_period": {
|
||||
"end": "2019-08-24T14:15:22Z",
|
||||
"issued_at": "2019-08-24T14:15:22Z",
|
||||
|
||||
Generated
+6
-10
@@ -3902,7 +3902,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"enabled": true,
|
||||
"entitlement": "entitled",
|
||||
"limit": 0,
|
||||
"soft_limit": 0,
|
||||
"usage_period": {
|
||||
"end": "2019-08-24T14:15:22Z",
|
||||
"issued_at": "2019-08-24T14:15:22Z",
|
||||
@@ -3914,7 +3913,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"enabled": true,
|
||||
"entitlement": "entitled",
|
||||
"limit": 0,
|
||||
"soft_limit": 0,
|
||||
"usage_period": {
|
||||
"end": "2019-08-24T14:15:22Z",
|
||||
"issued_at": "2019-08-24T14:15:22Z",
|
||||
@@ -4196,7 +4194,6 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
||||
"enabled": true,
|
||||
"entitlement": "entitled",
|
||||
"limit": 0,
|
||||
"soft_limit": 0,
|
||||
"usage_period": {
|
||||
"end": "2019-08-24T14:15:22Z",
|
||||
"issued_at": "2019-08-24T14:15:22Z",
|
||||
@@ -4207,13 +4204,12 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|---------------|----------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `actual` | integer | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `entitlement` | [codersdk.Entitlement](#codersdkentitlement) | false | | |
|
||||
| `limit` | integer | false | | |
|
||||
| `soft_limit` | integer | false | | Soft limit is the soft limit of the feature, and is only used for showing included limits in the dashboard. No license validation or warnings are generated from this value. |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|---------------|----------------------------------------------|----------|--------------|-------------|
|
||||
| `actual` | integer | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `entitlement` | [codersdk.Entitlement](#codersdkentitlement) | false | | |
|
||||
| `limit` | integer | false | | |
|
||||
|`usage_period`|[codersdk.UsagePeriod](#codersdkusageperiod)|false||Usage period denotes that the usage is a counter that accumulates over this period (and most likely resets with the issuance of the next license).
|
||||
These dates are determined from the license that this entitlement comes from, see enterprise/coderd/license/license.go.
|
||||
Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
|
||||
@@ -678,7 +678,7 @@ func TestManagedAgentLimit(t *testing.T) {
|
||||
// expiry warnings.
|
||||
GraceAt: time.Now().Add(time.Hour * 24 * 60),
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24 * 90),
|
||||
}).ManagedAgentLimit(1, 1),
|
||||
}).ManagedAgentLimit(1),
|
||||
})
|
||||
|
||||
// Get entitlements to check that the license is a-ok.
|
||||
@@ -689,11 +689,7 @@ func TestManagedAgentLimit(t *testing.T) {
|
||||
require.True(t, agentLimit.Enabled)
|
||||
require.NotNil(t, agentLimit.Limit)
|
||||
require.EqualValues(t, 1, *agentLimit.Limit)
|
||||
require.NotNil(t, agentLimit.SoftLimit)
|
||||
require.EqualValues(t, 1, *agentLimit.SoftLimit)
|
||||
require.Empty(t, sdkEntitlements.Errors)
|
||||
// There should be a warning since we're really close to our agent limit.
|
||||
require.Equal(t, sdkEntitlements.Warnings[0], "You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information.")
|
||||
|
||||
// Create a fake provision response that claims there are agents in the
|
||||
// template and every built workspace.
|
||||
|
||||
@@ -231,12 +231,8 @@ func (opts *LicenseOptions) AIGovernanceAddon(limit int64) *LicenseOptions {
|
||||
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.
|
||||
opts = opts.Feature(codersdk.FeatureName("managed_agent_limit_soft"), soft)
|
||||
opts = opts.Feature(codersdk.FeatureName("managed_agent_limit_hard"), hard)
|
||||
return opts
|
||||
func (opts *LicenseOptions) ManagedAgentLimit(limit int64) *LicenseOptions {
|
||||
return opts.Feature(codersdk.FeatureManagedAgentLimit, limit)
|
||||
}
|
||||
|
||||
func (opts *LicenseOptions) Feature(name codersdk.FeatureName, value int64) *LicenseOptions {
|
||||
|
||||
@@ -15,60 +15,9 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
// These features are only included in the license and are not actually
|
||||
// entitlements after the licenses are processed. These values will be
|
||||
// merged into the codersdk.FeatureManagedAgentLimit feature.
|
||||
//
|
||||
// The reason we need two separate features is because the License v3 format
|
||||
// uses map[string]int64 for features, so we're unable to use a single value
|
||||
// with a struct like `{"soft": 100, "hard": 200}`. This is unfortunate and
|
||||
// we should fix this with a new license format v4 in the future.
|
||||
//
|
||||
// These are intentionally not exported as they should not be used outside
|
||||
// of this package (except tests).
|
||||
featureManagedAgentLimitHard codersdk.FeatureName = "managed_agent_limit_hard"
|
||||
featureManagedAgentLimitSoft codersdk.FeatureName = "managed_agent_limit_soft"
|
||||
)
|
||||
|
||||
var (
|
||||
// Mapping of license feature names to the SDK feature name.
|
||||
// This is used to map from multiple usage period features into a single SDK
|
||||
// feature.
|
||||
featureGrouping = map[codersdk.FeatureName]struct {
|
||||
// The parent feature.
|
||||
sdkFeature codersdk.FeatureName
|
||||
// Whether the value of the license feature is the soft limit or the hard
|
||||
// limit.
|
||||
isSoft bool
|
||||
}{
|
||||
// Map featureManagedAgentLimitHard and featureManagedAgentLimitSoft to
|
||||
// codersdk.FeatureManagedAgentLimit.
|
||||
featureManagedAgentLimitHard: {
|
||||
sdkFeature: codersdk.FeatureManagedAgentLimit,
|
||||
isSoft: false,
|
||||
},
|
||||
featureManagedAgentLimitSoft: {
|
||||
sdkFeature: codersdk.FeatureManagedAgentLimit,
|
||||
isSoft: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Features that are forbidden to be set in a license. These are the SDK
|
||||
// features in the usagedBasedFeatureGrouping map.
|
||||
licenseForbiddenFeatures = func() map[codersdk.FeatureName]struct{} {
|
||||
features := make(map[codersdk.FeatureName]struct{})
|
||||
for _, feature := range featureGrouping {
|
||||
features[feature.sdkFeature] = struct{}{}
|
||||
}
|
||||
return features
|
||||
}()
|
||||
)
|
||||
|
||||
// Entitlements processes licenses to return whether features are enabled or not.
|
||||
// TODO(@deansheather): This function and the related LicensesEntitlements
|
||||
// function should be refactored into smaller functions that:
|
||||
@@ -280,17 +229,15 @@ func LicensesEntitlements(
|
||||
// licenses with the corresponding features actually set
|
||||
// trump this default entitlement, even if they are set to a
|
||||
// smaller value.
|
||||
defaultManagedAgentsIsuedAt = time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||
defaultManagedAgentsStart = defaultManagedAgentsIsuedAt
|
||||
defaultManagedAgentsEnd = defaultManagedAgentsStart.AddDate(100, 0, 0)
|
||||
defaultManagedAgentsSoftLimit int64 = 1000
|
||||
defaultManagedAgentsHardLimit int64 = 1000
|
||||
defaultManagedAgentsIsuedAt = time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||
defaultManagedAgentsStart = defaultManagedAgentsIsuedAt
|
||||
defaultManagedAgentsEnd = defaultManagedAgentsStart.AddDate(100, 0, 0)
|
||||
defaultManagedAgentsLimit int64 = 1000
|
||||
)
|
||||
entitlements.AddFeature(codersdk.FeatureManagedAgentLimit, codersdk.Feature{
|
||||
Enabled: true,
|
||||
Entitlement: entitlement,
|
||||
SoftLimit: &defaultManagedAgentsSoftLimit,
|
||||
Limit: &defaultManagedAgentsHardLimit,
|
||||
Limit: &defaultManagedAgentsLimit,
|
||||
UsagePeriod: &codersdk.UsagePeriod{
|
||||
IssuedAt: defaultManagedAgentsIsuedAt,
|
||||
Start: defaultManagedAgentsStart,
|
||||
@@ -310,15 +257,6 @@ func LicensesEntitlements(
|
||||
|
||||
// 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.
|
||||
continue
|
||||
}
|
||||
if _, ok := featureGrouping[featureName]; ok {
|
||||
// These features need very special handling due to merging
|
||||
// multiple feature values into a single SDK feature.
|
||||
continue
|
||||
}
|
||||
if featureName.UsesLimit() || featureName.UsesUsagePeriod() {
|
||||
// Limit and usage period features are handled below.
|
||||
// They don't provide default values as they are always enabled
|
||||
@@ -335,30 +273,24 @@ func LicensesEntitlements(
|
||||
})
|
||||
}
|
||||
|
||||
// A map of SDK feature name to the uncommitted usage feature.
|
||||
uncommittedUsageFeatures := map[codersdk.FeatureName]usageLimit{}
|
||||
|
||||
// Features al-la-carte
|
||||
for featureName, featureValue := range claims.Features {
|
||||
if _, ok := licenseForbiddenFeatures[featureName]; ok {
|
||||
entitlements.Errors = append(entitlements.Errors,
|
||||
fmt.Sprintf("Feature %s is forbidden to be set in a license.", featureName))
|
||||
continue
|
||||
// Old-style licenses encode the managed agent limit as
|
||||
// separate soft/hard features.
|
||||
//
|
||||
// This could be removed in a future release, but can only be
|
||||
// done once all old licenses containing this are no longer in use.
|
||||
if featureName == "managed_agent_limit_soft" {
|
||||
// Maps the soft limit to the canonical feature name
|
||||
featureName = codersdk.FeatureManagedAgentLimit
|
||||
}
|
||||
if featureValue < 0 {
|
||||
// We currently don't use negative values for features.
|
||||
if featureName == "managed_agent_limit_hard" {
|
||||
// We can safely ignore the hard limit as it is no longer used.
|
||||
continue
|
||||
}
|
||||
|
||||
// Special handling for grouped (e.g. usage period) features.
|
||||
if grouping, ok := featureGrouping[featureName]; ok {
|
||||
ul := uncommittedUsageFeatures[grouping.sdkFeature]
|
||||
if grouping.isSoft {
|
||||
ul.Soft = &featureValue
|
||||
} else {
|
||||
ul.Hard = &featureValue
|
||||
}
|
||||
uncommittedUsageFeatures[grouping.sdkFeature] = ul
|
||||
if featureValue < 0 {
|
||||
// We currently don't use negative values for features.
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -372,6 +304,17 @@ func LicensesEntitlements(
|
||||
|
||||
// Handling for limit features.
|
||||
switch {
|
||||
case featureName.UsesUsagePeriod():
|
||||
entitlements.AddFeature(featureName, codersdk.Feature{
|
||||
Enabled: featureValue > 0,
|
||||
Entitlement: entitlement,
|
||||
Limit: &featureValue,
|
||||
UsagePeriod: &codersdk.UsagePeriod{
|
||||
IssuedAt: claims.IssuedAt.Time,
|
||||
Start: usagePeriodStart,
|
||||
End: usagePeriodEnd,
|
||||
},
|
||||
})
|
||||
case featureName.UsesLimit():
|
||||
if featureValue <= 0 {
|
||||
// 0 limit value or less doesn't make sense, so we skip it.
|
||||
@@ -402,46 +345,6 @@ func LicensesEntitlements(
|
||||
}
|
||||
}
|
||||
|
||||
// Apply uncommitted usage features to the entitlements.
|
||||
for featureName, ul := range uncommittedUsageFeatures {
|
||||
if ul.Soft == nil || ul.Hard == nil {
|
||||
// Invalid license.
|
||||
entitlements.Errors = append(entitlements.Errors,
|
||||
fmt.Sprintf("Invalid license (%s): feature %s has missing soft or hard limit values", license.UUID.String(), featureName))
|
||||
continue
|
||||
}
|
||||
if *ul.Hard < *ul.Soft {
|
||||
entitlements.Errors = append(entitlements.Errors,
|
||||
fmt.Sprintf("Invalid license (%s): feature %s has a hard limit less than the soft limit", license.UUID.String(), featureName))
|
||||
continue
|
||||
}
|
||||
if *ul.Hard < 0 || *ul.Soft < 0 {
|
||||
entitlements.Errors = append(entitlements.Errors,
|
||||
fmt.Sprintf("Invalid license (%s): feature %s has a soft or hard limit less than 0", license.UUID.String(), featureName))
|
||||
continue
|
||||
}
|
||||
|
||||
feature := codersdk.Feature{
|
||||
Enabled: true,
|
||||
Entitlement: entitlement,
|
||||
SoftLimit: ul.Soft,
|
||||
Limit: ul.Hard,
|
||||
// `Actual` will be populated below when warnings are generated.
|
||||
UsagePeriod: &codersdk.UsagePeriod{
|
||||
IssuedAt: claims.IssuedAt.Time,
|
||||
Start: usagePeriodStart,
|
||||
End: usagePeriodEnd,
|
||||
},
|
||||
}
|
||||
// If the hard limit is 0, the feature is disabled.
|
||||
if *ul.Hard <= 0 {
|
||||
feature.Enabled = false
|
||||
feature.SoftLimit = ptr.Ref(int64(0))
|
||||
feature.Limit = ptr.Ref(int64(0))
|
||||
}
|
||||
entitlements.AddFeature(featureName, feature)
|
||||
}
|
||||
|
||||
addonFeatures := make(map[codersdk.FeatureName]codersdk.Feature)
|
||||
|
||||
// Finally, add all features from the addons. We do this last so that
|
||||
@@ -557,32 +460,9 @@ func LicensesEntitlements(
|
||||
entitlements.AddFeature(codersdk.FeatureManagedAgentLimit, agentLimit)
|
||||
|
||||
// Only issue warnings if the feature is enabled.
|
||||
if agentLimit.Enabled {
|
||||
var softLimit int64
|
||||
if agentLimit.SoftLimit != nil {
|
||||
softLimit = *agentLimit.SoftLimit
|
||||
}
|
||||
var hardLimit int64
|
||||
if agentLimit.Limit != nil {
|
||||
hardLimit = *agentLimit.Limit
|
||||
}
|
||||
|
||||
// Issue a warning early:
|
||||
// 1. If the soft limit and hard limit are equal, at 75% of the hard
|
||||
// limit.
|
||||
// 2. If the limit is greater than the soft limit, at 75% of the
|
||||
// difference between the hard limit and the soft limit.
|
||||
softWarningThreshold := int64(float64(hardLimit) * 0.75)
|
||||
if hardLimit > softLimit && softLimit > 0 {
|
||||
softWarningThreshold = softLimit + int64(float64(hardLimit-softLimit)*0.75)
|
||||
}
|
||||
if managedAgentCount >= *agentLimit.Limit {
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
codersdk.LicenseManagedAgentLimitExceededWarningText)
|
||||
} else if managedAgentCount >= softWarningThreshold {
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
"You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information.")
|
||||
}
|
||||
if agentLimit.Enabled && agentLimit.Limit != nil && managedAgentCount >= *agentLimit.Limit {
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
codersdk.LicenseManagedAgentLimitExceededWarningText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,11 +563,6 @@ var (
|
||||
|
||||
type Features map[codersdk.FeatureName]int64
|
||||
|
||||
type usageLimit struct {
|
||||
Soft *int64
|
||||
Hard *int64 // 0 means "disabled"
|
||||
}
|
||||
|
||||
// Claims is the full set of claims in a license.
|
||||
type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
|
||||
@@ -76,8 +76,7 @@ func TestEntitlements(t *testing.T) {
|
||||
f := make(license.Features)
|
||||
for _, name := range codersdk.FeatureNames {
|
||||
if name == codersdk.FeatureManagedAgentLimit {
|
||||
f[codersdk.FeatureName("managed_agent_limit_soft")] = 100
|
||||
f[codersdk.FeatureName("managed_agent_limit_hard")] = 200
|
||||
f[codersdk.FeatureManagedAgentLimit] = 100
|
||||
continue
|
||||
}
|
||||
f[name] = 1
|
||||
@@ -533,8 +532,7 @@ func TestEntitlements(t *testing.T) {
|
||||
t.Run("Premium", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
const userLimit = 1
|
||||
const expectedAgentSoftLimit = 1000
|
||||
const expectedAgentHardLimit = 1000
|
||||
const expectedAgentLimit = 1000
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
licenseOptions := coderdenttest.LicenseOptions{
|
||||
@@ -566,8 +564,7 @@ func TestEntitlements(t *testing.T) {
|
||||
agentEntitlement := entitlements.Features[featureName]
|
||||
require.True(t, agentEntitlement.Enabled)
|
||||
require.Equal(t, codersdk.EntitlementEntitled, agentEntitlement.Entitlement)
|
||||
require.EqualValues(t, expectedAgentSoftLimit, *agentEntitlement.SoftLimit)
|
||||
require.EqualValues(t, expectedAgentHardLimit, *agentEntitlement.Limit)
|
||||
require.EqualValues(t, expectedAgentLimit, *agentEntitlement.Limit)
|
||||
|
||||
// This might be shocking, but there's a sound reason for this.
|
||||
// See license.go for more details.
|
||||
@@ -840,7 +837,7 @@ func TestEntitlements(t *testing.T) {
|
||||
},
|
||||
}).
|
||||
UserLimit(100).
|
||||
ManagedAgentLimit(100, 200)
|
||||
ManagedAgentLimit(100)
|
||||
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
@@ -882,16 +879,15 @@ 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)
|
||||
require.EqualValues(t, 200, *managedAgentLimit.Limit)
|
||||
// The soft limit value (100) is used as the single Limit.
|
||||
require.EqualValues(t, 100, *managedAgentLimit.Limit)
|
||||
require.NotNil(t, managedAgentLimit.Actual)
|
||||
require.EqualValues(t, 175, *managedAgentLimit.Actual)
|
||||
|
||||
// Should've also populated a warning.
|
||||
// Usage exceeds the limit, so an exceeded warning should be present.
|
||||
require.Len(t, entitlements.Warnings, 1)
|
||||
require.Equal(t, "You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information.", entitlements.Warnings[0])
|
||||
require.Equal(t, codersdk.LicenseManagedAgentLimitExceededWarningText, entitlements.Warnings[0])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1121,13 +1117,12 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
{
|
||||
Name: "ManagedAgentLimit",
|
||||
Licenses: []*coderdenttest.LicenseOptions{
|
||||
enterpriseLicense().UserLimit(100).ManagedAgentLimit(100, 200),
|
||||
enterpriseLicense().UserLimit(100).ManagedAgentLimit(100),
|
||||
},
|
||||
Arguments: license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
// 175 will generate a warning as it's over 75% of the
|
||||
// difference between the soft and hard limit.
|
||||
return 174, nil
|
||||
// 74 is below the limit (soft=100), so no warning.
|
||||
return 74, nil
|
||||
},
|
||||
},
|
||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
@@ -1136,9 +1131,9 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
assert.True(t, feature.Enabled)
|
||||
assert.Equal(t, int64(100), *feature.SoftLimit)
|
||||
assert.Equal(t, int64(200), *feature.Limit)
|
||||
assert.Equal(t, int64(174), *feature.Actual)
|
||||
// Soft limit value is used as the single Limit.
|
||||
assert.Equal(t, int64(100), *feature.Limit)
|
||||
assert.Equal(t, int64(74), *feature.Actual)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1151,7 +1146,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
WithIssuedAt(time.Now().Add(-time.Hour * 2)),
|
||||
enterpriseLicense().
|
||||
UserLimit(100).
|
||||
ManagedAgentLimit(100, 100).
|
||||
ManagedAgentLimit(100).
|
||||
WithIssuedAt(time.Now().Add(-time.Hour * 1)).
|
||||
GracePeriod(time.Now()),
|
||||
},
|
||||
@@ -1168,7 +1163,6 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
assert.Equal(t, codersdk.EntitlementGracePeriod, feature.Entitlement)
|
||||
assert.True(t, feature.Enabled)
|
||||
assert.Equal(t, int64(100), *feature.SoftLimit)
|
||||
assert.Equal(t, int64(100), *feature.Limit)
|
||||
assert.Equal(t, int64(74), *feature.Actual)
|
||||
},
|
||||
@@ -1183,7 +1177,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
WithIssuedAt(time.Now().Add(-time.Hour * 2)),
|
||||
enterpriseLicense().
|
||||
UserLimit(100).
|
||||
ManagedAgentLimit(100, 200).
|
||||
ManagedAgentLimit(100).
|
||||
WithIssuedAt(time.Now().Add(-time.Hour * 1)).
|
||||
Expired(time.Now()),
|
||||
},
|
||||
@@ -1196,84 +1190,33 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
assert.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
||||
assert.False(t, feature.Enabled)
|
||||
assert.Nil(t, feature.SoftLimit)
|
||||
assert.Nil(t, feature.Limit)
|
||||
assert.Nil(t, feature.Actual)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ManagedAgentLimitWarning/ApproachingLimit/DifferentSoftAndHardLimit",
|
||||
Name: "ManagedAgentLimitWarning/ExceededLimit",
|
||||
Licenses: []*coderdenttest.LicenseOptions{
|
||||
enterpriseLicense().
|
||||
UserLimit(100).
|
||||
ManagedAgentLimit(100, 200),
|
||||
ManagedAgentLimit(100),
|
||||
},
|
||||
Arguments: license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
return 175, nil
|
||||
return 150, nil
|
||||
},
|
||||
},
|
||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
assert.Len(t, entitlements.Warnings, 1)
|
||||
assert.Equal(t, "You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information.", entitlements.Warnings[0])
|
||||
assert.Equal(t, codersdk.LicenseManagedAgentLimitExceededWarningText, entitlements.Warnings[0])
|
||||
assertNoErrors(t, entitlements)
|
||||
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
assert.True(t, feature.Enabled)
|
||||
assert.Equal(t, int64(100), *feature.SoftLimit)
|
||||
assert.Equal(t, int64(200), *feature.Limit)
|
||||
assert.Equal(t, int64(175), *feature.Actual)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ManagedAgentLimitWarning/ApproachingLimit/EqualSoftAndHardLimit",
|
||||
Licenses: []*coderdenttest.LicenseOptions{
|
||||
enterpriseLicense().
|
||||
UserLimit(100).
|
||||
ManagedAgentLimit(100, 100),
|
||||
},
|
||||
Arguments: license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
return 75, nil
|
||||
},
|
||||
},
|
||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
assert.Len(t, entitlements.Warnings, 1)
|
||||
assert.Equal(t, "You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information.", entitlements.Warnings[0])
|
||||
assertNoErrors(t, entitlements)
|
||||
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
assert.True(t, feature.Enabled)
|
||||
assert.Equal(t, int64(100), *feature.SoftLimit)
|
||||
// Soft limit (100) is used as the single Limit.
|
||||
assert.Equal(t, int64(100), *feature.Limit)
|
||||
assert.Equal(t, int64(75), *feature.Actual)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ManagedAgentLimitWarning/BreachedLimit",
|
||||
Licenses: []*coderdenttest.LicenseOptions{
|
||||
enterpriseLicense().
|
||||
UserLimit(100).
|
||||
ManagedAgentLimit(100, 200),
|
||||
},
|
||||
Arguments: license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
return 200, nil
|
||||
},
|
||||
},
|
||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
assert.Len(t, entitlements.Warnings, 1)
|
||||
assert.Equal(t, "You have built more workspaces with managed agents than your license allows.", entitlements.Warnings[0])
|
||||
assertNoErrors(t, entitlements)
|
||||
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
assert.True(t, feature.Enabled)
|
||||
assert.Equal(t, int64(100), *feature.SoftLimit)
|
||||
assert.Equal(t, int64(200), *feature.Limit)
|
||||
assert.Equal(t, int64(200), *feature.Actual)
|
||||
assert.Equal(t, int64(150), *feature.Actual)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1472,173 +1415,240 @@ func TestAIBridgeSoftWarning(t *testing.T) {
|
||||
func TestUsageLimitFeatures(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
sdkFeatureName codersdk.FeatureName
|
||||
softLimitFeatureName codersdk.FeatureName
|
||||
hardLimitFeatureName codersdk.FeatureName
|
||||
}{
|
||||
{
|
||||
sdkFeatureName: codersdk.FeatureManagedAgentLimit,
|
||||
softLimitFeatureName: codersdk.FeatureName("managed_agent_limit_soft"),
|
||||
hardLimitFeatureName: codersdk.FeatureName("managed_agent_limit_hard"),
|
||||
},
|
||||
}
|
||||
// Ensures that usage limit features are ranked by issued at, not by
|
||||
// values.
|
||||
t.Run("IssuedAtRanking", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(string(c.sdkFeatureName), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Generate 2 real licenses both with managed agent limit
|
||||
// features. lic2 should trump lic1 even though it has a lower
|
||||
// limit, because it was issued later.
|
||||
lic1 := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
IssuedAt: time.Now().Add(-time.Minute * 2),
|
||||
NotBefore: time.Now().Add(-time.Minute * 2),
|
||||
ExpiresAt: time.Now().Add(time.Hour * 2),
|
||||
Features: license.Features{
|
||||
codersdk.FeatureManagedAgentLimit: 100,
|
||||
},
|
||||
}),
|
||||
}
|
||||
lic2Iat := time.Now().Add(-time.Minute * 1)
|
||||
lic2Nbf := lic2Iat.Add(-time.Minute)
|
||||
lic2Exp := lic2Iat.Add(time.Hour)
|
||||
lic2 := database.License{
|
||||
ID: 2,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: lic2Exp,
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
IssuedAt: lic2Iat,
|
||||
NotBefore: lic2Nbf,
|
||||
ExpiresAt: lic2Exp,
|
||||
Features: license.Features{
|
||||
codersdk.FeatureManagedAgentLimit: 50,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
// Test for either a missing soft or hard limit feature value.
|
||||
t.Run("MissingGroupedFeature", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
const actualAgents = 10
|
||||
arguments := license.FeatureArguments{
|
||||
ActiveUserCount: 10,
|
||||
ReplicaCount: 0,
|
||||
ExternalAuthCount: 0,
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
return actualAgents, nil
|
||||
},
|
||||
}
|
||||
|
||||
for _, feature := range []codersdk.FeatureName{
|
||||
c.softLimitFeatureName,
|
||||
c.hardLimitFeatureName,
|
||||
} {
|
||||
t.Run(string(feature), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Load the licenses in both orders to ensure the correct
|
||||
// behavior is observed no matter the order.
|
||||
for _, order := range [][]database.License{
|
||||
{lic1, lic2},
|
||||
{lic2, lic1},
|
||||
} {
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), order, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
||||
require.NoError(t, err)
|
||||
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
feature: 100,
|
||||
},
|
||||
}),
|
||||
}
|
||||
feature, ok := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
require.NotNil(t, feature.Limit)
|
||||
require.EqualValues(t, 50, *feature.Limit)
|
||||
require.NotNil(t, feature.Actual)
|
||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||
require.NotNil(t, feature.UsagePeriod)
|
||||
require.WithinDuration(t, lic2Iat, feature.UsagePeriod.IssuedAt, 2*time.Second)
|
||||
require.WithinDuration(t, lic2Nbf, feature.UsagePeriod.Start, 2*time.Second)
|
||||
require.WithinDuration(t, lic2Exp, feature.UsagePeriod.End, 2*time.Second)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
arguments := license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), []database.License{lic}, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
||||
require.NoError(t, err)
|
||||
// TestOldStyleManagedAgentLicenses ensures backward compatibility with
|
||||
// older licenses that encode the managed agent limit using separate
|
||||
// "managed_agent_limit_soft" and "managed_agent_limit_hard" feature keys
|
||||
// instead of the canonical "managed_agent_limit" key.
|
||||
func TestOldStyleManagedAgentLicenses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
feature, ok := entitlements.Features[c.sdkFeatureName]
|
||||
require.True(t, ok, "feature %s not found", c.sdkFeatureName)
|
||||
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
||||
t.Run("SoftAndHard", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Len(t, entitlements.Errors, 1)
|
||||
require.Equal(t, fmt.Sprintf("Invalid license (%v): feature %s has missing soft or hard limit values", lic.UUID, c.sdkFeatureName), entitlements.Errors[0])
|
||||
})
|
||||
}
|
||||
})
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureName("managed_agent_limit_soft"): 100,
|
||||
codersdk.FeatureName("managed_agent_limit_hard"): 200,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
t.Run("HardBelowSoft", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
const actualAgents = 42
|
||||
arguments := license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(_ context.Context, _, _ time.Time) (int64, error) {
|
||||
return actualAgents, nil
|
||||
},
|
||||
}
|
||||
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
c.softLimitFeatureName: 100,
|
||||
c.hardLimitFeatureName: 50,
|
||||
},
|
||||
}),
|
||||
}
|
||||
entitlements, err := license.LicensesEntitlements(
|
||||
context.Background(), time.Now(), []database.License{lic},
|
||||
map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, entitlements.Errors)
|
||||
|
||||
arguments := license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), []database.License{lic}, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
||||
require.NoError(t, err)
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
require.True(t, feature.Enabled)
|
||||
require.NotNil(t, feature.Limit)
|
||||
// The soft limit should be used as the canonical limit.
|
||||
require.EqualValues(t, 100, *feature.Limit)
|
||||
require.NotNil(t, feature.Actual)
|
||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||
require.NotNil(t, feature.UsagePeriod)
|
||||
})
|
||||
|
||||
feature, ok := entitlements.Features[c.sdkFeatureName]
|
||||
require.True(t, ok, "feature %s not found", c.sdkFeatureName)
|
||||
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
||||
t.Run("OnlySoft", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Len(t, entitlements.Errors, 1)
|
||||
require.Equal(t, fmt.Sprintf("Invalid license (%v): feature %s has a hard limit less than the soft limit", lic.UUID, c.sdkFeatureName), entitlements.Errors[0])
|
||||
})
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureName("managed_agent_limit_soft"): 75,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
// Ensures that these features are ranked by issued at, not by
|
||||
// values.
|
||||
t.Run("IssuedAtRanking", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
const actualAgents = 10
|
||||
arguments := license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(_ context.Context, _, _ time.Time) (int64, error) {
|
||||
return actualAgents, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Generate 2 real licenses both with managed agent limit
|
||||
// features. lic2 should trump lic1 even though it has a lower
|
||||
// limit, because it was issued later.
|
||||
lic1 := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
IssuedAt: time.Now().Add(-time.Minute * 2),
|
||||
NotBefore: time.Now().Add(-time.Minute * 2),
|
||||
ExpiresAt: time.Now().Add(time.Hour * 2),
|
||||
Features: license.Features{
|
||||
c.softLimitFeatureName: 100,
|
||||
c.hardLimitFeatureName: 200,
|
||||
},
|
||||
}),
|
||||
}
|
||||
lic2Iat := time.Now().Add(-time.Minute * 1)
|
||||
lic2Nbf := lic2Iat.Add(-time.Minute)
|
||||
lic2Exp := lic2Iat.Add(time.Hour)
|
||||
lic2 := database.License{
|
||||
ID: 2,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: lic2Exp,
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
IssuedAt: lic2Iat,
|
||||
NotBefore: lic2Nbf,
|
||||
ExpiresAt: lic2Exp,
|
||||
Features: license.Features{
|
||||
c.softLimitFeatureName: 50,
|
||||
c.hardLimitFeatureName: 100,
|
||||
},
|
||||
}),
|
||||
}
|
||||
entitlements, err := license.LicensesEntitlements(
|
||||
context.Background(), time.Now(), []database.License{lic},
|
||||
map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, entitlements.Errors)
|
||||
|
||||
const actualAgents = 10
|
||||
arguments := license.FeatureArguments{
|
||||
ActiveUserCount: 10,
|
||||
ReplicaCount: 0,
|
||||
ExternalAuthCount: 0,
|
||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
||||
return actualAgents, nil
|
||||
},
|
||||
}
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
require.True(t, feature.Enabled)
|
||||
require.NotNil(t, feature.Limit)
|
||||
require.EqualValues(t, 75, *feature.Limit)
|
||||
})
|
||||
|
||||
// Load the licenses in both orders to ensure the correct
|
||||
// behavior is observed no matter the order.
|
||||
for _, order := range [][]database.License{
|
||||
{lic1, lic2},
|
||||
{lic2, lic1},
|
||||
} {
|
||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), order, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
||||
require.NoError(t, err)
|
||||
// A license with only the hard limit key should silently ignore it,
|
||||
// leaving the feature unset (not entitled).
|
||||
t.Run("OnlyHard", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
feature, ok := entitlements.Features[c.sdkFeatureName]
|
||||
require.True(t, ok, "feature %s not found", c.sdkFeatureName)
|
||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
require.NotNil(t, feature.Limit)
|
||||
require.EqualValues(t, 100, *feature.Limit)
|
||||
require.NotNil(t, feature.SoftLimit)
|
||||
require.EqualValues(t, 50, *feature.SoftLimit)
|
||||
require.NotNil(t, feature.Actual)
|
||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||
require.NotNil(t, feature.UsagePeriod)
|
||||
require.WithinDuration(t, lic2Iat, feature.UsagePeriod.IssuedAt, 2*time.Second)
|
||||
require.WithinDuration(t, lic2Nbf, feature.UsagePeriod.Start, 2*time.Second)
|
||||
require.WithinDuration(t, lic2Exp, feature.UsagePeriod.End, 2*time.Second)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureName("managed_agent_limit_hard"): 200,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
arguments := license.FeatureArguments{
|
||||
ManagedAgentCountFn: func(_ context.Context, _, _ time.Time) (int64, error) {
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
entitlements, err := license.LicensesEntitlements(
|
||||
context.Background(), time.Now(), []database.License{lic},
|
||||
map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, entitlements.Errors)
|
||||
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
||||
})
|
||||
|
||||
// Old-style license with both soft and hard set to zero should
|
||||
// explicitly disable the feature (and override any Premium default).
|
||||
t.Run("ExplicitZero", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
Exp: time.Now().Add(time.Hour),
|
||||
UUID: uuid.New(),
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Features: license.Features{
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureName("managed_agent_limit_soft"): 0,
|
||||
codersdk.FeatureName("managed_agent_limit_hard"): 0,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const actualAgents = 5
|
||||
arguments := license.FeatureArguments{
|
||||
ActiveUserCount: 10,
|
||||
ManagedAgentCountFn: func(_ context.Context, _, _ time.Time) (int64, error) {
|
||||
return actualAgents, nil
|
||||
},
|
||||
}
|
||||
|
||||
entitlements, err := license.LicensesEntitlements(
|
||||
context.Background(), time.Now(), []database.License{lic},
|
||||
map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
require.False(t, feature.Enabled)
|
||||
require.NotNil(t, feature.Limit)
|
||||
require.EqualValues(t, 0, *feature.Limit)
|
||||
require.NotNil(t, feature.Actual)
|
||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
@@ -1676,20 +1686,16 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
||||
require.Nil(t, feature.Limit)
|
||||
require.Nil(t, feature.SoftLimit)
|
||||
require.Nil(t, feature.Actual)
|
||||
require.Nil(t, feature.UsagePeriod)
|
||||
})
|
||||
|
||||
// "Premium" licenses should receive a default managed agent limit of:
|
||||
// soft = 1000
|
||||
// hard = 1000
|
||||
// "Premium" licenses should receive a default managed agent limit of 1000.
|
||||
t.Run("Premium", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const userLimit = 33
|
||||
const softLimit = 1000
|
||||
const hardLimit = 1000
|
||||
const defaultLimit = 1000
|
||||
lic := database.License{
|
||||
ID: 1,
|
||||
UploadedAt: time.Now(),
|
||||
@@ -1720,9 +1726,7 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
require.NotNil(t, feature.Limit)
|
||||
require.EqualValues(t, hardLimit, *feature.Limit)
|
||||
require.NotNil(t, feature.SoftLimit)
|
||||
require.EqualValues(t, softLimit, *feature.SoftLimit)
|
||||
require.EqualValues(t, defaultLimit, *feature.Limit)
|
||||
require.NotNil(t, feature.Actual)
|
||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||
require.NotNil(t, feature.UsagePeriod)
|
||||
@@ -1731,8 +1735,8 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
require.NotZero(t, feature.UsagePeriod.End)
|
||||
})
|
||||
|
||||
// "Premium" licenses with an explicit managed agent limit should not
|
||||
// receive a default managed agent limit.
|
||||
// "Premium" licenses with an explicit managed agent limit should use
|
||||
// that value instead of the default.
|
||||
t.Run("PremiumExplicitValues", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1744,9 +1748,8 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Features: license.Features{
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureName("managed_agent_limit_soft"): 100,
|
||||
codersdk.FeatureName("managed_agent_limit_hard"): 200,
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureManagedAgentLimit: 100,
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -1768,9 +1771,7 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||
require.NotNil(t, feature.Limit)
|
||||
require.EqualValues(t, 200, *feature.Limit)
|
||||
require.NotNil(t, feature.SoftLimit)
|
||||
require.EqualValues(t, 100, *feature.SoftLimit)
|
||||
require.EqualValues(t, 100, *feature.Limit)
|
||||
require.NotNil(t, feature.Actual)
|
||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||
require.NotNil(t, feature.UsagePeriod)
|
||||
@@ -1792,9 +1793,8 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
FeatureSet: codersdk.FeatureSetPremium,
|
||||
Features: license.Features{
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureName("managed_agent_limit_soft"): 0,
|
||||
codersdk.FeatureName("managed_agent_limit_hard"): 0,
|
||||
codersdk.FeatureUserLimit: 100,
|
||||
codersdk.FeatureManagedAgentLimit: 0,
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -1818,8 +1818,6 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
||||
require.False(t, feature.Enabled)
|
||||
require.NotNil(t, feature.Limit)
|
||||
require.EqualValues(t, 0, *feature.Limit)
|
||||
require.NotNil(t, feature.SoftLimit)
|
||||
require.EqualValues(t, 0, *feature.SoftLimit)
|
||||
require.NotNil(t, feature.Actual)
|
||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||
require.NotNil(t, feature.UsagePeriod)
|
||||
|
||||
@@ -4728,7 +4728,7 @@ func TestWorkspaceAITask(t *testing.T) {
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}).ManagedAgentLimit(10, 20),
|
||||
}).ManagedAgentLimit(10),
|
||||
})
|
||||
|
||||
client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID,
|
||||
|
||||
Generated
-6
@@ -2112,12 +2112,6 @@ export interface Feature {
|
||||
readonly enabled: boolean;
|
||||
readonly limit?: number;
|
||||
readonly actual?: number;
|
||||
/**
|
||||
* SoftLimit is the soft limit of the feature, and is only used for showing
|
||||
* included limits in the dashboard. No license validation or warnings are
|
||||
* generated from this value.
|
||||
*/
|
||||
readonly soft_limit?: number;
|
||||
/**
|
||||
* UsagePeriod denotes that the usage is a counter that accumulates over
|
||||
* this period (and most likely resets with the issuance of the next
|
||||
|
||||
+10
-54
@@ -9,8 +9,7 @@ const meta: Meta<typeof ManagedAgentsConsumption> = {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 50000,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
@@ -31,8 +30,7 @@ export const ZeroUsage: Story = {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 0,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
@@ -47,9 +45,8 @@ export const NearLimit: Story = {
|
||||
args: {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 115000,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
actual: 55000,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
@@ -60,13 +57,12 @@ export const NearLimit: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const OverIncluded: Story = {
|
||||
export const OverLimit: Story = {
|
||||
args: {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 80000,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
@@ -82,25 +78,7 @@ export const LowUsage: Story = {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 25000,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
issued_at: "February 27, 2025",
|
||||
},
|
||||
entitlement: "entitled",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const IncludedAtLimit: Story = {
|
||||
args: {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 25000,
|
||||
soft_limit: 30500,
|
||||
limit: 30500,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
@@ -116,7 +94,6 @@ export const Disabled: Story = {
|
||||
managedAgentFeature: {
|
||||
enabled: false,
|
||||
actual: undefined,
|
||||
soft_limit: undefined,
|
||||
limit: undefined,
|
||||
usage_period: undefined,
|
||||
entitlement: "not_entitled",
|
||||
@@ -136,7 +113,6 @@ export const ErrorMissingData: Story = {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: undefined,
|
||||
soft_limit: undefined,
|
||||
limit: undefined,
|
||||
usage_period: undefined,
|
||||
entitlement: "entitled",
|
||||
@@ -149,25 +125,7 @@ export const ErrorNegativeValues: Story = {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: -100,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
issued_at: "February 27, 2025",
|
||||
},
|
||||
entitlement: "entitled",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorSoftLimitExceedsLimit: Story = {
|
||||
args: {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 50000,
|
||||
soft_limit: 150000,
|
||||
limit: 120000,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "February 27, 2025",
|
||||
end: "February 27, 2026",
|
||||
@@ -183,8 +141,7 @@ export const ErrorInvalidDates: Story = {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 50000,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "invalid-date",
|
||||
end: "February 27, 2026",
|
||||
@@ -200,8 +157,7 @@ export const ErrorEndBeforeStart: Story = {
|
||||
managedAgentFeature: {
|
||||
enabled: true,
|
||||
actual: 50000,
|
||||
soft_limit: 60000,
|
||||
limit: 120000,
|
||||
limit: 60000,
|
||||
usage_period: {
|
||||
start: "February 27, 2026",
|
||||
end: "February 27, 2025",
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
}
|
||||
|
||||
const usage = managedAgentFeature.actual;
|
||||
const included = managedAgentFeature.soft_limit;
|
||||
const included = managedAgentFeature.limit;
|
||||
const startDate = managedAgentFeature.usage_period?.start;
|
||||
const endDate = managedAgentFeature.usage_period?.end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user