Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: implement premium vs enterprise licenses#13907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Emyrk merged 15 commits intomainfromstevenmasley/premium_license
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
15 commits
Select commitHold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 172 additions & 2 deletionscodersdk/deployment.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"time"
Expand All@@ -34,6 +35,21 @@ const (
EntitlementNotEntitled Entitlement = "not_entitled"
)

// Weight converts the enum types to a numerical value for easier
// comparisons. Easier than sets of if statements.
func (e Entitlement) Weight() int {
switch e {
case EntitlementEntitled:
return 2
case EntitlementGracePeriod:
return 1
case EntitlementNotEntitled:
return -1
default:
return -2
}
}

// FeatureName represents the internal name of a feature.
// To add a new feature, add it to this set of enums as well as the FeatureNames
// array below.
Expand DownExpand Up@@ -95,8 +111,11 @@ func (n FeatureName) Humanize() string {
}

// AlwaysEnable returns if the feature is always enabled if entitled.
// Warning: We don't know if we need this functionality.
// This method may disappear at any time.
// This is required because some features are only enabled if they are entitled
// and not required.
// E.g: "multiple-organizations" is disabled by default in AGPL and enterprise
// deployments. This feature should only be enabled for premium deployments
// when it is entitled.
func (n FeatureName) AlwaysEnable() bool {
return map[FeatureName]bool{
FeatureMultipleExternalAuth: true,
Expand All@@ -105,16 +124,144 @@ func (n FeatureName) AlwaysEnable() bool {
FeatureWorkspaceBatchActions: true,
FeatureHighAvailability: true,
FeatureCustomRoles: true,
FeatureMultipleOrganizations: true,
}[n]
}

// FeatureSet represents a grouping of features. Rather than manually
// assigning features al-la-carte when making a license, a set can be specified.
// Sets are dynamic in the sense a feature can be added to a set, granting the
// feature to existing licenses out in the wild.
// If features were granted al-la-carte, we would need to reissue the existing
// old licenses to include the new feature.
type FeatureSet string

const (
FeatureSetNone FeatureSet = ""
FeatureSetEnterprise FeatureSet = "enterprise"
FeatureSetPremium FeatureSet = "premium"
)

func (set FeatureSet) Features() []FeatureName {
switch FeatureSet(strings.ToLower(string(set))) {
case FeatureSetEnterprise:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

A while ago Ammar did work to make it so we don't have to list new features in a bunch of places.

Instead of this, could we do the inverse where Premium simply detracts from the list instead? Seems easier to mentally model.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

If I understand correctly, you are saying we have 1 list,All() (it's FeatureNames() or something atm).

And we defineenterprise = All() - [multi-org],premium = All(). Rather than this explicit listing?

If we define it that way, it feels more likely to accidentally include a feature in "enterprise", and then we'd have to revoke it later. This current method might be a bit of a nuisance to deal with, but it errors on the side of restrictive.

Now our unit tests do not use feature sets, they manually define features. So this might reveal something lacking in our unit tests that only running a real server will pickup 🤔.

I'm still a bit split on the ideal way.

Copy link
MemberAuthor

@EmyrkEmyrkJul 24, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I switch it 👍. We should probably add some tests to assert we don't leak features into the wrong set, but not in this PR.

7436159 +8e11ff9

kylecarbs reacted with thumbs up emoji
// Enterprise is the set 'AllFeatures' minus some select features.

// Copy the list of all features
enterpriseFeatures := make([]FeatureName, len(FeatureNames))
copy(enterpriseFeatures, FeatureNames)
// Remove the selection
enterpriseFeatures = slices.DeleteFunc(enterpriseFeatures, func(f FeatureName) bool {
switch f {
// Add all features that should be excluded in the Enterprise feature set.
case FeatureMultipleOrganizations:
return true
default:
return false
}
})

return enterpriseFeatures
case FeatureSetPremium:
premiumFeatures := make([]FeatureName, len(FeatureNames))
copy(premiumFeatures, FeatureNames)
// FeatureSetPremium is just all features.
return premiumFeatures
}
// By default, return an empty set.
return []FeatureName{}
}

type Feature struct {
Entitlement Entitlement `json:"entitlement"`
Enabled bool `json:"enabled"`
Limit *int64 `json:"limit,omitempty"`
Actual *int64 `json:"actual,omitempty"`
}

// Compare compares two features and returns an integer representing
// if the first feature (f) is greater than, equal to, or less than the second
// feature (b). "Greater than" means the first feature has more functionality
// than the second feature. It is assumed the features are for the same FeatureName.
//
// A feature is considered greater than another feature if:
// 1. Graceful & capable > Entitled & not capable
// 2. The entitlement is greater
// 3. The limit is greater
// 4. Enabled is greater than disabled
// 5. The actual is greater
func (f Feature) Compare(b Feature) int {
if !f.Capable() || !b.Capable() {
// If either is incapable, then it is possible a grace period
// feature can be "greater" than an entitled.
// If either is "NotEntitled" then we can defer to a strict entitlement
// check.
if f.Entitlement.Weight() >= 0 && b.Entitlement.Weight() >= 0 {
if f.Capable() && !b.Capable() {
return 1
}
if b.Capable() && !f.Capable() {
return -1
}
}
}

// Strict entitlement check. Higher is better
entitlementDifference := f.Entitlement.Weight() - b.Entitlement.Weight()
if entitlementDifference != 0 {
return entitlementDifference
}

// If the entitlement is the same, then we can compare the limits.
if f.Limit == nil && b.Limit != nil {
return -1
}
if f.Limit != nil && b.Limit == nil {
return 1
}
if f.Limit != nil && b.Limit != nil {
difference := *f.Limit - *b.Limit
if difference != 0 {
return int(difference)
}
}

// Enabled is better than disabled.
if f.Enabled && !b.Enabled {
return 1
}
if !f.Enabled && b.Enabled {
return -1
}

// Higher actual is better
if f.Actual == nil && b.Actual != nil {
return -1
}
if f.Actual != nil && b.Actual == nil {
return 1
}
if f.Actual != nil && b.Actual != nil {
difference := *f.Actual - *b.Actual
if difference != 0 {
return int(difference)
}
}

return 0
}

// Capable is a helper function that returns if a given feature has a limit
// that is greater than or equal to the actual.
// If this condition is not true, then the feature is not capable of being used
// since the limit is not high enough.
func (f Feature) Capable() bool {
if f.Limit != nil && f.Actual != nil {
return *f.Limit >= *f.Actual
}
return true
}

type Entitlements struct {
Features map[FeatureName]Feature `json:"features"`
Warnings []string `json:"warnings"`
Expand All@@ -125,6 +272,29 @@ type Entitlements struct {
RefreshedAt time.Time `json:"refreshed_at" format:"date-time"`
}

// AddFeature will add the feature to the entitlements iff it expands
// the set of features granted by the entitlements. If it does not, it will
// be ignored and the existing feature with the same name will remain.
//
// All features should be added as atomic items, and not merged in any way.
// Merging entitlements could lead to unexpected behavior, like a larger user
// limit in grace period merging with a smaller one in an "entitled" state. This
// could lead to the larger limit being extended as "entitled", which is not correct.
func (e *Entitlements) AddFeature(name FeatureName, add Feature) {
existing, ok := e.Features[name]
if !ok {
e.Features[name] = add
return
}

// Compare the features, keep the one that is "better"
comparison := add.Compare(existing)
if comparison > 0 {
e.Features[name] = add
return
}
}

func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil)
if err != nil {
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp