mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +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": {
|
"limit": {
|
||||||
"type": "integer"
|
"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": {
|
"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",
|
"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": [
|
"allOf": [
|
||||||
|
|||||||
Generated
-4
@@ -13895,10 +13895,6 @@
|
|||||||
"limit": {
|
"limit": {
|
||||||
"type": "integer"
|
"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": {
|
"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",
|
"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": [
|
"allOf": [
|
||||||
|
|||||||
@@ -372,10 +372,6 @@ type Feature struct {
|
|||||||
|
|
||||||
// Below is only for features that use usage periods.
|
// 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
|
// UsagePeriod denotes that the usage is a counter that accumulates over
|
||||||
// this period (and most likely resets with the issuance of the next
|
// this period (and most likely resets with the issuance of the next
|
||||||
// license).
|
// license).
|
||||||
|
|||||||
Generated
-2
@@ -329,7 +329,6 @@ curl -X GET http://coder-server:8080/api/v2/entitlements \
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"entitlement": "entitled",
|
"entitlement": "entitled",
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
"soft_limit": 0,
|
|
||||||
"usage_period": {
|
"usage_period": {
|
||||||
"end": "2019-08-24T14:15:22Z",
|
"end": "2019-08-24T14:15:22Z",
|
||||||
"issued_at": "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,
|
"enabled": true,
|
||||||
"entitlement": "entitled",
|
"entitlement": "entitled",
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
"soft_limit": 0,
|
|
||||||
"usage_period": {
|
"usage_period": {
|
||||||
"end": "2019-08-24T14:15:22Z",
|
"end": "2019-08-24T14:15:22Z",
|
||||||
"issued_at": "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,
|
"enabled": true,
|
||||||
"entitlement": "entitled",
|
"entitlement": "entitled",
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
"soft_limit": 0,
|
|
||||||
"usage_period": {
|
"usage_period": {
|
||||||
"end": "2019-08-24T14:15:22Z",
|
"end": "2019-08-24T14:15:22Z",
|
||||||
"issued_at": "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,
|
"enabled": true,
|
||||||
"entitlement": "entitled",
|
"entitlement": "entitled",
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
"soft_limit": 0,
|
|
||||||
"usage_period": {
|
"usage_period": {
|
||||||
"end": "2019-08-24T14:15:22Z",
|
"end": "2019-08-24T14:15:22Z",
|
||||||
"issued_at": "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,
|
"enabled": true,
|
||||||
"entitlement": "entitled",
|
"entitlement": "entitled",
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
"soft_limit": 0,
|
|
||||||
"usage_period": {
|
"usage_period": {
|
||||||
"end": "2019-08-24T14:15:22Z",
|
"end": "2019-08-24T14:15:22Z",
|
||||||
"issued_at": "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
|
### Properties
|
||||||
|
|
||||||
| Name | Type | Required | Restrictions | Description |
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|---------------|----------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|----------------------------------------------|----------|--------------|-------------|
|
||||||
| `actual` | integer | false | | |
|
| `actual` | integer | false | | |
|
||||||
| `enabled` | boolean | false | | |
|
| `enabled` | boolean | false | | |
|
||||||
| `entitlement` | [codersdk.Entitlement](#codersdkentitlement) | false | | |
|
| `entitlement` | [codersdk.Entitlement](#codersdkentitlement) | false | | |
|
||||||
| `limit` | integer | 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. |
|
|
||||||
|`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).
|
|`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.
|
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|
|
Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ func TestManagedAgentLimit(t *testing.T) {
|
|||||||
// expiry warnings.
|
// expiry warnings.
|
||||||
GraceAt: time.Now().Add(time.Hour * 24 * 60),
|
GraceAt: time.Now().Add(time.Hour * 24 * 60),
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 24 * 90),
|
ExpiresAt: time.Now().Add(time.Hour * 24 * 90),
|
||||||
}).ManagedAgentLimit(1, 1),
|
}).ManagedAgentLimit(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get entitlements to check that the license is a-ok.
|
// 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.True(t, agentLimit.Enabled)
|
||||||
require.NotNil(t, agentLimit.Limit)
|
require.NotNil(t, agentLimit.Limit)
|
||||||
require.EqualValues(t, 1, *agentLimit.Limit)
|
require.EqualValues(t, 1, *agentLimit.Limit)
|
||||||
require.NotNil(t, agentLimit.SoftLimit)
|
|
||||||
require.EqualValues(t, 1, *agentLimit.SoftLimit)
|
|
||||||
require.Empty(t, sdkEntitlements.Errors)
|
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
|
// Create a fake provision response that claims there are agents in the
|
||||||
// template and every built workspace.
|
// template and every built workspace.
|
||||||
|
|||||||
@@ -231,12 +231,8 @@ func (opts *LicenseOptions) AIGovernanceAddon(limit int64) *LicenseOptions {
|
|||||||
return opts.Feature(codersdk.FeatureAIGovernanceUserLimit, limit)
|
return opts.Feature(codersdk.FeatureAIGovernanceUserLimit, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *LicenseOptions) ManagedAgentLimit(soft int64, hard int64) *LicenseOptions {
|
func (opts *LicenseOptions) ManagedAgentLimit(limit int64) *LicenseOptions {
|
||||||
// These don't use named or exported feature names, see
|
return opts.Feature(codersdk.FeatureManagedAgentLimit, limit)
|
||||||
// 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) Feature(name codersdk.FeatureName, value int64) *LicenseOptions {
|
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"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"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.
|
// Entitlements processes licenses to return whether features are enabled or not.
|
||||||
// TODO(@deansheather): This function and the related LicensesEntitlements
|
// TODO(@deansheather): This function and the related LicensesEntitlements
|
||||||
// function should be refactored into smaller functions that:
|
// function should be refactored into smaller functions that:
|
||||||
@@ -280,17 +229,15 @@ func LicensesEntitlements(
|
|||||||
// licenses with the corresponding features actually set
|
// licenses with the corresponding features actually set
|
||||||
// trump this default entitlement, even if they are set to a
|
// trump this default entitlement, even if they are set to a
|
||||||
// smaller value.
|
// smaller value.
|
||||||
defaultManagedAgentsIsuedAt = time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC)
|
defaultManagedAgentsIsuedAt = time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||||
defaultManagedAgentsStart = defaultManagedAgentsIsuedAt
|
defaultManagedAgentsStart = defaultManagedAgentsIsuedAt
|
||||||
defaultManagedAgentsEnd = defaultManagedAgentsStart.AddDate(100, 0, 0)
|
defaultManagedAgentsEnd = defaultManagedAgentsStart.AddDate(100, 0, 0)
|
||||||
defaultManagedAgentsSoftLimit int64 = 1000
|
defaultManagedAgentsLimit int64 = 1000
|
||||||
defaultManagedAgentsHardLimit int64 = 1000
|
|
||||||
)
|
)
|
||||||
entitlements.AddFeature(codersdk.FeatureManagedAgentLimit, codersdk.Feature{
|
entitlements.AddFeature(codersdk.FeatureManagedAgentLimit, codersdk.Feature{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Entitlement: entitlement,
|
Entitlement: entitlement,
|
||||||
SoftLimit: &defaultManagedAgentsSoftLimit,
|
Limit: &defaultManagedAgentsLimit,
|
||||||
Limit: &defaultManagedAgentsHardLimit,
|
|
||||||
UsagePeriod: &codersdk.UsagePeriod{
|
UsagePeriod: &codersdk.UsagePeriod{
|
||||||
IssuedAt: defaultManagedAgentsIsuedAt,
|
IssuedAt: defaultManagedAgentsIsuedAt,
|
||||||
Start: defaultManagedAgentsStart,
|
Start: defaultManagedAgentsStart,
|
||||||
@@ -310,15 +257,6 @@ func LicensesEntitlements(
|
|||||||
|
|
||||||
// Add all features from the feature set.
|
// Add all features from the feature set.
|
||||||
for _, featureName := range claims.FeatureSet.Features() {
|
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() {
|
if featureName.UsesLimit() || featureName.UsesUsagePeriod() {
|
||||||
// Limit and usage period features are handled below.
|
// Limit and usage period features are handled below.
|
||||||
// They don't provide default values as they are always enabled
|
// 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
|
// Features al-la-carte
|
||||||
for featureName, featureValue := range claims.Features {
|
for featureName, featureValue := range claims.Features {
|
||||||
if _, ok := licenseForbiddenFeatures[featureName]; ok {
|
// Old-style licenses encode the managed agent limit as
|
||||||
entitlements.Errors = append(entitlements.Errors,
|
// separate soft/hard features.
|
||||||
fmt.Sprintf("Feature %s is forbidden to be set in a license.", featureName))
|
//
|
||||||
continue
|
// 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 {
|
if featureName == "managed_agent_limit_hard" {
|
||||||
// We currently don't use negative values for features.
|
// We can safely ignore the hard limit as it is no longer used.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for grouped (e.g. usage period) features.
|
if featureValue < 0 {
|
||||||
if grouping, ok := featureGrouping[featureName]; ok {
|
// We currently don't use negative values for features.
|
||||||
ul := uncommittedUsageFeatures[grouping.sdkFeature]
|
|
||||||
if grouping.isSoft {
|
|
||||||
ul.Soft = &featureValue
|
|
||||||
} else {
|
|
||||||
ul.Hard = &featureValue
|
|
||||||
}
|
|
||||||
uncommittedUsageFeatures[grouping.sdkFeature] = ul
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +304,17 @@ func LicensesEntitlements(
|
|||||||
|
|
||||||
// Handling for limit features.
|
// Handling for limit features.
|
||||||
switch {
|
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():
|
case featureName.UsesLimit():
|
||||||
if featureValue <= 0 {
|
if featureValue <= 0 {
|
||||||
// 0 limit value or less doesn't make sense, so we skip it.
|
// 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)
|
addonFeatures := make(map[codersdk.FeatureName]codersdk.Feature)
|
||||||
|
|
||||||
// Finally, add all features from the addons. We do this last so that
|
// Finally, add all features from the addons. We do this last so that
|
||||||
@@ -557,32 +460,9 @@ func LicensesEntitlements(
|
|||||||
entitlements.AddFeature(codersdk.FeatureManagedAgentLimit, agentLimit)
|
entitlements.AddFeature(codersdk.FeatureManagedAgentLimit, agentLimit)
|
||||||
|
|
||||||
// Only issue warnings if the feature is enabled.
|
// Only issue warnings if the feature is enabled.
|
||||||
if agentLimit.Enabled {
|
if agentLimit.Enabled && agentLimit.Limit != nil && managedAgentCount >= *agentLimit.Limit {
|
||||||
var softLimit int64
|
entitlements.Warnings = append(entitlements.Warnings,
|
||||||
if agentLimit.SoftLimit != nil {
|
codersdk.LicenseManagedAgentLimitExceededWarningText)
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,11 +563,6 @@ var (
|
|||||||
|
|
||||||
type Features map[codersdk.FeatureName]int64
|
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.
|
// Claims is the full set of claims in a license.
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
|
|||||||
@@ -76,8 +76,7 @@ func TestEntitlements(t *testing.T) {
|
|||||||
f := make(license.Features)
|
f := make(license.Features)
|
||||||
for _, name := range codersdk.FeatureNames {
|
for _, name := range codersdk.FeatureNames {
|
||||||
if name == codersdk.FeatureManagedAgentLimit {
|
if name == codersdk.FeatureManagedAgentLimit {
|
||||||
f[codersdk.FeatureName("managed_agent_limit_soft")] = 100
|
f[codersdk.FeatureManagedAgentLimit] = 100
|
||||||
f[codersdk.FeatureName("managed_agent_limit_hard")] = 200
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f[name] = 1
|
f[name] = 1
|
||||||
@@ -533,8 +532,7 @@ func TestEntitlements(t *testing.T) {
|
|||||||
t.Run("Premium", func(t *testing.T) {
|
t.Run("Premium", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
const userLimit = 1
|
const userLimit = 1
|
||||||
const expectedAgentSoftLimit = 1000
|
const expectedAgentLimit = 1000
|
||||||
const expectedAgentHardLimit = 1000
|
|
||||||
|
|
||||||
db, _ := dbtestutil.NewDB(t)
|
db, _ := dbtestutil.NewDB(t)
|
||||||
licenseOptions := coderdenttest.LicenseOptions{
|
licenseOptions := coderdenttest.LicenseOptions{
|
||||||
@@ -566,8 +564,7 @@ func TestEntitlements(t *testing.T) {
|
|||||||
agentEntitlement := entitlements.Features[featureName]
|
agentEntitlement := entitlements.Features[featureName]
|
||||||
require.True(t, agentEntitlement.Enabled)
|
require.True(t, agentEntitlement.Enabled)
|
||||||
require.Equal(t, codersdk.EntitlementEntitled, agentEntitlement.Entitlement)
|
require.Equal(t, codersdk.EntitlementEntitled, agentEntitlement.Entitlement)
|
||||||
require.EqualValues(t, expectedAgentSoftLimit, *agentEntitlement.SoftLimit)
|
require.EqualValues(t, expectedAgentLimit, *agentEntitlement.Limit)
|
||||||
require.EqualValues(t, expectedAgentHardLimit, *agentEntitlement.Limit)
|
|
||||||
|
|
||||||
// This might be shocking, but there's a sound reason for this.
|
// This might be shocking, but there's a sound reason for this.
|
||||||
// See license.go for more details.
|
// See license.go for more details.
|
||||||
@@ -840,7 +837,7 @@ func TestEntitlements(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}).
|
}).
|
||||||
UserLimit(100).
|
UserLimit(100).
|
||||||
ManagedAgentLimit(100, 200)
|
ManagedAgentLimit(100)
|
||||||
|
|
||||||
lic := database.License{
|
lic := database.License{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
@@ -882,16 +879,15 @@ func TestEntitlements(t *testing.T) {
|
|||||||
managedAgentLimit, ok := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
managedAgentLimit, ok := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
require.NotNil(t, managedAgentLimit.SoftLimit)
|
|
||||||
require.EqualValues(t, 100, *managedAgentLimit.SoftLimit)
|
|
||||||
require.NotNil(t, managedAgentLimit.Limit)
|
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.NotNil(t, managedAgentLimit.Actual)
|
||||||
require.EqualValues(t, 175, *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.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",
|
Name: "ManagedAgentLimit",
|
||||||
Licenses: []*coderdenttest.LicenseOptions{
|
Licenses: []*coderdenttest.LicenseOptions{
|
||||||
enterpriseLicense().UserLimit(100).ManagedAgentLimit(100, 200),
|
enterpriseLicense().UserLimit(100).ManagedAgentLimit(100),
|
||||||
},
|
},
|
||||||
Arguments: license.FeatureArguments{
|
Arguments: license.FeatureArguments{
|
||||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
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
|
// 74 is below the limit (soft=100), so no warning.
|
||||||
// difference between the soft and hard limit.
|
return 74, nil
|
||||||
return 174, nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||||
@@ -1136,9 +1131,9 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||||
assert.True(t, feature.Enabled)
|
assert.True(t, feature.Enabled)
|
||||||
assert.Equal(t, int64(100), *feature.SoftLimit)
|
// Soft limit value is used as the single Limit.
|
||||||
assert.Equal(t, int64(200), *feature.Limit)
|
assert.Equal(t, int64(100), *feature.Limit)
|
||||||
assert.Equal(t, int64(174), *feature.Actual)
|
assert.Equal(t, int64(74), *feature.Actual)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1151,7 +1146,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
WithIssuedAt(time.Now().Add(-time.Hour * 2)),
|
WithIssuedAt(time.Now().Add(-time.Hour * 2)),
|
||||||
enterpriseLicense().
|
enterpriseLicense().
|
||||||
UserLimit(100).
|
UserLimit(100).
|
||||||
ManagedAgentLimit(100, 100).
|
ManagedAgentLimit(100).
|
||||||
WithIssuedAt(time.Now().Add(-time.Hour * 1)).
|
WithIssuedAt(time.Now().Add(-time.Hour * 1)).
|
||||||
GracePeriod(time.Now()),
|
GracePeriod(time.Now()),
|
||||||
},
|
},
|
||||||
@@ -1168,7 +1163,6 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
assert.Equal(t, codersdk.EntitlementGracePeriod, feature.Entitlement)
|
assert.Equal(t, codersdk.EntitlementGracePeriod, feature.Entitlement)
|
||||||
assert.True(t, feature.Enabled)
|
assert.True(t, feature.Enabled)
|
||||||
assert.Equal(t, int64(100), *feature.SoftLimit)
|
|
||||||
assert.Equal(t, int64(100), *feature.Limit)
|
assert.Equal(t, int64(100), *feature.Limit)
|
||||||
assert.Equal(t, int64(74), *feature.Actual)
|
assert.Equal(t, int64(74), *feature.Actual)
|
||||||
},
|
},
|
||||||
@@ -1183,7 +1177,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
WithIssuedAt(time.Now().Add(-time.Hour * 2)),
|
WithIssuedAt(time.Now().Add(-time.Hour * 2)),
|
||||||
enterpriseLicense().
|
enterpriseLicense().
|
||||||
UserLimit(100).
|
UserLimit(100).
|
||||||
ManagedAgentLimit(100, 200).
|
ManagedAgentLimit(100).
|
||||||
WithIssuedAt(time.Now().Add(-time.Hour * 1)).
|
WithIssuedAt(time.Now().Add(-time.Hour * 1)).
|
||||||
Expired(time.Now()),
|
Expired(time.Now()),
|
||||||
},
|
},
|
||||||
@@ -1196,84 +1190,33 @@ func TestLicenseEntitlements(t *testing.T) {
|
|||||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
assert.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
assert.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
||||||
assert.False(t, feature.Enabled)
|
assert.False(t, feature.Enabled)
|
||||||
assert.Nil(t, feature.SoftLimit)
|
|
||||||
assert.Nil(t, feature.Limit)
|
assert.Nil(t, feature.Limit)
|
||||||
assert.Nil(t, feature.Actual)
|
assert.Nil(t, feature.Actual)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "ManagedAgentLimitWarning/ApproachingLimit/DifferentSoftAndHardLimit",
|
Name: "ManagedAgentLimitWarning/ExceededLimit",
|
||||||
Licenses: []*coderdenttest.LicenseOptions{
|
Licenses: []*coderdenttest.LicenseOptions{
|
||||||
enterpriseLicense().
|
enterpriseLicense().
|
||||||
UserLimit(100).
|
UserLimit(100).
|
||||||
ManagedAgentLimit(100, 200),
|
ManagedAgentLimit(100),
|
||||||
},
|
},
|
||||||
Arguments: license.FeatureArguments{
|
Arguments: license.FeatureArguments{
|
||||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
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) {
|
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||||
assert.Len(t, entitlements.Warnings, 1)
|
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)
|
assertNoErrors(t, entitlements)
|
||||||
|
|
||||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
assert.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||||
assert.True(t, feature.Enabled)
|
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(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)
|
|
||||||
assert.Equal(t, int64(100), *feature.Limit)
|
assert.Equal(t, int64(100), *feature.Limit)
|
||||||
assert.Equal(t, int64(75), *feature.Actual)
|
assert.Equal(t, int64(150), *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)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1472,173 +1415,240 @@ func TestAIBridgeSoftWarning(t *testing.T) {
|
|||||||
func TestUsageLimitFeatures(t *testing.T) {
|
func TestUsageLimitFeatures(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cases := []struct {
|
// Ensures that usage limit features are ranked by issued at, not by
|
||||||
sdkFeatureName codersdk.FeatureName
|
// values.
|
||||||
softLimitFeatureName codersdk.FeatureName
|
t.Run("IssuedAtRanking", func(t *testing.T) {
|
||||||
hardLimitFeatureName codersdk.FeatureName
|
t.Parallel()
|
||||||
}{
|
|
||||||
{
|
|
||||||
sdkFeatureName: codersdk.FeatureManagedAgentLimit,
|
|
||||||
softLimitFeatureName: codersdk.FeatureName("managed_agent_limit_soft"),
|
|
||||||
hardLimitFeatureName: codersdk.FeatureName("managed_agent_limit_hard"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range cases {
|
// Generate 2 real licenses both with managed agent limit
|
||||||
t.Run(string(c.sdkFeatureName), func(t *testing.T) {
|
// features. lic2 should trump lic1 even though it has a lower
|
||||||
t.Parallel()
|
// 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.
|
const actualAgents = 10
|
||||||
t.Run("MissingGroupedFeature", func(t *testing.T) {
|
arguments := license.FeatureArguments{
|
||||||
t.Parallel()
|
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{
|
// Load the licenses in both orders to ensure the correct
|
||||||
c.softLimitFeatureName,
|
// behavior is observed no matter the order.
|
||||||
c.hardLimitFeatureName,
|
for _, order := range [][]database.License{
|
||||||
} {
|
{lic1, lic2},
|
||||||
t.Run(string(feature), func(t *testing.T) {
|
{lic2, lic1},
|
||||||
t.Parallel()
|
} {
|
||||||
|
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), order, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
lic := database.License{
|
feature, ok := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
ID: 1,
|
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||||
UploadedAt: time.Now(),
|
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||||
Exp: time.Now().Add(time.Hour),
|
require.NotNil(t, feature.Limit)
|
||||||
UUID: uuid.New(),
|
require.EqualValues(t, 50, *feature.Limit)
|
||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
require.NotNil(t, feature.Actual)
|
||||||
Features: license.Features{
|
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||||
feature: 100,
|
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{
|
// TestOldStyleManagedAgentLicenses ensures backward compatibility with
|
||||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
// older licenses that encode the managed agent limit using separate
|
||||||
return 0, nil
|
// "managed_agent_limit_soft" and "managed_agent_limit_hard" feature keys
|
||||||
},
|
// instead of the canonical "managed_agent_limit" key.
|
||||||
}
|
func TestOldStyleManagedAgentLicenses(t *testing.T) {
|
||||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), []database.License{lic}, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
t.Parallel()
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
feature, ok := entitlements.Features[c.sdkFeatureName]
|
t.Run("SoftAndHard", func(t *testing.T) {
|
||||||
require.True(t, ok, "feature %s not found", c.sdkFeatureName)
|
t.Parallel()
|
||||||
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
|
||||||
|
|
||||||
require.Len(t, entitlements.Errors, 1)
|
lic := database.License{
|
||||||
require.Equal(t, fmt.Sprintf("Invalid license (%v): feature %s has missing soft or hard limit values", lic.UUID, c.sdkFeatureName), entitlements.Errors[0])
|
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) {
|
const actualAgents = 42
|
||||||
t.Parallel()
|
arguments := license.FeatureArguments{
|
||||||
|
ManagedAgentCountFn: func(_ context.Context, _, _ time.Time) (int64, error) {
|
||||||
|
return actualAgents, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
lic := database.License{
|
entitlements, err := license.LicensesEntitlements(
|
||||||
ID: 1,
|
context.Background(), time.Now(), []database.License{lic},
|
||||||
UploadedAt: time.Now(),
|
map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments,
|
||||||
Exp: time.Now().Add(time.Hour),
|
)
|
||||||
UUID: uuid.New(),
|
require.NoError(t, err)
|
||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
require.Empty(t, entitlements.Errors)
|
||||||
Features: license.Features{
|
|
||||||
c.softLimitFeatureName: 100,
|
|
||||||
c.hardLimitFeatureName: 50,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
arguments := license.FeatureArguments{
|
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||||
return 0, nil
|
require.True(t, feature.Enabled)
|
||||||
},
|
require.NotNil(t, feature.Limit)
|
||||||
}
|
// The soft limit should be used as the canonical limit.
|
||||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), []database.License{lic}, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
require.EqualValues(t, 100, *feature.Limit)
|
||||||
require.NoError(t, err)
|
require.NotNil(t, feature.Actual)
|
||||||
|
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||||
|
require.NotNil(t, feature.UsagePeriod)
|
||||||
|
})
|
||||||
|
|
||||||
feature, ok := entitlements.Features[c.sdkFeatureName]
|
t.Run("OnlySoft", func(t *testing.T) {
|
||||||
require.True(t, ok, "feature %s not found", c.sdkFeatureName)
|
t.Parallel()
|
||||||
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
|
||||||
|
|
||||||
require.Len(t, entitlements.Errors, 1)
|
lic := database.License{
|
||||||
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])
|
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
|
const actualAgents = 10
|
||||||
// values.
|
arguments := license.FeatureArguments{
|
||||||
t.Run("IssuedAtRanking", func(t *testing.T) {
|
ManagedAgentCountFn: func(_ context.Context, _, _ time.Time) (int64, error) {
|
||||||
t.Parallel()
|
return actualAgents, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Generate 2 real licenses both with managed agent limit
|
entitlements, err := license.LicensesEntitlements(
|
||||||
// features. lic2 should trump lic1 even though it has a lower
|
context.Background(), time.Now(), []database.License{lic},
|
||||||
// limit, because it was issued later.
|
map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments,
|
||||||
lic1 := database.License{
|
)
|
||||||
ID: 1,
|
require.NoError(t, err)
|
||||||
UploadedAt: time.Now(),
|
require.Empty(t, entitlements.Errors)
|
||||||
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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualAgents = 10
|
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||||
arguments := license.FeatureArguments{
|
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||||
ActiveUserCount: 10,
|
require.True(t, feature.Enabled)
|
||||||
ReplicaCount: 0,
|
require.NotNil(t, feature.Limit)
|
||||||
ExternalAuthCount: 0,
|
require.EqualValues(t, 75, *feature.Limit)
|
||||||
ManagedAgentCountFn: func(ctx context.Context, from time.Time, to time.Time) (int64, error) {
|
})
|
||||||
return actualAgents, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the licenses in both orders to ensure the correct
|
// A license with only the hard limit key should silently ignore it,
|
||||||
// behavior is observed no matter the order.
|
// leaving the feature unset (not entitled).
|
||||||
for _, order := range [][]database.License{
|
t.Run("OnlyHard", func(t *testing.T) {
|
||||||
{lic1, lic2},
|
t.Parallel()
|
||||||
{lic2, lic1},
|
|
||||||
} {
|
|
||||||
entitlements, err := license.LicensesEntitlements(context.Background(), time.Now(), order, map[codersdk.FeatureName]bool{}, coderdenttest.Keys, arguments)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
feature, ok := entitlements.Features[c.sdkFeatureName]
|
lic := database.License{
|
||||||
require.True(t, ok, "feature %s not found", c.sdkFeatureName)
|
ID: 1,
|
||||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
UploadedAt: time.Now(),
|
||||||
require.NotNil(t, feature.Limit)
|
Exp: time.Now().Add(time.Hour),
|
||||||
require.EqualValues(t, 100, *feature.Limit)
|
UUID: uuid.New(),
|
||||||
require.NotNil(t, feature.SoftLimit)
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
require.EqualValues(t, 50, *feature.SoftLimit)
|
Features: license.Features{
|
||||||
require.NotNil(t, feature.Actual)
|
codersdk.FeatureName("managed_agent_limit_hard"): 200,
|
||||||
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(_ 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) {
|
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.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||||
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
require.Equal(t, codersdk.EntitlementNotEntitled, feature.Entitlement)
|
||||||
require.Nil(t, feature.Limit)
|
require.Nil(t, feature.Limit)
|
||||||
require.Nil(t, feature.SoftLimit)
|
|
||||||
require.Nil(t, feature.Actual)
|
require.Nil(t, feature.Actual)
|
||||||
require.Nil(t, feature.UsagePeriod)
|
require.Nil(t, feature.UsagePeriod)
|
||||||
})
|
})
|
||||||
|
|
||||||
// "Premium" licenses should receive a default managed agent limit of:
|
// "Premium" licenses should receive a default managed agent limit of 1000.
|
||||||
// soft = 1000
|
|
||||||
// hard = 1000
|
|
||||||
t.Run("Premium", func(t *testing.T) {
|
t.Run("Premium", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const userLimit = 33
|
const userLimit = 33
|
||||||
const softLimit = 1000
|
const defaultLimit = 1000
|
||||||
const hardLimit = 1000
|
|
||||||
lic := database.License{
|
lic := database.License{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
UploadedAt: time.Now(),
|
UploadedAt: time.Now(),
|
||||||
@@ -1720,9 +1726,7 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
|||||||
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||||
require.NotNil(t, feature.Limit)
|
require.NotNil(t, feature.Limit)
|
||||||
require.EqualValues(t, hardLimit, *feature.Limit)
|
require.EqualValues(t, defaultLimit, *feature.Limit)
|
||||||
require.NotNil(t, feature.SoftLimit)
|
|
||||||
require.EqualValues(t, softLimit, *feature.SoftLimit)
|
|
||||||
require.NotNil(t, feature.Actual)
|
require.NotNil(t, feature.Actual)
|
||||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||||
require.NotNil(t, feature.UsagePeriod)
|
require.NotNil(t, feature.UsagePeriod)
|
||||||
@@ -1731,8 +1735,8 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
|||||||
require.NotZero(t, feature.UsagePeriod.End)
|
require.NotZero(t, feature.UsagePeriod.End)
|
||||||
})
|
})
|
||||||
|
|
||||||
// "Premium" licenses with an explicit managed agent limit should not
|
// "Premium" licenses with an explicit managed agent limit should use
|
||||||
// receive a default managed agent limit.
|
// that value instead of the default.
|
||||||
t.Run("PremiumExplicitValues", func(t *testing.T) {
|
t.Run("PremiumExplicitValues", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1744,9 +1748,8 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
|||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 100,
|
codersdk.FeatureUserLimit: 100,
|
||||||
codersdk.FeatureName("managed_agent_limit_soft"): 100,
|
codersdk.FeatureManagedAgentLimit: 100,
|
||||||
codersdk.FeatureName("managed_agent_limit_hard"): 200,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -1768,9 +1771,7 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
|||||||
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
require.True(t, ok, "feature %s not found", codersdk.FeatureManagedAgentLimit)
|
||||||
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
|
||||||
require.NotNil(t, feature.Limit)
|
require.NotNil(t, feature.Limit)
|
||||||
require.EqualValues(t, 200, *feature.Limit)
|
require.EqualValues(t, 100, *feature.Limit)
|
||||||
require.NotNil(t, feature.SoftLimit)
|
|
||||||
require.EqualValues(t, 100, *feature.SoftLimit)
|
|
||||||
require.NotNil(t, feature.Actual)
|
require.NotNil(t, feature.Actual)
|
||||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||||
require.NotNil(t, feature.UsagePeriod)
|
require.NotNil(t, feature.UsagePeriod)
|
||||||
@@ -1792,9 +1793,8 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
|||||||
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
FeatureSet: codersdk.FeatureSetPremium,
|
FeatureSet: codersdk.FeatureSetPremium,
|
||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureUserLimit: 100,
|
codersdk.FeatureUserLimit: 100,
|
||||||
codersdk.FeatureName("managed_agent_limit_soft"): 0,
|
codersdk.FeatureManagedAgentLimit: 0,
|
||||||
codersdk.FeatureName("managed_agent_limit_hard"): 0,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -1818,8 +1818,6 @@ func TestManagedAgentLimitDefault(t *testing.T) {
|
|||||||
require.False(t, feature.Enabled)
|
require.False(t, feature.Enabled)
|
||||||
require.NotNil(t, feature.Limit)
|
require.NotNil(t, feature.Limit)
|
||||||
require.EqualValues(t, 0, *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.NotNil(t, feature.Actual)
|
||||||
require.EqualValues(t, actualAgents, *feature.Actual)
|
require.EqualValues(t, actualAgents, *feature.Actual)
|
||||||
require.NotNil(t, feature.UsagePeriod)
|
require.NotNil(t, feature.UsagePeriod)
|
||||||
|
|||||||
@@ -4728,7 +4728,7 @@ func TestWorkspaceAITask(t *testing.T) {
|
|||||||
Features: license.Features{
|
Features: license.Features{
|
||||||
codersdk.FeatureTemplateRBAC: 1,
|
codersdk.FeatureTemplateRBAC: 1,
|
||||||
},
|
},
|
||||||
}).ManagedAgentLimit(10, 20),
|
}).ManagedAgentLimit(10),
|
||||||
})
|
})
|
||||||
|
|
||||||
client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID,
|
client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID,
|
||||||
|
|||||||
Generated
-6
@@ -2112,12 +2112,6 @@ export interface Feature {
|
|||||||
readonly enabled: boolean;
|
readonly enabled: boolean;
|
||||||
readonly limit?: number;
|
readonly limit?: number;
|
||||||
readonly actual?: 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
|
* UsagePeriod denotes that the usage is a counter that accumulates over
|
||||||
* this period (and most likely resets with the issuance of the next
|
* this period (and most likely resets with the issuance of the next
|
||||||
|
|||||||
+10
-54
@@ -9,8 +9,7 @@ const meta: Meta<typeof ManagedAgentsConsumption> = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: 50000,
|
actual: 50000,
|
||||||
soft_limit: 60000,
|
limit: 60000,
|
||||||
limit: 120000,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "February 27, 2025",
|
start: "February 27, 2025",
|
||||||
end: "February 27, 2026",
|
end: "February 27, 2026",
|
||||||
@@ -31,8 +30,7 @@ export const ZeroUsage: Story = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: 0,
|
actual: 0,
|
||||||
soft_limit: 60000,
|
limit: 60000,
|
||||||
limit: 120000,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "February 27, 2025",
|
start: "February 27, 2025",
|
||||||
end: "February 27, 2026",
|
end: "February 27, 2026",
|
||||||
@@ -47,9 +45,8 @@ export const NearLimit: Story = {
|
|||||||
args: {
|
args: {
|
||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: 115000,
|
actual: 55000,
|
||||||
soft_limit: 60000,
|
limit: 60000,
|
||||||
limit: 120000,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "February 27, 2025",
|
start: "February 27, 2025",
|
||||||
end: "February 27, 2026",
|
end: "February 27, 2026",
|
||||||
@@ -60,13 +57,12 @@ export const NearLimit: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OverIncluded: Story = {
|
export const OverLimit: Story = {
|
||||||
args: {
|
args: {
|
||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: 80000,
|
actual: 80000,
|
||||||
soft_limit: 60000,
|
limit: 60000,
|
||||||
limit: 120000,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "February 27, 2025",
|
start: "February 27, 2025",
|
||||||
end: "February 27, 2026",
|
end: "February 27, 2026",
|
||||||
@@ -82,25 +78,7 @@ export const LowUsage: Story = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: 25000,
|
actual: 25000,
|
||||||
soft_limit: 60000,
|
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,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "February 27, 2025",
|
start: "February 27, 2025",
|
||||||
end: "February 27, 2026",
|
end: "February 27, 2026",
|
||||||
@@ -116,7 +94,6 @@ export const Disabled: Story = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
actual: undefined,
|
actual: undefined,
|
||||||
soft_limit: undefined,
|
|
||||||
limit: undefined,
|
limit: undefined,
|
||||||
usage_period: undefined,
|
usage_period: undefined,
|
||||||
entitlement: "not_entitled",
|
entitlement: "not_entitled",
|
||||||
@@ -136,7 +113,6 @@ export const ErrorMissingData: Story = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: undefined,
|
actual: undefined,
|
||||||
soft_limit: undefined,
|
|
||||||
limit: undefined,
|
limit: undefined,
|
||||||
usage_period: undefined,
|
usage_period: undefined,
|
||||||
entitlement: "entitled",
|
entitlement: "entitled",
|
||||||
@@ -149,25 +125,7 @@ export const ErrorNegativeValues: Story = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: -100,
|
actual: -100,
|
||||||
soft_limit: 60000,
|
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,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "February 27, 2025",
|
start: "February 27, 2025",
|
||||||
end: "February 27, 2026",
|
end: "February 27, 2026",
|
||||||
@@ -183,8 +141,7 @@ export const ErrorInvalidDates: Story = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: 50000,
|
actual: 50000,
|
||||||
soft_limit: 60000,
|
limit: 60000,
|
||||||
limit: 120000,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "invalid-date",
|
start: "invalid-date",
|
||||||
end: "February 27, 2026",
|
end: "February 27, 2026",
|
||||||
@@ -200,8 +157,7 @@ export const ErrorEndBeforeStart: Story = {
|
|||||||
managedAgentFeature: {
|
managedAgentFeature: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
actual: 50000,
|
actual: 50000,
|
||||||
soft_limit: 60000,
|
limit: 60000,
|
||||||
limit: 120000,
|
|
||||||
usage_period: {
|
usage_period: {
|
||||||
start: "February 27, 2026",
|
start: "February 27, 2026",
|
||||||
end: "February 27, 2025",
|
end: "February 27, 2025",
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const usage = managedAgentFeature.actual;
|
const usage = managedAgentFeature.actual;
|
||||||
const included = managedAgentFeature.soft_limit;
|
const included = managedAgentFeature.limit;
|
||||||
const startDate = managedAgentFeature.usage_period?.start;
|
const startDate = managedAgentFeature.usage_period?.start;
|
||||||
const endDate = managedAgentFeature.usage_period?.end;
|
const endDate = managedAgentFeature.usage_period?.end;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user