9
9
"os"
10
10
"path/filepath"
11
11
"reflect"
12
+ "slices"
12
13
"strconv"
13
14
"strings"
14
15
"time"
@@ -34,6 +35,21 @@ const (
34
35
EntitlementNotEntitled Entitlement = "not_entitled"
35
36
)
36
37
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
+
37
53
// FeatureName represents the internal name of a feature.
38
54
// To add a new feature, add it to this set of enums as well as the FeatureNames
39
55
// array below.
@@ -95,8 +111,11 @@ func (n FeatureName) Humanize() string {
95
111
}
96
112
97
113
// 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.
100
119
func (n FeatureName ) AlwaysEnable () bool {
101
120
return map [FeatureName ]bool {
102
121
FeatureMultipleExternalAuth : true ,
@@ -105,16 +124,144 @@ func (n FeatureName) AlwaysEnable() bool {
105
124
FeatureWorkspaceBatchActions : true ,
106
125
FeatureHighAvailability : true ,
107
126
FeatureCustomRoles : true ,
127
+ FeatureMultipleOrganizations : true ,
108
128
}[n ]
109
129
}
110
130
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
+
111
175
type Feature struct {
112
176
Entitlement Entitlement `json:"entitlement"`
113
177
Enabled bool `json:"enabled"`
114
178
Limit * int64 `json:"limit,omitempty"`
115
179
Actual * int64 `json:"actual,omitempty"`
116
180
}
117
181
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
+
118
265
type Entitlements struct {
119
266
Features map [FeatureName ]Feature `json:"features"`
120
267
Warnings []string `json:"warnings"`
@@ -125,6 +272,29 @@ type Entitlements struct {
125
272
RefreshedAt time.Time `json:"refreshed_at" format:"date-time"`
126
273
}
127
274
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
+
128
298
func (c * Client ) Entitlements (ctx context.Context ) (Entitlements , error ) {
129
299
res , err := c .Request (ctx , http .MethodGet , "/api/v2/entitlements" , nil )
130
300
if err != nil {
0 commit comments