99"os"
1010"path/filepath"
1111"reflect"
12+ "slices"
1213"strconv"
1314"strings"
1415"time"
@@ -34,6 +35,21 @@ const (
3435EntitlementNotEntitled Entitlement = "not_entitled"
3536)
3637
38+ // Weight converts the enum types to a numerical value for easier
39+ // comparisons. Easier than sets of if statements.
40+ func (e Entitlement )Weight ()int {
41+ switch e {
42+ case EntitlementEntitled :
43+ return 2
44+ case EntitlementGracePeriod :
45+ return 1
46+ case EntitlementNotEntitled :
47+ return - 1
48+ default :
49+ return - 2
50+ }
51+ }
52+
3753// FeatureName represents the internal name of a feature.
3854// To add a new feature, add it to this set of enums as well as the FeatureNames
3955// array below.
@@ -95,8 +111,11 @@ func (n FeatureName) Humanize() string {
95111}
96112
97113// AlwaysEnable returns if the feature is always enabled if entitled.
98- // Warning: We don't know if we need this functionality.
99- // This method may disappear at any time.
114+ // This is required because some features are only enabled if they are entitled
115+ // and not required.
116+ // E.g: "multiple-organizations" is disabled by default in AGPL and enterprise
117+ // deployments. This feature should only be enabled for premium deployments
118+ // when it is entitled.
100119func (n FeatureName )AlwaysEnable ()bool {
101120return map [FeatureName ]bool {
102121FeatureMultipleExternalAuth :true ,
@@ -105,16 +124,144 @@ func (n FeatureName) AlwaysEnable() bool {
105124FeatureWorkspaceBatchActions :true ,
106125FeatureHighAvailability :true ,
107126FeatureCustomRoles :true ,
127+ FeatureMultipleOrganizations :true ,
108128}[n ]
109129}
110130
131+ // FeatureSet represents a grouping of features. Rather than manually
132+ // assigning features al-la-carte when making a license, a set can be specified.
133+ // Sets are dynamic in the sense a feature can be added to a set, granting the
134+ // feature to existing licenses out in the wild.
135+ // If features were granted al-la-carte, we would need to reissue the existing
136+ // old licenses to include the new feature.
137+ type FeatureSet string
138+
139+ const (
140+ FeatureSetNone FeatureSet = ""
141+ FeatureSetEnterprise FeatureSet = "enterprise"
142+ FeatureSetPremium FeatureSet = "premium"
143+ )
144+
145+ func (set FeatureSet )Features () []FeatureName {
146+ switch FeatureSet (strings .ToLower (string (set ))) {
147+ case FeatureSetEnterprise :
148+ // Enterprise is the set 'AllFeatures' minus some select features.
149+
150+ // Copy the list of all features
151+ enterpriseFeatures := make ([]FeatureName ,len (FeatureNames ))
152+ copy (enterpriseFeatures ,FeatureNames )
153+ // Remove the selection
154+ enterpriseFeatures = slices .DeleteFunc (enterpriseFeatures ,func (f FeatureName )bool {
155+ switch f {
156+ // Add all features that should be excluded in the Enterprise feature set.
157+ case FeatureMultipleOrganizations :
158+ return true
159+ default :
160+ return false
161+ }
162+ })
163+
164+ return enterpriseFeatures
165+ case FeatureSetPremium :
166+ premiumFeatures := make ([]FeatureName ,len (FeatureNames ))
167+ copy (premiumFeatures ,FeatureNames )
168+ // FeatureSetPremium is just all features.
169+ return premiumFeatures
170+ }
171+ // By default, return an empty set.
172+ return []FeatureName {}
173+ }
174+
111175type Feature struct {
112176Entitlement Entitlement `json:"entitlement"`
113177Enabled bool `json:"enabled"`
114178Limit * int64 `json:"limit,omitempty"`
115179Actual * int64 `json:"actual,omitempty"`
116180}
117181
182+ // Compare compares two features and returns an integer representing
183+ // if the first feature (f) is greater than, equal to, or less than the second
184+ // feature (b). "Greater than" means the first feature has more functionality
185+ // than the second feature. It is assumed the features are for the same FeatureName.
186+ //
187+ // A feature is considered greater than another feature if:
188+ // 1. Graceful & capable > Entitled & not capable
189+ // 2. The entitlement is greater
190+ // 3. The limit is greater
191+ // 4. Enabled is greater than disabled
192+ // 5. The actual is greater
193+ func (f Feature )Compare (b Feature )int {
194+ if ! f .Capable ()|| ! b .Capable () {
195+ // If either is incapable, then it is possible a grace period
196+ // feature can be "greater" than an entitled.
197+ // If either is "NotEntitled" then we can defer to a strict entitlement
198+ // check.
199+ if f .Entitlement .Weight ()>= 0 && b .Entitlement .Weight ()>= 0 {
200+ if f .Capable ()&& ! b .Capable () {
201+ return 1
202+ }
203+ if b .Capable ()&& ! f .Capable () {
204+ return - 1
205+ }
206+ }
207+ }
208+
209+ // Strict entitlement check. Higher is better
210+ entitlementDifference := f .Entitlement .Weight ()- b .Entitlement .Weight ()
211+ if entitlementDifference != 0 {
212+ return entitlementDifference
213+ }
214+
215+ // If the entitlement is the same, then we can compare the limits.
216+ if f .Limit == nil && b .Limit != nil {
217+ return - 1
218+ }
219+ if f .Limit != nil && b .Limit == nil {
220+ return 1
221+ }
222+ if f .Limit != nil && b .Limit != nil {
223+ difference := * f .Limit - * b .Limit
224+ if difference != 0 {
225+ return int (difference )
226+ }
227+ }
228+
229+ // Enabled is better than disabled.
230+ if f .Enabled && ! b .Enabled {
231+ return 1
232+ }
233+ if ! f .Enabled && b .Enabled {
234+ return - 1
235+ }
236+
237+ // Higher actual is better
238+ if f .Actual == nil && b .Actual != nil {
239+ return - 1
240+ }
241+ if f .Actual != nil && b .Actual == nil {
242+ return 1
243+ }
244+ if f .Actual != nil && b .Actual != nil {
245+ difference := * f .Actual - * b .Actual
246+ if difference != 0 {
247+ return int (difference )
248+ }
249+ }
250+
251+ return 0
252+ }
253+
254+ // Capable is a helper function that returns if a given feature has a limit
255+ // that is greater than or equal to the actual.
256+ // If this condition is not true, then the feature is not capable of being used
257+ // since the limit is not high enough.
258+ func (f Feature )Capable ()bool {
259+ if f .Limit != nil && f .Actual != nil {
260+ return * f .Limit >= * f .Actual
261+ }
262+ return true
263+ }
264+
118265type Entitlements struct {
119266Features map [FeatureName ]Feature `json:"features"`
120267Warnings []string `json:"warnings"`
@@ -125,6 +272,29 @@ type Entitlements struct {
125272RefreshedAt time.Time `json:"refreshed_at" format:"date-time"`
126273}
127274
275+ // AddFeature will add the feature to the entitlements iff it expands
276+ // the set of features granted by the entitlements. If it does not, it will
277+ // be ignored and the existing feature with the same name will remain.
278+ //
279+ // All features should be added as atomic items, and not merged in any way.
280+ // Merging entitlements could lead to unexpected behavior, like a larger user
281+ // limit in grace period merging with a smaller one in an "entitled" state. This
282+ // could lead to the larger limit being extended as "entitled", which is not correct.
283+ func (e * Entitlements )AddFeature (name FeatureName ,add Feature ) {
284+ existing ,ok := e .Features [name ]
285+ if ! ok {
286+ e .Features [name ]= add
287+ return
288+ }
289+
290+ // Compare the features, keep the one that is "better"
291+ comparison := add .Compare (existing )
292+ if comparison > 0 {
293+ e .Features [name ]= add
294+ return
295+ }
296+ }
297+
128298func (c * Client )Entitlements (ctx context.Context ) (Entitlements ,error ) {
129299res ,err := c .Request (ctx ,http .MethodGet ,"/api/v2/entitlements" ,nil )
130300if err != nil {