@@ -31,18 +31,25 @@ import (
31
31
"github.com/spf13/cobra"
32
32
33
33
"k8s.io/apimachinery/pkg/util/version"
34
+ baseversion "k8s.io/component-base/version"
34
35
yaml "sigs.k8s.io/yaml/goyaml.v2"
35
36
)
36
37
37
38
var (
38
- alphabeticalOrder bool
39
- k8RootPath string
40
- unversionedFeatureListFile = "test/featuregates_linter/test_data/unversioned_feature_list.yaml"
41
- versionedFeatureListFile = "test/featuregates_linter/test_data/versioned_feature_list.yaml"
39
+ alphabeticalOrder bool
40
+ k8RootPath string
41
+ versionedFeatureListFile = "test/compatibility_lifecycle/reference/versioned_feature_list.yaml"
42
+ // thresholdVersion is the version after which we require emulation support for feature removal
43
+ // 1.31 is when we introduced emulation version support
44
+ thresholdVersion = version .MajorMinor (1 , 31 )
42
45
)
43
46
44
47
const (
45
- featureGatePkg = "\" k8s.io/component-base/featuregate\" "
48
+ featureGatePkg = "\" k8s.io/component-base/featuregate\" "
49
+ generatedFileWarning = `# This file is generated by compatibility_lifecycle tool.
50
+ # Do not edit manually. Run hack/update-featuregates.sh to regenerate.
51
+
52
+ `
46
53
)
47
54
48
55
type featureSpec struct {
@@ -95,39 +102,33 @@ func NewUpdateFeatureListCommand() *cobra.Command {
95
102
}
96
103
97
104
func verifyFeatureListFunc (cmd * cobra.Command , args []string ) {
98
- if err := verifyOrUpdateFeatureList (k8RootPath , unversionedFeatureListFile , false , false ); err != nil {
99
- fmt .Fprintf (os .Stderr , "Failed to verify feature list: \n %s" , err )
100
- os .Exit (1 )
101
- }
102
- if err := verifyOrUpdateFeatureList (k8RootPath , versionedFeatureListFile , false , true ); err != nil {
105
+ currentVersion := version .MustParse (baseversion .DefaultKubeBinaryVersion )
106
+ if err := verifyOrUpdateFeatureList (k8RootPath , versionedFeatureListFile , currentVersion , false ); err != nil {
103
107
fmt .Fprintf (os .Stderr , "Failed to verify versioned feature list: \n %s" , err )
104
108
os .Exit (1 )
105
109
}
106
110
}
107
111
108
112
func updateFeatureListFunc (cmd * cobra.Command , args []string ) {
109
- if err := verifyOrUpdateFeatureList (k8RootPath , unversionedFeatureListFile , true , false ); err != nil {
110
- fmt .Fprintf (os .Stderr , "Failed to update feature list: \n %s" , err )
111
- os .Exit (1 )
112
- }
113
- if err := verifyOrUpdateFeatureList (k8RootPath , versionedFeatureListFile , true , true ); err != nil {
113
+ currentVersion := version .MustParse (baseversion .DefaultKubeBinaryVersion )
114
+ if err := verifyOrUpdateFeatureList (k8RootPath , versionedFeatureListFile , currentVersion , true ); err != nil {
114
115
fmt .Fprintf (os .Stderr , "Failed to update versioned feature list: \n %s" , err )
115
116
os .Exit (1 )
116
117
}
117
118
}
118
119
119
120
// verifyOrUpdateFeatureList walks all the files under pkg/ and staging/ to find the list of all the features in
120
- // map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate. VersionedSpecs.
121
+ // map[featuregate.Feature]featuregate.VersionedSpecs.
121
122
// It will then update the feature list in featureListFile, or verifies there is no change from the existing list.
122
- func verifyOrUpdateFeatureList (rootPath , featureListFile string , update , versioned bool ) error {
123
+ func verifyOrUpdateFeatureList (rootPath , featureListFile string , currentVersion * version. Version , update bool ) error {
123
124
featureList := []featureInfo {}
124
- features , err := searchPathForFeatures (filepath .Join (rootPath , "pkg" ), versioned )
125
+ features , err := searchPathForFeatures (filepath .Join (rootPath , "pkg" ))
125
126
if err != nil {
126
127
return err
127
128
}
128
129
featureList = append (featureList , features ... )
129
130
130
- features , err = searchPathForFeatures (filepath .Join (rootPath , "staging" ), versioned )
131
+ features , err = searchPathForFeatures (filepath .Join (rootPath , "staging" ))
131
132
if err != nil {
132
133
return err
133
134
}
@@ -153,29 +154,22 @@ func verifyOrUpdateFeatureList(rootPath, featureListFile string, update, version
153
154
return err
154
155
}
155
156
156
- // only feature deletion is allowed for unversioned features.
157
- // all new features or feature updates should be migrated to versioned feature gate.
158
- // https://github.com/kubernetes/kubernetes/issues/125031
159
- if ! versioned {
160
- if err := verifyFeatureDeletionOnly (featureList , baseFeatureList ); err != nil {
161
- return err
162
- }
157
+ if err := verifyFeatureRemoval (featureList , baseFeatureList , currentVersion , thresholdVersion ); err != nil {
158
+ return err
163
159
}
164
160
165
161
featureListBytes , err := yaml .Marshal (featureList )
166
162
if err != nil {
167
163
return err
168
164
}
165
+ featureListBytes = []byte (generatedFileWarning + string (featureListBytes ))
169
166
if update {
170
167
return os .WriteFile (filePath , featureListBytes , 0644 )
171
168
}
172
169
173
170
if diff := cmp .Diff (featureListBytes , baseFeatureListBytes ); diff != "" {
174
- if versioned {
175
- return fmt .Errorf ("detected diff in versioned feature list (%s), diff: \n %s" , versionedFeatureListFile , diff )
176
- } else {
177
- return fmt .Errorf ("detected diff in unversioned feature list (%s), diff: \n %s" , unversionedFeatureListFile , diff )
178
- }
171
+ return fmt .Errorf ("detected diff in versioned feature list (%s), diff: \n %s" , versionedFeatureListFile , diff )
172
+
179
173
}
180
174
return nil
181
175
}
@@ -205,27 +199,70 @@ func dedupeFeatureList(featureList []featureInfo) ([]featureInfo, error) {
205
199
return deduped , nil
206
200
}
207
201
208
- func verifyFeatureDeletionOnly (newFeatureList []featureInfo , oldFeatureList []featureInfo ) error {
209
- oldFeatureSet := make (map [string ]* featureInfo )
210
- for _ , f := range oldFeatureList {
211
- oldFeatureSet [f .Name ] = & f
212
- }
213
- newFeatures := []string {}
214
- for _ , f := range newFeatureList {
215
- oldSpecs , found := oldFeatureSet [f .Name ]
216
- if ! found {
217
- newFeatures = append (newFeatures , f .Name )
218
- } else if ! reflect .DeepEqual (* oldSpecs , f ) {
219
- return fmt .Errorf ("feature %s changed with diff: %s" , f .Name , cmp .Diff (* oldSpecs , f ))
202
+ // verifyFeatureRemoval checks if removed features are allowed to be removed based on their lifecycle.
203
+ // Alpha features can be removed anytime without error.
204
+ // Returns error if:
205
+ // - Beta features are removed (not allowed)
206
+ // - GA/Deprecated features are removed without being locked to default
207
+ // - GA/Deprecated features locked after v1.31 are removed before 3 minor versions
208
+ // have passed (required for emulation support)
209
+ func verifyFeatureRemoval (featureList []featureInfo , baseFeatureList []featureInfo ,
210
+ currentVersion * version.Version , thresholdVersion * version.Version ) error {
211
+ if thresholdVersion == nil {
212
+ thresholdVersion = version .MajorMinor (0 , 0 )
213
+ }
214
+ baseFeatures := make (map [string ]featureInfo )
215
+ for _ , f := range baseFeatureList {
216
+ baseFeatures [f .Name ] = f
217
+ }
218
+ currentFeatures := make (map [string ]featureInfo )
219
+ for _ , f := range featureList {
220
+ currentFeatures [f .Name ] = f
221
+ }
222
+
223
+ for name , baseFeature := range baseFeatures {
224
+ // Check if feature was removed
225
+ if _ , found := currentFeatures [name ]; found {
226
+ continue
227
+ }
228
+
229
+ // Feature was removed, check if allowed
230
+ specs := baseFeature .VersionedSpecs
231
+ if len (specs ) == 0 {
232
+ return fmt .Errorf ("feature %s has no version specs" , name )
233
+ }
234
+
235
+ lastSpec := specs [len (specs )- 1 ]
236
+ switch lastSpec .PreRelease {
237
+ case "Alpha" :
238
+ continue // can remove alpha features anytime
239
+ case "Beta" :
240
+ return fmt .Errorf ("feature %s cannot be removed while in beta" , name )
241
+ case "GA" , "Deprecated" :
242
+ if ! lastSpec .LockToDefault {
243
+ return fmt .Errorf ("feature %s cannot be removed because it is in GA or Deprecated state and is not locked to default" , name )
244
+ }
245
+ specVer , err := version .Parse (lastSpec .Version )
246
+ if err != nil {
247
+ return fmt .Errorf ("invalid version \" %s\" for feature %s: %w" , lastSpec .Version , name , err )
248
+ }
249
+ // we do not require the 3 version retention for features locked before the thresholdVersion.
250
+ // TODO: remove after 1.34
251
+ if ! specVer .GreaterThan (thresholdVersion ) {
252
+ continue
253
+ }
254
+ minRemovalVer := specVer .AddMinor (3 )
255
+ if currentVersion .LessThan (minRemovalVer ) {
256
+ return fmt .Errorf ("feature %s cannot be removed until version %s (required for emulation support)" ,
257
+ name , minRemovalVer )
258
+ }
259
+
220
260
}
221
- }
222
- if len (newFeatures ) > 0 {
223
- return fmt .Errorf ("new features added to FeatureSpec map! %v\n Please add new features through VersionedSpecs map ONLY! " , newFeatures )
224
261
}
225
262
return nil
226
263
}
227
264
228
- func searchPathForFeatures (path string , versioned bool ) ([]featureInfo , error ) {
265
+ func searchPathForFeatures (path string ) ([]featureInfo , error ) {
229
266
allFeatures := []featureInfo {}
230
267
// Create a FileSet to work with
231
268
fset := token .NewFileSet ()
@@ -239,7 +276,12 @@ func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
239
276
if strings .HasSuffix (path , "_test.go" ) {
240
277
return nil
241
278
}
242
- features , parseErr := extractFeatureInfoListFromFile (fset , path , versioned )
279
+ // exclude generated files
280
+ base := filepath .Base (path )
281
+ if strings .HasPrefix (base , "zz_generated" ) {
282
+ return nil
283
+ }
284
+ features , parseErr := extractFeatureInfoListFromFile (fset , path )
243
285
if parseErr != nil {
244
286
return parseErr
245
287
}
@@ -249,9 +291,9 @@ func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
249
291
return allFeatures , err
250
292
}
251
293
252
- // extractFeatureInfoListFromFile extracts info all the the features from
253
- // map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate. VersionedSpecs from the given file.
254
- func extractFeatureInfoListFromFile (fset * token.FileSet , filePath string , versioned bool ) (allFeatures []featureInfo , err error ) {
294
+ // extractFeatureInfoListFromFile extracts info of all the features from
295
+ // map[featuregate.Feature]featuregate.VersionedSpecs in the given file.
296
+ func extractFeatureInfoListFromFile (fset * token.FileSet , filePath string ) (allFeatures []featureInfo , err error ) {
255
297
// Parse the file and create an AST
256
298
absFilePath , err := filepath .Abs (filePath )
257
299
if err != nil {
@@ -274,7 +316,7 @@ func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versio
274
316
if vspec , ok := spec .(* ast.ValueSpec ); ok {
275
317
for _ , name := range vspec .Names {
276
318
for _ , value := range vspec .Values {
277
- features , err := extractFeatureInfoList (filePath , value , aliasMap , variables , versioned )
319
+ features , err := extractFeatureInfoList (filePath , value , aliasMap , variables )
278
320
if err != nil {
279
321
return allFeatures , err
280
322
}
@@ -291,7 +333,7 @@ func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versio
291
333
for _ , stmt := range fd .Body .List {
292
334
if st , ok := stmt .(* ast.ReturnStmt ); ok {
293
335
for _ , value := range st .Results {
294
- features , err := extractFeatureInfoList (filePath , value , aliasMap , variables , versioned )
336
+ features , err := extractFeatureInfoList (filePath , value , aliasMap , variables )
295
337
if err != nil {
296
338
return allFeatures , err
297
339
}
@@ -332,8 +374,8 @@ func verifyAlphabeticOrder(keys []string, path string) error {
332
374
}
333
375
334
376
// extractFeatureInfoList extracts the info all the the features from
335
- // map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate. VersionedSpecs.
336
- func extractFeatureInfoList (filePath string , v ast.Expr , aliasMap map [string ]string , variables map [string ]ast.Expr , versioned bool ) ([]featureInfo , error ) {
377
+ // map[featuregate.Feature]featuregate.VersionedSpecs.
378
+ func extractFeatureInfoList (filePath string , v ast.Expr , aliasMap map [string ]string , variables map [string ]ast.Expr ) ([]featureInfo , error ) {
337
379
keys := []string {}
338
380
features := []featureInfo {}
339
381
cl , ok := v .(* ast.CompositeLit )
@@ -344,15 +386,15 @@ func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]str
344
386
if ! ok {
345
387
return features , nil
346
388
}
347
- if ! isFeatureSpecType (mt .Value , aliasMap , versioned ) {
389
+ if ! isFeatureSpecType (mt .Value , aliasMap ) {
348
390
return features , nil
349
391
}
350
392
for _ , elt := range cl .Elts {
351
393
kv , ok := elt .(* ast.KeyValueExpr )
352
394
if ! ok {
353
395
continue
354
396
}
355
- info , err := parseFeatureInfo (variables , kv , versioned )
397
+ info , err := parseFeatureInfo (variables , kv )
356
398
if err != nil {
357
399
return features , err
358
400
}
@@ -368,31 +410,24 @@ func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]str
368
410
return features , nil
369
411
}
370
412
371
- func isFeatureSpecType (v ast.Expr , aliasMap map [string ]string , versioned bool ) bool {
372
- typeName := "FeatureSpec"
373
- if versioned {
374
- typeName = "VersionedSpecs"
375
- }
413
+ func isFeatureSpecType (v ast.Expr , aliasMap map [string ]string ) bool {
414
+ typeName := "VersionedSpecs"
376
415
alias , ok := aliasMap [featureGatePkg ]
377
416
if ok {
378
417
typeName = alias + "." + typeName
379
418
}
380
419
return identifierName (v , false ) == typeName
381
420
}
382
421
383
- func parseFeatureInfo (variables map [string ]ast.Expr , kv * ast.KeyValueExpr , versioned bool ) (featureInfo , error ) {
422
+ func parseFeatureInfo (variables map [string ]ast.Expr , kv * ast.KeyValueExpr ) (featureInfo , error ) {
384
423
info := featureInfo {
385
424
Name : identifierName (kv .Key , true ),
386
425
FullName : identifierName (kv .Key , false ),
387
426
VersionedSpecs : []featureSpec {},
388
427
}
389
428
specExps := []ast.Expr {}
390
- if versioned {
391
- if cl , ok := kv .Value .(* ast.CompositeLit ); ok {
392
- specExps = append (specExps , cl .Elts ... )
393
- }
394
- } else {
395
- specExps = append (specExps , kv .Value )
429
+ if cl , ok := kv .Value .(* ast.CompositeLit ); ok {
430
+ specExps = append (specExps , cl .Elts ... )
396
431
}
397
432
for _ , specExp := range specExps {
398
433
spec , err := parseFeatureSpec (variables , specExp )
0 commit comments