Skip to content

Commit b00f88e

Browse files
authored
Merge pull request kubernetes#129414 from yongruilin/compatibility-version-featuregate
feat: Refactors featuregate lifecycle management script
2 parents bb19eac + 088daf4 commit b00f88e

13 files changed

+304
-457
lines changed

hack/update-featuregates.sh

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
# This script updates test/featuregates_linter/test_data/unversioned_feature_list.yaml and
18-
# test/featuregates_linter/test_data/versioned_feature_list.yaml with all the feature gate features.
17+
# This script updates test/compatibility_lifecycle/reference/versioned_feature_list.yaml
18+
# with all the feature gate features.
1919
# Usage: `hack/update-featuregates.sh`.
2020

2121
set -o errexit
@@ -27,4 +27,4 @@ source "${KUBE_ROOT}/hack/lib/init.sh"
2727

2828
cd "${KUBE_ROOT}"
2929

30-
go run test/featuregates_linter/main.go feature-gates update
30+
go run test/compatibility_lifecycle/main.go feature-gates update

hack/verify-featuregates.sh

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
# This script checks test/featuregates_linter/test_data/unversioned_feature_list.yaml and
18-
# test/featuregates_linter/test_data/versioned_feature_list.yaml are up to date with all the feature gate features.
17+
# This script checks test/compatibility_lifecycle/reference/versioned_feature_list.yaml
18+
# are up to date with all the feature gate features, and verifies no feature is removed before 3 versions post `lockedToDefault:true`.
1919
# We should run `hack/update-featuregates.sh` if the list is out of date.
2020
# Usage: `hack/verify-featuregates.sh`.
2121

@@ -30,7 +30,7 @@ kube::golang::setup_env
3030

3131
cd "${KUBE_ROOT}"
3232

33-
if ! go run test/featuregates_linter/main.go feature-gates verify; then
33+
if ! go run test/compatibility_lifecycle/main.go feature-gates verify; then
3434
echo "Please run 'hack/update-featuregates.sh' to update the feature list."
3535
exit 1
3636
fi
File renamed without changes.
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
This directory contains commands for [compatibility lifecycle verification](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/4330-compatibility-versions/README.md)
2+
3+
Currently, the following commands are implemented:
4+
```
5+
# Verify feature gate list is up to date
6+
go run test/compatibility_lifecycle/main.go feature-gates verify
7+
8+
# Update feature gate list
9+
go run test/compatibility_lifecycle/main.go feature-gates update
10+
```

test/featuregates_linter/cmd/feature_gates.go renamed to test/compatibility_lifecycle/cmd/feature_gates.go

+104-69
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,25 @@ import (
3131
"github.com/spf13/cobra"
3232

3333
"k8s.io/apimachinery/pkg/util/version"
34+
baseversion "k8s.io/component-base/version"
3435
yaml "sigs.k8s.io/yaml/goyaml.v2"
3536
)
3637

3738
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)
4245
)
4346

4447
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+
`
4653
)
4754

4855
type featureSpec struct {
@@ -95,39 +102,33 @@ func NewUpdateFeatureListCommand() *cobra.Command {
95102
}
96103

97104
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 {
103107
fmt.Fprintf(os.Stderr, "Failed to verify versioned feature list: \n%s", err)
104108
os.Exit(1)
105109
}
106110
}
107111

108112
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 {
114115
fmt.Fprintf(os.Stderr, "Failed to update versioned feature list: \n%s", err)
115116
os.Exit(1)
116117
}
117118
}
118119

119120
// 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.
121122
// 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 {
123124
featureList := []featureInfo{}
124-
features, err := searchPathForFeatures(filepath.Join(rootPath, "pkg"), versioned)
125+
features, err := searchPathForFeatures(filepath.Join(rootPath, "pkg"))
125126
if err != nil {
126127
return err
127128
}
128129
featureList = append(featureList, features...)
129130

130-
features, err = searchPathForFeatures(filepath.Join(rootPath, "staging"), versioned)
131+
features, err = searchPathForFeatures(filepath.Join(rootPath, "staging"))
131132
if err != nil {
132133
return err
133134
}
@@ -153,29 +154,22 @@ func verifyOrUpdateFeatureList(rootPath, featureListFile string, update, version
153154
return err
154155
}
155156

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
163159
}
164160

165161
featureListBytes, err := yaml.Marshal(featureList)
166162
if err != nil {
167163
return err
168164
}
165+
featureListBytes = []byte(generatedFileWarning + string(featureListBytes))
169166
if update {
170167
return os.WriteFile(filePath, featureListBytes, 0644)
171168
}
172169

173170
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+
179173
}
180174
return nil
181175
}
@@ -205,27 +199,70 @@ func dedupeFeatureList(featureList []featureInfo) ([]featureInfo, error) {
205199
return deduped, nil
206200
}
207201

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+
220260
}
221-
}
222-
if len(newFeatures) > 0 {
223-
return fmt.Errorf("new features added to FeatureSpec map! %v\nPlease add new features through VersionedSpecs map ONLY! ", newFeatures)
224261
}
225262
return nil
226263
}
227264

228-
func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
265+
func searchPathForFeatures(path string) ([]featureInfo, error) {
229266
allFeatures := []featureInfo{}
230267
// Create a FileSet to work with
231268
fset := token.NewFileSet()
@@ -239,7 +276,12 @@ func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
239276
if strings.HasSuffix(path, "_test.go") {
240277
return nil
241278
}
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)
243285
if parseErr != nil {
244286
return parseErr
245287
}
@@ -249,9 +291,9 @@ func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
249291
return allFeatures, err
250292
}
251293

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) {
255297
// Parse the file and create an AST
256298
absFilePath, err := filepath.Abs(filePath)
257299
if err != nil {
@@ -274,7 +316,7 @@ func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versio
274316
if vspec, ok := spec.(*ast.ValueSpec); ok {
275317
for _, name := range vspec.Names {
276318
for _, value := range vspec.Values {
277-
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables, versioned)
319+
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables)
278320
if err != nil {
279321
return allFeatures, err
280322
}
@@ -291,7 +333,7 @@ func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versio
291333
for _, stmt := range fd.Body.List {
292334
if st, ok := stmt.(*ast.ReturnStmt); ok {
293335
for _, value := range st.Results {
294-
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables, versioned)
336+
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables)
295337
if err != nil {
296338
return allFeatures, err
297339
}
@@ -332,8 +374,8 @@ func verifyAlphabeticOrder(keys []string, path string) error {
332374
}
333375

334376
// 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) {
337379
keys := []string{}
338380
features := []featureInfo{}
339381
cl, ok := v.(*ast.CompositeLit)
@@ -344,15 +386,15 @@ func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]str
344386
if !ok {
345387
return features, nil
346388
}
347-
if !isFeatureSpecType(mt.Value, aliasMap, versioned) {
389+
if !isFeatureSpecType(mt.Value, aliasMap) {
348390
return features, nil
349391
}
350392
for _, elt := range cl.Elts {
351393
kv, ok := elt.(*ast.KeyValueExpr)
352394
if !ok {
353395
continue
354396
}
355-
info, err := parseFeatureInfo(variables, kv, versioned)
397+
info, err := parseFeatureInfo(variables, kv)
356398
if err != nil {
357399
return features, err
358400
}
@@ -368,31 +410,24 @@ func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]str
368410
return features, nil
369411
}
370412

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"
376415
alias, ok := aliasMap[featureGatePkg]
377416
if ok {
378417
typeName = alias + "." + typeName
379418
}
380419
return identifierName(v, false) == typeName
381420
}
382421

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) {
384423
info := featureInfo{
385424
Name: identifierName(kv.Key, true),
386425
FullName: identifierName(kv.Key, false),
387426
VersionedSpecs: []featureSpec{},
388427
}
389428
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...)
396431
}
397432
for _, specExp := range specExps {
398433
spec, err := parseFeatureSpec(variables, specExp)

0 commit comments

Comments
 (0)