mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 02:08:13 +00:00 
			
		
		
		
	Verify: add static analysis to verify new feature gates are added as versioned feature specs.
Signed-off-by: Siyuan Zhang <sizhang@google.com>
This commit is contained in:
		| @@ -77,6 +77,7 @@ QUICK_PATTERNS+=( | ||||
|   "verify-api-groups.sh" | ||||
|   "verify-boilerplate.sh" | ||||
|   "verify-external-dependencies-version.sh" | ||||
|   "verify-featuregates.sh" | ||||
|   "verify-fieldname-docs.sh" | ||||
|   "verify-gofmt.sh" | ||||
|   "verify-imports.sh" | ||||
|   | ||||
							
								
								
									
										30
									
								
								hack/update-featuregates.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								hack/update-featuregates.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| # Copyright 2024 The Kubernetes Authors. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
|  | ||||
| # This script updates  test/featuregates_linter/test_data/unversioned_feature_list.yaml and | ||||
| # test/featuregates_linter/test_data/versioned_feature_list.yaml with all the feature gate features. | ||||
| # Usage: `hack/update-featuregates.sh`. | ||||
|  | ||||
| set -o errexit | ||||
| set -o nounset | ||||
| set -o pipefail | ||||
|  | ||||
| KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. | ||||
| source "${KUBE_ROOT}/hack/lib/init.sh" | ||||
|  | ||||
| cd "${KUBE_ROOT}" | ||||
|  | ||||
| go run test/featuregates_linter/main.go feature-gates update | ||||
							
								
								
									
										31
									
								
								hack/verify-featuregates.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										31
									
								
								hack/verify-featuregates.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| # Copyright 2024 The Kubernetes Authors. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
|  | ||||
| # This script checks test/featuregates_linter/test_data/unversioned_feature_list.yaml and | ||||
| # test/featuregates_linter/test_data/versioned_feature_list.yaml are up to date with all the feature gate features. | ||||
| # We should run `hack/update-featuregates.sh` if the list is out of date. | ||||
| # Usage: `hack/verify-featuregates.sh`. | ||||
|  | ||||
| set -o errexit | ||||
| set -o nounset | ||||
| set -o pipefail | ||||
|  | ||||
| KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. | ||||
| source "${KUBE_ROOT}/hack/lib/init.sh" | ||||
|  | ||||
| cd "${KUBE_ROOT}" | ||||
|  | ||||
| go run test/featuregates_linter/main.go feature-gates verify | ||||
							
								
								
									
										14
									
								
								test/featuregates_linter/OWNERS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								test/featuregates_linter/OWNERS
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| # See the OWNERS docs at https://go.k8s.io/owners | ||||
|  | ||||
| approvers: | ||||
|   - feature-approvers | ||||
|   - sig-api-machinery-api-approvers | ||||
|   - jpbetz | ||||
|   - siyuanfoundation | ||||
| reviewers: | ||||
|   - feature-approvers | ||||
|   - sig-api-machinery-api-reviewers | ||||
|   - jpbetz | ||||
|   - siyuanfoundation | ||||
| labels: | ||||
|   - sig/api-machinery | ||||
							
								
								
									
										8
									
								
								test/featuregates_linter/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/featuregates_linter/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| This directory contains static analysis scripts for verify functions. | ||||
|  | ||||
| Currently, the following commands are implemented: | ||||
| ``` | ||||
| go run test/featuregates_linter/main.go feature-gates verify-no-new-unversioned --new-features-file="${new_features_file}" --old-features-file="${old_features_file}" | ||||
|  | ||||
| go run test/featuregates_linter/main.go feature-gates verify-alphabetic-order --features-file="${features_file}" | ||||
| ``` | ||||
							
								
								
									
										477
									
								
								test/featuregates_linter/cmd/feature_gates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										477
									
								
								test/featuregates_linter/cmd/feature_gates.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,477 @@ | ||||
| /* | ||||
| Copyright 2024 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"go/ast" | ||||
| 	"go/parser" | ||||
| 	"go/token" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"gopkg.in/yaml.v2" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	alphabeticalOrder          bool | ||||
| 	k8RootPath                 string | ||||
| 	unversionedFeatureListFile = "test/featuregates_linter/test_data/unversioned_feature_list.yaml" | ||||
| 	versionedFeatureListFile   = "test/featuregates_linter/test_data/versioned_feature_list.yaml" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	featureGatePkg = "\"k8s.io/component-base/featuregate\"" | ||||
| ) | ||||
|  | ||||
| type featureSpec struct { | ||||
| 	Default       bool   `yaml:"default" json:"default"` | ||||
| 	LockToDefault bool   `yaml:"lockToDefault" json:"lockToDefault"` | ||||
| 	PreRelease    string `yaml:"preRelease" json:"preRelease"` | ||||
| 	Version       string `yaml:"version" json:"version"` | ||||
| } | ||||
|  | ||||
| type featureInfo struct { | ||||
| 	Name           string        `yaml:"name" json:"name"` | ||||
| 	FullName       string        `yaml:"-" json:"-"` | ||||
| 	VersionedSpecs []featureSpec `yaml:"versionedSpecs" json:"versionedSpecs"` | ||||
| } | ||||
|  | ||||
| // NewFeatureGatesCommand returns the cobra command for "feature-gates". | ||||
| func NewFeatureGatesCommand() *cobra.Command { | ||||
| 	cmd := &cobra.Command{ | ||||
| 		Use:   "feature-gates <subcommand>", | ||||
| 		Short: "Commands related to feature gate verifications and updates", | ||||
| 	} | ||||
| 	defaultRootPath, err := filepath.Abs(".") | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	cmd.Flags().StringVar(&k8RootPath, "root-path", defaultRootPath, "absolute path of the k8s repository") | ||||
|  | ||||
| 	cmd.AddCommand(NewVerifyFeatureListCommand()) | ||||
| 	cmd.AddCommand(NewUpdateFeatureListCommand()) | ||||
| 	return cmd | ||||
| } | ||||
|  | ||||
| func NewVerifyFeatureListCommand() *cobra.Command { | ||||
| 	cmd := cobra.Command{ | ||||
| 		Use:   "verify", | ||||
| 		Short: "Verifies feature list files are up to date.", | ||||
| 		Run:   verifyFeatureListFunc, | ||||
| 	} | ||||
| 	cmd.Flags().BoolVar(&alphabeticalOrder, "alphabetical-order", false, "if true, verify all features in any FeatureSpec map are ordered aphabetically") | ||||
| 	return &cmd | ||||
| } | ||||
|  | ||||
| func NewUpdateFeatureListCommand() *cobra.Command { | ||||
| 	cmd := cobra.Command{ | ||||
| 		Use:   "update", | ||||
| 		Short: "updates feature list files.", | ||||
| 		Run:   updateFeatureListFunc, | ||||
| 	} | ||||
| 	return &cmd | ||||
| } | ||||
|  | ||||
| func verifyFeatureListFunc(cmd *cobra.Command, args []string) { | ||||
| 	if err := verifyOrUpdateFeatureList(k8RootPath, unversionedFeatureListFile, false, false); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	if err := verifyOrUpdateFeatureList(k8RootPath, versionedFeatureListFile, false, true); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func updateFeatureListFunc(cmd *cobra.Command, args []string) { | ||||
| 	if err := verifyOrUpdateFeatureList(k8RootPath, unversionedFeatureListFile, true, false); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	if err := verifyOrUpdateFeatureList(k8RootPath, versionedFeatureListFile, true, true); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // verifyOrUpdateFeatureList walks all the files under pkg/ and staging/ to find the list of all the features in | ||||
| // map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate.VersionedSpecs. | ||||
| // It will then update the feature list in featureListFile, or verifies there is no change from the existing list. | ||||
| func verifyOrUpdateFeatureList(rootPath, featureListFile string, update, versioned bool) error { | ||||
| 	featureList := []featureInfo{} | ||||
| 	features, err := searchPathForFeatures(filepath.Join(rootPath, "pkg"), versioned) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	featureList = append(featureList, features...) | ||||
|  | ||||
| 	features, err = searchPathForFeatures(filepath.Join(rootPath, "staging"), versioned) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	featureList = append(featureList, features...) | ||||
|  | ||||
| 	sort.Slice(featureList, func(i, j int) bool { | ||||
| 		return strings.ToLower(featureList[i].Name) < strings.ToLower(featureList[j].Name) | ||||
| 	}) | ||||
| 	featureList, err = dedupeFeatureList(featureList) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	filePath := filepath.Join(rootPath, featureListFile) | ||||
| 	baseFeatureListBytes, err := os.ReadFile(filePath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	baseFeatureList := []featureInfo{} | ||||
| 	err = yaml.Unmarshal(baseFeatureListBytes, &baseFeatureList) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// only feature deletion is allowed for unversioned features. | ||||
| 	// all new features or feature updates should be migrated to versioned feature gate. | ||||
| 	// https://github.com/kubernetes/kubernetes/issues/125031 | ||||
| 	if !versioned { | ||||
| 		if err := verifyFeatureDeletionOnly(featureList, baseFeatureList); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if update { | ||||
| 		data, err := yaml.Marshal(featureList) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		return os.WriteFile(filePath, data, 0644) | ||||
| 	} | ||||
|  | ||||
| 	if diff := cmp.Diff(featureList, baseFeatureList); diff != "" { | ||||
| 		return fmt.Errorf("detected diff in unversioned feature list, diff: \n%s", diff) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func dedupeFeatureList(featureList []featureInfo) ([]featureInfo, error) { | ||||
| 	if featureList == nil || len(featureList) < 1 { | ||||
| 		return featureList, nil | ||||
| 	} | ||||
| 	last := featureList[0] | ||||
| 	// clean up FullName field for the final output | ||||
| 	last.FullName = "" | ||||
| 	deduped := []featureInfo{last} | ||||
| 	for i := 1; i < len(featureList); i++ { | ||||
| 		f := featureList[i] | ||||
| 		if f.Name == last.Name { | ||||
| 			// if it is a duplicate feature, verify the lifecycles are the same | ||||
| 			if !reflect.DeepEqual(last.VersionedSpecs, f.VersionedSpecs) { | ||||
| 				return deduped, fmt.Errorf("multiple conflicting specs found for feature:%s, [\n%v, \n%v]", last.Name, last.VersionedSpecs, f.VersionedSpecs) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		last = f | ||||
| 		last.FullName = "" | ||||
| 		deduped = append(deduped, last) | ||||
|  | ||||
| 	} | ||||
| 	return deduped, nil | ||||
| } | ||||
|  | ||||
| func verifyFeatureDeletionOnly(newFeatureList []featureInfo, oldFeatureList []featureInfo) error { | ||||
| 	oldFeatureSet := make(map[string]*featureInfo) | ||||
| 	for _, f := range oldFeatureList { | ||||
| 		oldFeatureSet[f.Name] = &f | ||||
| 	} | ||||
| 	newFeatures := []string{} | ||||
| 	for _, f := range newFeatureList { | ||||
| 		oldSpecs, found := oldFeatureSet[f.Name] | ||||
| 		if !found { | ||||
| 			newFeatures = append(newFeatures, f.Name) | ||||
| 		} else if !reflect.DeepEqual(*oldSpecs, f) { | ||||
| 			return fmt.Errorf("feature %s changed with diff: %s", f.Name, cmp.Diff(*oldSpecs, f)) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(newFeatures) > 0 { | ||||
| 		return fmt.Errorf("new features added to FeatureSpec map! %v\nPlease add new features through VersionedSpecs map ONLY! ", newFeatures) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) { | ||||
| 	allFeatures := []featureInfo{} | ||||
| 	// Create a FileSet to work with | ||||
| 	fset := token.NewFileSet() | ||||
| 	err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { | ||||
| 		if strings.HasPrefix(path, "vendor") || strings.HasPrefix(path, "_") { | ||||
| 			return filepath.SkipDir | ||||
| 		} | ||||
| 		if !strings.HasSuffix(path, ".go") { | ||||
| 			return nil | ||||
| 		} | ||||
| 		if strings.HasSuffix(path, "_test.go") { | ||||
| 			return nil | ||||
| 		} | ||||
| 		features, parseErr := extractFeatureInfoListFromFile(fset, path, versioned) | ||||
| 		if parseErr != nil { | ||||
| 			return parseErr | ||||
| 		} | ||||
| 		allFeatures = append(allFeatures, features...) | ||||
| 		return nil | ||||
| 	}) | ||||
| 	return allFeatures, err | ||||
| } | ||||
|  | ||||
| // extractFeatureInfoListFromFile extracts info all the the features from | ||||
| // map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate.VersionedSpecs from the given file. | ||||
| func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versioned bool) (allFeatures []featureInfo, err error) { | ||||
| 	// Parse the file and create an AST | ||||
| 	absFilePath, err := filepath.Abs(filePath) | ||||
| 	if err != nil { | ||||
| 		return allFeatures, err | ||||
| 	} | ||||
| 	file, err := parser.ParseFile(fset, absFilePath, nil, parser.AllErrors) | ||||
| 	if err != nil { | ||||
| 		return allFeatures, err | ||||
| 	} | ||||
| 	aliasMap := importAliasMap(file.Imports) | ||||
| 	// any file containing features should have imported the featuregate pkg. | ||||
| 	if _, ok := aliasMap[featureGatePkg]; !ok { | ||||
| 		return allFeatures, err | ||||
| 	} | ||||
| 	variables := globalVariableDeclarations(file) | ||||
|  | ||||
| 	for _, d := range file.Decls { | ||||
| 		if gd, ok := d.(*ast.GenDecl); ok && (gd.Tok == token.CONST || gd.Tok == token.VAR) { | ||||
| 			for _, spec := range gd.Specs { | ||||
| 				if vspec, ok := spec.(*ast.ValueSpec); ok { | ||||
| 					for _, name := range vspec.Names { | ||||
| 						for _, value := range vspec.Values { | ||||
| 							features, err := extractFeatureInfoList(filePath, value, aliasMap, variables, versioned) | ||||
| 							if err != nil { | ||||
| 								return allFeatures, err | ||||
| 							} | ||||
| 							if len(features) > 0 { | ||||
| 								fmt.Printf("found %d features in FeatureSpecMap var %s in file: %s\n", len(features), name, filePath) | ||||
| 								allFeatures = append(allFeatures, features...) | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if fd, ok := d.(*ast.FuncDecl); ok { | ||||
| 			for _, stmt := range fd.Body.List { | ||||
| 				if st, ok := stmt.(*ast.ReturnStmt); ok { | ||||
| 					for _, value := range st.Results { | ||||
| 						features, err := extractFeatureInfoList(filePath, value, aliasMap, variables, versioned) | ||||
| 						if err != nil { | ||||
| 							return allFeatures, err | ||||
| 						} | ||||
| 						if len(features) > 0 { | ||||
| 							fmt.Printf("found %d features in FeatureSpecMap of func %s in file: %s\n", len(features), fd.Name, filePath) | ||||
| 							allFeatures = append(allFeatures, features...) | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func getPkgPrefix(s string) string { | ||||
| 	if strings.Contains(s, ".") { | ||||
| 		return strings.Split(s, ".")[0] | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func verifyAlphabeticOrder(keys []string, path string) error { | ||||
| 	keysSorted := make([]string, len(keys)) | ||||
| 	copy(keysSorted, keys) | ||||
| 	sort.Slice(keysSorted, func(i, j int) bool { | ||||
| 		keyI := strings.ToLower(keysSorted[i]) | ||||
| 		keyJ := strings.ToLower(keysSorted[j]) | ||||
| 		if getPkgPrefix(keyI) == getPkgPrefix(keyJ) { | ||||
| 			return keyI < keyJ | ||||
| 		} | ||||
| 		return getPkgPrefix(keyI) < getPkgPrefix(keyJ) | ||||
| 	}) | ||||
| 	if diff := cmp.Diff(keys, keysSorted); diff != "" { | ||||
| 		return fmt.Errorf("features in %s are not in alphabetic order, diff: %s", path, diff) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // extractFeatureInfoList extracts the info all the the features from | ||||
| // map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate.VersionedSpecs. | ||||
| func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]string, variables map[string]ast.Expr, versioned bool) ([]featureInfo, error) { | ||||
| 	keys := []string{} | ||||
| 	features := []featureInfo{} | ||||
| 	cl, ok := v.(*ast.CompositeLit) | ||||
| 	if !ok { | ||||
| 		return features, nil | ||||
| 	} | ||||
| 	mt, ok := cl.Type.(*ast.MapType) | ||||
| 	if !ok { | ||||
| 		return features, nil | ||||
| 	} | ||||
| 	if !isFeatureSpecType(mt.Value, aliasMap, versioned) { | ||||
| 		return features, nil | ||||
| 	} | ||||
| 	for _, elt := range cl.Elts { | ||||
| 		kv, ok := elt.(*ast.KeyValueExpr) | ||||
| 		if !ok { | ||||
| 			continue | ||||
| 		} | ||||
| 		info, err := parseFeatureInfo(variables, kv, versioned) | ||||
| 		if err != nil { | ||||
| 			return features, err | ||||
| 		} | ||||
| 		features = append(features, info) | ||||
| 		keys = append(keys, info.FullName) | ||||
| 	} | ||||
| 	if alphabeticalOrder { | ||||
| 		// verifies the features are sorted in the map | ||||
| 		if err := verifyAlphabeticOrder(keys, filePath); err != nil { | ||||
| 			return features, err | ||||
| 		} | ||||
| 	} | ||||
| 	return features, nil | ||||
| } | ||||
|  | ||||
| func isFeatureSpecType(v ast.Expr, aliasMap map[string]string, versioned bool) bool { | ||||
| 	typeName := "FeatureSpec" | ||||
| 	if versioned { | ||||
| 		typeName = "VersionedSpecs" | ||||
| 	} | ||||
| 	alias, ok := aliasMap[featureGatePkg] | ||||
| 	if ok { | ||||
| 		typeName = alias + "." + typeName | ||||
| 	} | ||||
| 	return identifierName(v, false) == typeName | ||||
| } | ||||
|  | ||||
| func parseFeatureInfo(variables map[string]ast.Expr, kv *ast.KeyValueExpr, versioned bool) (featureInfo, error) { | ||||
| 	info := featureInfo{ | ||||
| 		Name:           identifierName(kv.Key, true), | ||||
| 		FullName:       identifierName(kv.Key, false), | ||||
| 		VersionedSpecs: []featureSpec{}, | ||||
| 	} | ||||
| 	specExps := []ast.Expr{} | ||||
| 	if versioned { | ||||
| 		if cl, ok := kv.Value.(*ast.CompositeLit); ok { | ||||
| 			specExps = append(specExps, cl.Elts...) | ||||
| 		} | ||||
| 	} else { | ||||
| 		specExps = append(specExps, kv.Value) | ||||
| 	} | ||||
| 	for _, specExp := range specExps { | ||||
| 		spec, err := parseFeatureSpec(variables, specExp) | ||||
| 		if err != nil { | ||||
| 			return info, err | ||||
| 		} | ||||
| 		info.VersionedSpecs = append(info.VersionedSpecs, spec) | ||||
| 	} | ||||
| 	// verify FeatureSpec in VersionedSpecs are ordered by version. | ||||
| 	if len(info.VersionedSpecs) > 1 { | ||||
| 		specsSorted := make([]featureSpec, len(info.VersionedSpecs)) | ||||
| 		copy(specsSorted, info.VersionedSpecs) | ||||
| 		sort.Slice(specsSorted, func(i, j int) bool { | ||||
| 			verI := version.MustParse(specsSorted[i].Version) | ||||
| 			verJ := version.MustParse(specsSorted[j].Version) | ||||
| 			return verI.LessThan(verJ) | ||||
| 		}) | ||||
| 		if diff := cmp.Diff(info.VersionedSpecs, specsSorted); diff != "" { | ||||
| 			return info, fmt.Errorf("VersionedSpecs in feature %s are not ordered by version, diff: %s", info.Name, diff) | ||||
| 		} | ||||
| 	} | ||||
| 	return info, nil | ||||
| } | ||||
|  | ||||
| func parseFeatureSpec(variables map[string]ast.Expr, v ast.Expr) (featureSpec, error) { | ||||
| 	spec := featureSpec{} | ||||
| 	cl, ok := v.(*ast.CompositeLit) | ||||
| 	if !ok { | ||||
| 		return spec, fmt.Errorf("expect FeatureSpec to be a CompositeLit") | ||||
| 	} | ||||
| 	for _, elt := range cl.Elts { | ||||
| 		switch eltType := elt.(type) { | ||||
| 		case *ast.KeyValueExpr: | ||||
| 			key := identifierName(eltType.Key, true) | ||||
| 			switch key { | ||||
| 			case "Default": | ||||
| 				boolValue, err := parseBool(variables, eltType.Value) | ||||
| 				if err != nil { | ||||
| 					return spec, err | ||||
| 				} | ||||
| 				spec.Default = boolValue | ||||
|  | ||||
| 			case "LockToDefault": | ||||
| 				boolValue, err := parseBool(variables, eltType.Value) | ||||
| 				if err != nil { | ||||
| 					return spec, err | ||||
| 				} | ||||
| 				spec.LockToDefault = boolValue | ||||
|  | ||||
| 			case "PreRelease": | ||||
| 				spec.PreRelease = identifierName(eltType.Value, true) | ||||
|  | ||||
| 			case "Version": | ||||
| 				ver, err := parseVersion(eltType.Value) | ||||
| 				if err != nil { | ||||
| 					return spec, err | ||||
| 				} | ||||
| 				spec.Version = ver | ||||
| 			} | ||||
|  | ||||
| 		default: | ||||
| 			return spec, fmt.Errorf("cannot parse FeatureSpec") | ||||
|  | ||||
| 		} | ||||
| 	} | ||||
| 	return spec, nil | ||||
| } | ||||
|  | ||||
| func parseVersion(v ast.Expr) (string, error) { | ||||
| 	fc, ok := v.(*ast.CallExpr) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("expect FeatureSpec Version to be a function call") | ||||
| 	} | ||||
| 	funcName := identifierName(fc.Fun, true) | ||||
| 	switch funcName { | ||||
| 	case "MustParse": | ||||
| 		return basicStringLiteral(fc.Args[0]) | ||||
|  | ||||
| 	case "MajorMinor": | ||||
| 		major, err := basicIntLiteral(fc.Args[0]) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		minor, err := basicIntLiteral(fc.Args[1]) | ||||
| 		return fmt.Sprintf("%d.%d", major, minor), err | ||||
|  | ||||
| 	default: | ||||
| 		return "", fmt.Errorf("unrecognized function call in FeatureSpec Version") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										985
									
								
								test/featuregates_linter/cmd/feature_gates_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										985
									
								
								test/featuregates_linter/cmd/feature_gates_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,985 @@ | ||||
| /* | ||||
| Copyright 2024 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"go/ast" | ||||
| 	"go/token" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| ) | ||||
|  | ||||
| func TestVerifyAlphabeticOrder(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name      string | ||||
| 		keys      []string | ||||
| 		expectErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "ordered versioned specs", | ||||
| 			keys: []string{ | ||||
| 				"SchedulerQueueingHints", "SELinuxMount", "ServiceAccountTokenJTI", | ||||
| 				"genericfeatures.AdmissionWebhookMatchConditions", | ||||
| 				"genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "unordered versioned specs", | ||||
| 			keys: []string{ | ||||
| 				"SELinuxMount", "SchedulerQueueingHints", "ServiceAccountTokenJTI", | ||||
| 				"genericfeatures.AdmissionWebhookMatchConditions", | ||||
| 				"genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 			}, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "unordered versioned specs with mixed pkg prefix", | ||||
| 			keys: []string{ | ||||
| 				"genericfeatures.AdmissionWebhookMatchConditions", | ||||
| 				"SchedulerQueueingHints", "SELinuxMount", "ServiceAccountTokenJTI", | ||||
| 				"genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 			}, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "unordered versioned specs with pkg prefix", | ||||
| 			keys: []string{ | ||||
| 				"SchedulerQueueingHints", "SELinuxMount", "ServiceAccountTokenJTI", | ||||
| 				"genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 				"genericfeatures.AdmissionWebhookMatchConditions", | ||||
| 			}, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			err := verifyAlphabeticOrder(tc.keys, "") | ||||
| 			if tc.expectErr { | ||||
| 				if err == nil { | ||||
| 					t.Fatal("expected error, got nil") | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestVerifyOrUpdateFeatureListUnversioned(t *testing.T) { | ||||
| 	featureListFileContent := `- name: AppArmorFields | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ClusterTrustBundleProjection | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CPUCFSQuotaPeriod | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| ` | ||||
| 	tests := []struct { | ||||
| 		name                          string | ||||
| 		goFileContent                 string | ||||
| 		updatedFeatureListFileContent string | ||||
| 		expectVerifyErr               bool | ||||
| 		expectUpdateErr               bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "no change", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.Beta}, | ||||
| 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	ClusterTrustBundleProjection: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
| var otherFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.Beta}, | ||||
| } | ||||
| `, | ||||
| 			updatedFeatureListFileContent: featureListFileContent, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "same feature added twice with different lifecycle", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.Beta}, | ||||
| 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	ClusterTrustBundleProjection: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
| 	var otherFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			expectUpdateErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "new feature added", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.Beta}, | ||||
| 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	ClusterTrustBundleProjection: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	SELinuxMount: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			expectUpdateErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "delete feature", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.Beta}, | ||||
| 	ClusterTrustBundleProjection: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			updatedFeatureListFileContent: `- name: AppArmorFields | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ClusterTrustBundleProjection | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| `, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "update feature", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.GA}, | ||||
| 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	ClusterTrustBundleProjection: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
| 			`, | ||||
| 			expectVerifyErr: true, | ||||
| 			expectUpdateErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			featureListFile := writeContentToTmpFile(t, "", "feature_list.yaml", strings.TrimSpace(featureListFileContent)) | ||||
| 			tmpDir := filepath.Dir(featureListFile.Name()) | ||||
| 			_ = writeContentToTmpFile(t, tmpDir, "pkg/new_features.go", tc.goFileContent) | ||||
| 			err := verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), false, false) | ||||
| 			if tc.expectVerifyErr != (err != nil) { | ||||
| 				t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err) | ||||
| 			} | ||||
| 			err = verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), true, false) | ||||
| 			if tc.expectUpdateErr != (err != nil) { | ||||
| 				t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err) | ||||
| 			} | ||||
| 			if tc.expectUpdateErr { | ||||
| 				return | ||||
| 			} | ||||
| 			updatedFeatureListFileContent, err := os.ReadFile(featureListFile.Name()) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if diff := cmp.Diff(string(updatedFeatureListFileContent), tc.updatedFeatureListFileContent); diff != "" { | ||||
| 				t.Errorf("updatedFeatureListFileContent does not match expected, diff=%s", diff) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestVerifyOrUpdateFeatureListVersioned(t *testing.T) { | ||||
| 	featureListFileContent := `- name: APIListChunking | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "1.30" | ||||
| - name: AppArmorFields | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.30" | ||||
| - name: CPUCFSQuotaPeriod | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "1.30" | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.31" | ||||
| ` | ||||
| 	tests := []struct { | ||||
| 		name                          string | ||||
| 		goFileContent                 string | ||||
| 		updatedFeatureListFileContent string | ||||
| 		expectVerifyErr               bool | ||||
| 		expectUpdateErr               bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "no change", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	genericfeatures.APIListChunking: { | ||||
| 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 	}, | ||||
| } | ||||
| var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| } | ||||
| `, | ||||
| 			updatedFeatureListFileContent: featureListFileContent, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "same feature added twice with different lifecycle", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	genericfeatures.APIListChunking: { | ||||
| 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 	}, | ||||
| } | ||||
| var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Alpha}, | ||||
| 	}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			expectUpdateErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "VersionedSpecs not ordered by version", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	genericfeatures.APIListChunking: { | ||||
| 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 	}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			expectUpdateErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "add new feature", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	ClusterTrustBundleProjection: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	genericfeatures.APIListChunking: { | ||||
| 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 	}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			updatedFeatureListFileContent: `- name: APIListChunking | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "1.30" | ||||
| - name: AppArmorFields | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.30" | ||||
| - name: ClusterTrustBundleProjection | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.30" | ||||
| - name: CPUCFSQuotaPeriod | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "1.30" | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.31" | ||||
| `, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "remove feature", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	genericfeatures.APIListChunking: { | ||||
| 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 	}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			updatedFeatureListFileContent: `- name: APIListChunking | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "1.30" | ||||
| - name: CPUCFSQuotaPeriod | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "1.30" | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.31" | ||||
| `, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "update feature", | ||||
| 			goFileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 		{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA}, | ||||
| 	}, | ||||
| 	genericfeatures.APIListChunking: { | ||||
| 		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 	}, | ||||
| } | ||||
| `, | ||||
| 			expectVerifyErr: true, | ||||
| 			updatedFeatureListFileContent: `- name: APIListChunking | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "1.30" | ||||
| - name: AppArmorFields | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.30" | ||||
| - name: CPUCFSQuotaPeriod | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "1.30" | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "1.31" | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: GA | ||||
|     version: "1.32" | ||||
| `, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			featureListFile := writeContentToTmpFile(t, "", "feature_list.yaml", strings.TrimSpace(featureListFileContent)) | ||||
| 			tmpDir := filepath.Dir(featureListFile.Name()) | ||||
| 			_ = writeContentToTmpFile(t, tmpDir, "pkg/new_features.go", tc.goFileContent) | ||||
| 			err := verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), false, true) | ||||
| 			if tc.expectVerifyErr != (err != nil) { | ||||
| 				t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err) | ||||
| 			} | ||||
| 			err = verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), true, true) | ||||
| 			if tc.expectUpdateErr != (err != nil) { | ||||
| 				t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err) | ||||
| 			} | ||||
| 			if tc.expectUpdateErr { | ||||
| 				return | ||||
| 			} | ||||
| 			updatedFeatureListFileContent, err := os.ReadFile(featureListFile.Name()) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if diff := cmp.Diff(string(updatedFeatureListFileContent), tc.updatedFeatureListFileContent); diff != "" { | ||||
| 				t.Errorf("updatedFeatureListFileContent does not match expected, diff=%s", diff) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestExtractFeatureInfoListFromFile(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name             string | ||||
| 		fileContent      string | ||||
| 		expectedFeatures []featureInfo | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "map in var", | ||||
| 			fileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: featuregate.Beta}, | ||||
| 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	genericfeatures.AggregatedDiscoveryEndpoint: {Default: false, PreRelease: featuregate.Alpha}, | ||||
| } | ||||
| 			`, | ||||
| 			expectedFeatures: []featureInfo{ | ||||
| 				{ | ||||
| 					Name:     "AppArmorFields", | ||||
| 					FullName: "AppArmorFields", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: true, PreRelease: "Beta"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "CPUCFSQuotaPeriod", | ||||
| 					FullName: "CPUCFSQuotaPeriod", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "AggregatedDiscoveryEndpoint", | ||||
| 					FullName: "genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "map in var with alias", | ||||
| 			fileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	fg "k8s.io/component-base/featuregate" | ||||
| ) | ||||
| const ( | ||||
|     CPUCFSQuotaPeriodDefault = false | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[fg.Feature]fg.FeatureSpec{ | ||||
| 	AppArmorFields: {Default: true, PreRelease: fg.Beta}, | ||||
| 	CPUCFSQuotaPeriod: {Default: CPUCFSQuotaPeriodDefault, PreRelease: fg.Alpha}, | ||||
| 	genericfeatures.AggregatedDiscoveryEndpoint: {Default: false, PreRelease: fg.Alpha}, | ||||
| } | ||||
| 			`, | ||||
| 			expectedFeatures: []featureInfo{ | ||||
| 				{ | ||||
| 					Name:     "AppArmorFields", | ||||
| 					FullName: "AppArmorFields", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: true, PreRelease: "Beta"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "CPUCFSQuotaPeriod", | ||||
| 					FullName: "CPUCFSQuotaPeriod", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "AggregatedDiscoveryEndpoint", | ||||
| 					FullName: "genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "map in function return statement", | ||||
| 			fileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	ComponentSLIs featuregate.Feature = "ComponentSLIs" | ||||
| ) | ||||
|  | ||||
| func featureGates() map[featuregate.Feature]featuregate.FeatureSpec { | ||||
| 	return map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 		ComponentSLIs: {Default: true, PreRelease: featuregate.Beta}, | ||||
| 	} | ||||
| } | ||||
| 			`, | ||||
| 			expectedFeatures: []featureInfo{ | ||||
| 				{ | ||||
| 					Name:     "ComponentSLIs", | ||||
| 					FullName: "ComponentSLIs", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: true, PreRelease: "Beta"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		// 		{ | ||||
| 		// 			name: "map in function call", | ||||
| 		// 			fileContent: ` | ||||
| 		// package features | ||||
|  | ||||
| 		// import ( | ||||
| 		// 	"k8s.io/component-base/featuregate" | ||||
| 		// ) | ||||
|  | ||||
| 		// const ( | ||||
| 		// 	ComponentSLIs featuregate.Feature = "ComponentSLIs" | ||||
| 		// ) | ||||
|  | ||||
| 		// func featureGates() featuregate.FeatureGate { | ||||
| 		// 	featureGate := featuregate.NewFeatureGate() | ||||
| 		// 	_ = featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ | ||||
| 		// 		ComponentSLIs: { | ||||
| 		// 			Default: true, PreRelease: featuregate.Beta}}) | ||||
| 		// 	return featureGate | ||||
| 		// } | ||||
| 		// 			`, | ||||
| 		// 		}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			newFile := writeContentToTmpFile(t, "", "new_features.go", tc.fileContent) | ||||
| 			fset := token.NewFileSet() | ||||
| 			features, err := extractFeatureInfoListFromFile(fset, newFile.Name(), false) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if diff := cmp.Diff(features, tc.expectedFeatures); diff != "" { | ||||
| 				t.Errorf("File contents: got=%v, want=%v, diff=%s", features, tc.expectedFeatures, diff) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestExtractFeatureInfoListFromFileVersioned(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name             string | ||||
| 		fileContent      string | ||||
| 		expectedFeatures []featureInfo | ||||
| 		expectErr        bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "map in var", | ||||
| 			fileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 	}, | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	}, | ||||
| 	genericfeatures.AggregatedDiscoveryEndpoint: { | ||||
| 		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 	}, | ||||
| } | ||||
| 			`, | ||||
| 			expectedFeatures: []featureInfo{ | ||||
| 				{ | ||||
| 					Name:     "AppArmorFields", | ||||
| 					FullName: "AppArmorFields", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: true, PreRelease: "Beta", Version: "1.31"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "CPUCFSQuotaPeriod", | ||||
| 					FullName: "CPUCFSQuotaPeriod", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha", Version: "1.29"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "AggregatedDiscoveryEndpoint", | ||||
| 					FullName: "genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha", Version: "1.30"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "map in var with alias", | ||||
| 			fileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/version" | ||||
| 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||
| 	fg "k8s.io/component-base/featuregate" | ||||
| ) | ||||
| const ( | ||||
|     CPUCFSQuotaPeriodDefault = false | ||||
| ) | ||||
| var defaultVersionedKubernetesFeatureGates = map[fg.Feature]fg.VersionedSpecs{ | ||||
| 	AppArmorFields: { | ||||
| 		{Version: version.MustParse("1.31"), Default: true, PreRelease: fg.Beta}, | ||||
| 	}, | ||||
| 	CPUCFSQuotaPeriod: { | ||||
| 		{Version: version.MustParse("1.29"), Default: false, PreRelease: fg.Alpha}, | ||||
| 	}, | ||||
| 	genericfeatures.AggregatedDiscoveryEndpoint: { | ||||
| 		{Version: version.MustParse("1.30"), Default: false, PreRelease: fg.Alpha}, | ||||
| 	}, | ||||
| } | ||||
| 			`, | ||||
| 			expectedFeatures: []featureInfo{ | ||||
| 				{ | ||||
| 					Name:     "AppArmorFields", | ||||
| 					FullName: "AppArmorFields", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: true, PreRelease: "Beta", Version: "1.31"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "CPUCFSQuotaPeriod", | ||||
| 					FullName: "CPUCFSQuotaPeriod", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha", Version: "1.29"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:     "AggregatedDiscoveryEndpoint", | ||||
| 					FullName: "genericfeatures.AggregatedDiscoveryEndpoint", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha", Version: "1.30"}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "map in function return statement", | ||||
| 			fileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	ComponentSLIs featuregate.Feature = "ComponentSLIs" | ||||
| ) | ||||
|  | ||||
| func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs { | ||||
| 	return map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 		ComponentSLIs: { | ||||
| 			{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 			{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 			{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 			`, | ||||
| 			expectedFeatures: []featureInfo{ | ||||
| 				{ | ||||
| 					Name:     "ComponentSLIs", | ||||
| 					FullName: "ComponentSLIs", | ||||
| 					VersionedSpecs: []featureSpec{ | ||||
| 						{Default: false, PreRelease: "Alpha", Version: "1.30"}, | ||||
| 						{Default: true, PreRelease: "Beta", Version: "1.31"}, | ||||
| 						{Default: true, PreRelease: "GA", Version: "1.32", LockToDefault: true}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "error when VersionedSpecs not ordered by version", | ||||
| 			fileContent: ` | ||||
| package features | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/component-base/featuregate" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	ComponentSLIs featuregate.Feature = "ComponentSLIs" | ||||
| ) | ||||
|  | ||||
| func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs { | ||||
| 	return map[featuregate.Feature]featuregate.VersionedSpecs{ | ||||
| 		ComponentSLIs: { | ||||
| 			{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha}, | ||||
| 			{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, | ||||
| 			{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 			`, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			newFile := writeContentToTmpFile(t, "", "new_features.go", tc.fileContent) | ||||
| 			fset := token.NewFileSet() | ||||
| 			features, err := extractFeatureInfoListFromFile(fset, newFile.Name(), true) | ||||
| 			if tc.expectErr { | ||||
| 				if err == nil { | ||||
| 					t.Fatal("expect err") | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if diff := cmp.Diff(features, tc.expectedFeatures); diff != "" { | ||||
| 				t.Errorf("File contents: got=%v, want=%v, diff=%s", features, tc.expectedFeatures, diff) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func writeContentToTmpFile(t *testing.T, tmpDir, fileName, fileContent string) *os.File { | ||||
| 	if tmpDir == "" { | ||||
| 		p, err := os.MkdirTemp("", "k8s") | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		tmpDir = p | ||||
| 	} | ||||
| 	fullPath := filepath.Join(tmpDir, fileName) | ||||
| 	err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	tmpfile, err := os.Create(fullPath) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	_, err = tmpfile.WriteString(fileContent) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	err = tmpfile.Close() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	fmt.Printf("sizhangDebug: Written tmp file %s\n", tmpfile.Name()) | ||||
| 	return tmpfile | ||||
| } | ||||
|  | ||||
| func TestParseFeatureSpec(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name                string | ||||
| 		val                 ast.Expr | ||||
| 		expectedFeatureSpec featureSpec | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "spec by field name", | ||||
| 			expectedFeatureSpec: featureSpec{ | ||||
| 				Default: true, LockToDefault: true, PreRelease: "Beta", Version: "1.31", | ||||
| 			}, | ||||
| 			val: &ast.CompositeLit{ | ||||
| 				Elts: []ast.Expr{ | ||||
| 					&ast.KeyValueExpr{ | ||||
| 						Key: &ast.Ident{ | ||||
| 							Name: "Version", | ||||
| 						}, | ||||
| 						Value: &ast.CallExpr{ | ||||
| 							Fun: &ast.SelectorExpr{ | ||||
| 								X: &ast.Ident{ | ||||
| 									Name: "version", | ||||
| 								}, | ||||
| 								Sel: &ast.Ident{ | ||||
| 									Name: "MustParse", | ||||
| 								}, | ||||
| 							}, | ||||
| 							Args: []ast.Expr{ | ||||
| 								&ast.BasicLit{ | ||||
| 									Kind:  token.STRING, | ||||
| 									Value: "\"1.31\"", | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 					&ast.KeyValueExpr{ | ||||
| 						Key: &ast.Ident{ | ||||
| 							Name: "Default", | ||||
| 						}, | ||||
| 						Value: &ast.Ident{ | ||||
| 							Name: "true", | ||||
| 						}, | ||||
| 					}, | ||||
| 					&ast.KeyValueExpr{ | ||||
| 						Key: &ast.Ident{ | ||||
| 							Name: "LockToDefault", | ||||
| 						}, | ||||
| 						Value: &ast.Ident{ | ||||
| 							Name: "true", | ||||
| 						}, | ||||
| 					}, | ||||
| 					&ast.KeyValueExpr{ | ||||
| 						Key: &ast.Ident{ | ||||
| 							Name: "PreRelease", | ||||
| 						}, | ||||
| 						Value: &ast.SelectorExpr{ | ||||
| 							X: &ast.Ident{ | ||||
| 								Name: "featuregate", | ||||
| 							}, | ||||
| 							Sel: &ast.Ident{ | ||||
| 								Name: "Beta", | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			variables := map[string]ast.Expr{} | ||||
| 			spec, err := parseFeatureSpec(variables, tc.val) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(tc.expectedFeatureSpec, spec) { | ||||
| 				t.Errorf("expected: %#v, got %#v", tc.expectedFeatureSpec, spec) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										40
									
								
								test/featuregates_linter/cmd/root.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								test/featuregates_linter/cmd/root.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| /* | ||||
| Copyright 2024 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| var rootCmd = &cobra.Command{ | ||||
| 	Use:   "static-analysis", | ||||
| 	Short: "static-analysis", | ||||
| 	Long:  `static-analysis runs static analysis of go code.`, | ||||
| } | ||||
|  | ||||
| func Execute() { | ||||
| 	err := rootCmd.Execute() | ||||
| 	if err != nil { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	rootCmd.AddCommand(NewFeatureGatesCommand()) | ||||
| } | ||||
							
								
								
									
										135
									
								
								test/featuregates_linter/cmd/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								test/featuregates_linter/cmd/util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| /* | ||||
| Copyright 2024 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"go/ast" | ||||
| 	"go/token" | ||||
| 	"os/exec" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// env configs | ||||
| 	GOOS string = findGOOS() | ||||
| ) | ||||
|  | ||||
| func findGOOS() string { | ||||
| 	goCmd := exec.Command("go", "env", "GOOS") | ||||
| 	out, err := goCmd.CombinedOutput() | ||||
| 	if err != nil { | ||||
| 		panic(fmt.Sprintf("running `go env` failed: %v\n\n%s", err, string(out))) | ||||
| 	} | ||||
| 	if len(out) == 0 { | ||||
| 		panic("empty result from `go env GOOS`") | ||||
| 	} | ||||
| 	return string(out) | ||||
| } | ||||
|  | ||||
| // identifierName returns the name of an identifier. | ||||
| // if ignorePkg, only return the last part of the identifierName. | ||||
| func identifierName(v ast.Expr, ignorePkg bool) string { | ||||
| 	if id, ok := v.(*ast.Ident); ok { | ||||
| 		return id.Name | ||||
| 	} | ||||
| 	if se, ok := v.(*ast.SelectorExpr); ok { | ||||
| 		if ignorePkg { | ||||
| 			return identifierName(se.Sel, ignorePkg) | ||||
| 		} | ||||
| 		return identifierName(se.X, ignorePkg) + "." + identifierName(se.Sel, ignorePkg) | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| // importAliasMap returns the mapping from pkg path to import alias. | ||||
| func importAliasMap(imports []*ast.ImportSpec) map[string]string { | ||||
| 	m := map[string]string{} | ||||
| 	for _, im := range imports { | ||||
| 		var importAlias string | ||||
| 		if im.Name == nil { | ||||
| 			pathSegments := strings.Split(im.Path.Value, "/") | ||||
| 			importAlias = strings.Trim(pathSegments[len(pathSegments)-1], "\"") | ||||
| 		} else { | ||||
| 			importAlias = im.Name.String() | ||||
| 		} | ||||
| 		m[im.Path.Value] = importAlias | ||||
| 	} | ||||
| 	return m | ||||
| } | ||||
|  | ||||
| func basicStringLiteral(v ast.Expr) (string, error) { | ||||
| 	bl, ok := v.(*ast.BasicLit) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("cannot parse a non BasicLit to basicStringLiteral") | ||||
| 	} | ||||
|  | ||||
| 	if bl.Kind != token.STRING { | ||||
| 		return "", fmt.Errorf("cannot parse a non STRING token to basicStringLiteral") | ||||
| 	} | ||||
| 	return strings.Trim(bl.Value, `"`), nil | ||||
| } | ||||
|  | ||||
| func basicIntLiteral(v ast.Expr) (int64, error) { | ||||
| 	bl, ok := v.(*ast.BasicLit) | ||||
| 	if !ok { | ||||
| 		return 0, fmt.Errorf("cannot parse a non BasicLit to basicIntLiteral") | ||||
| 	} | ||||
|  | ||||
| 	if bl.Kind != token.INT { | ||||
| 		return 0, fmt.Errorf("cannot parse a non INT token to basicIntLiteral") | ||||
| 	} | ||||
| 	value, err := strconv.ParseInt(bl.Value, 10, 64) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return value, nil | ||||
| } | ||||
|  | ||||
| func parseBool(variables map[string]ast.Expr, v ast.Expr) (bool, error) { | ||||
| 	ident := identifierName(v, false) | ||||
| 	switch ident { | ||||
| 	case "true": | ||||
| 		return true, nil | ||||
| 	case "false": | ||||
| 		return false, nil | ||||
| 	default: | ||||
| 		if varVal, ok := variables[ident]; ok { | ||||
| 			return parseBool(variables, varVal) | ||||
| 		} | ||||
| 		return false, fmt.Errorf("cannot parse %s into bool", ident) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func globalVariableDeclarations(tree *ast.File) map[string]ast.Expr { | ||||
| 	consts := make(map[string]ast.Expr) | ||||
| 	for _, d := range tree.Decls { | ||||
| 		if gd, ok := d.(*ast.GenDecl); ok && (gd.Tok == token.CONST || gd.Tok == token.VAR) { | ||||
| 			for _, spec := range gd.Specs { | ||||
| 				if vspec, ok := spec.(*ast.ValueSpec); ok { | ||||
| 					for _, name := range vspec.Names { | ||||
| 						for _, value := range vspec.Values { | ||||
| 							consts[name.Name] = value | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return consts | ||||
| } | ||||
							
								
								
									
										23
									
								
								test/featuregates_linter/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								test/featuregates_linter/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| /* | ||||
| Copyright 2024 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package main | ||||
|  | ||||
| import "k8s.io/kubernetes/test/featuregates_linter/cmd" | ||||
|  | ||||
| func main() { | ||||
| 	cmd.Execute() | ||||
| } | ||||
							
								
								
									
										9
									
								
								test/featuregates_linter/test_data/OWNERS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/featuregates_linter/test_data/OWNERS
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # See the OWNERS docs at https://go.k8s.io/owners | ||||
|  | ||||
| # Changing feature lifecycle requires feature-approvers approval | ||||
| options: | ||||
|   no_parent_owners: true | ||||
| approvers: | ||||
|   - feature-approvers | ||||
| labels: | ||||
|   - area/feature-gates | ||||
							
								
								
									
										972
									
								
								test/featuregates_linter/test_data/unversioned_feature_list.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										972
									
								
								test/featuregates_linter/test_data/unversioned_feature_list.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,972 @@ | ||||
| - name: AdmissionWebhookMatchConditions | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: AggregatedDiscoveryEndpoint | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: AllowDNSOnlyNodeCSR | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Deprecated | ||||
|     version: "" | ||||
| - name: AllowInsecureKubeletCertificateSigningRequests | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Deprecated | ||||
|     version: "" | ||||
| - name: AllowServiceLBStatusOnNonLB | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Deprecated | ||||
|     version: "" | ||||
| - name: AnonymousAuthConfigurableEndpoints | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: AnyVolumeDataSource | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: APIListChunking | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: APIResponseCompression | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: APIServerIdentity | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: APIServerTracing | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: APIServingWithRoutine | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: AppArmor | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: AppArmorFields | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: AuthorizeNodeWithSelectors | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: AuthorizeWithSelectors | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CloudControllerManagerWebhook | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CloudDualStackNodeIPs | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: ClusterTrustBundle | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: ClusterTrustBundleProjection | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: ComponentSLIs | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ConcurrentWatchObjectDecode | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ConsistentListFromCache | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ContainerCheckpoint | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ContextualLogging | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: CoordinatedLeaderElection | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CPUCFSQuotaPeriod | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CPUManager | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: CPUManagerPolicyAlphaOptions | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CPUManagerPolicyBetaOptions | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: CPUManagerPolicyOptions | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: CRDValidationRatcheting | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: CronJobsScheduledAnnotation | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: CrossNamespaceVolumeDataSource | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CSIMigrationPortworx | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: CSIVolumeHealth | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: CustomResourceFieldSelectors | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: DevicePluginCDIDevices | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: DisableAllocatorDualWrite | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: DisableCloudProviders | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: DisableKubeletCloudCredentialProviders | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: DisableNodeKubeProxyVersion | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: DRAControlPlaneController | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: DynamicResourceAllocation | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: EfficientWatchResumption | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: ElasticIndexedJob | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: EventedPLEG | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: ExecProbeTimeout | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: GracefulNodeShutdown | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: GracefulNodeShutdownBasedOnPodPriority | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: HonorPVReclaimPolicy | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: HPAContainerMetrics | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: HPAScaleToZero | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: ImageMaximumGCAge | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ImageVolume | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: InPlacePodVerticalScaling | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: InTreePluginPortworxUnregister | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: JobBackoffLimitPerIndex | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: JobManagedBy | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: JobPodFailurePolicy | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: JobPodReplacementPolicy | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: JobSuccessPolicy | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: KMSv1 | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Deprecated | ||||
|     version: "" | ||||
| - name: KMSv2 | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: KMSv2KDF | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: KubeletCgroupDriverFromCRI | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: KubeletInUserNamespace | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: KubeletPodResourcesDynamicResources | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: KubeletPodResourcesGet | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: KubeletSeparateDiskGC | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: KubeletTracing | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: KubeProxyDrainingTerminatingNodes | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: LegacyServiceAccountTokenCleanUp | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: LoadBalancerIPMode | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: LocalStorageCapacityIsolationFSQuotaMonitoring | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: LogarithmicScaleDown | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: LoggingAlphaOptions | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: LoggingBetaOptions | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: MatchLabelKeysInPodAffinity | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: MatchLabelKeysInPodTopologySpread | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: MaxUnavailableStatefulSet | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: MemoryManager | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: MemoryQoS | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: MinDomainsInPodTopologySpread | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: MultiCIDRServiceAllocator | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: MutatingAdmissionPolicy | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: NewVolumeManagerReconstruction | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: NFTablesProxyMode | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: NodeInclusionPolicyInPodTopologySpread | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: NodeLogQuery | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: NodeOutOfServiceVolumeDetach | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: NodeSwap | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: OpenAPIEnums | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: PDBUnhealthyPodEvictionPolicy | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: PersistentVolumeLastPhaseTransitionTime | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: PodAndContainerStatsFromCRI | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: PodDeletionCost | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: PodDisruptionConditions | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: PodHostIPs | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: PodIndexLabel | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: PodLifecycleSleepAction | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: PodReadyToStartContainersCondition | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: PodSchedulingReadiness | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: PortForwardWebsockets | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ProcMountType | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: QOSReserved | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: RecoverVolumeExpansionFailure | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: RecursiveReadOnlyMounts | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: RelaxedEnvironmentVariableValidation | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: ReloadKubeletServerCertificateFile | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: RemainingItemCount | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: ResilientWatchCacheInitialization | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ResourceHealthStatus | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: RetryGenerateName | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: RotateKubeletServerCertificate | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: RuntimeClassInImageCriAPI | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: SchedulerQueueingHints | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: SELinuxMount | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: SELinuxMountReadWriteOncePod | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: SeparateCacheWatchRPC | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: SeparateTaintEvictionController | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ServerSideApply | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: ServerSideFieldValidation | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: ServiceAccountTokenJTI | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ServiceAccountTokenNodeBinding | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ServiceAccountTokenNodeBindingValidation | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ServiceAccountTokenPodNodeInfo | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ServiceTrafficDistribution | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: SidecarContainers | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: SizeMemoryBackedVolumes | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: StableLoadBalancerNodeSet | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: StatefulSetAutoDeletePVC | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: StatefulSetStartOrdinal | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: StorageNamespaceIndex | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: StorageVersionAPI | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: StorageVersionHash | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: StorageVersionMigrator | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: StrictCostEnforcementForVAP | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: StrictCostEnforcementForWebhooks | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: StructuredAuthenticationConfiguration | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: StructuredAuthorizationConfiguration | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: SupplementalGroupsPolicy | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: TopologyAwareHints | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: TopologyManagerPolicyAlphaOptions | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: TopologyManagerPolicyBetaOptions | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: TopologyManagerPolicyOptions | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: TranslateStreamCloseWebsocketRequests | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: UnauthenticatedHTTP2DOSMitigation | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: UnknownVersionInteroperabilityProxy | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: UserNamespacesPodSecurityStandards | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: UserNamespacesSupport | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ValidatingAdmissionPolicy | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: VolumeAttributesClass | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: VolumeCapacityPriority | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: WatchBookmark | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| - name: WatchCacheInitializationPostStartHook | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: WatchFromStorageWithoutResourceVersion | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: WatchList | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: WindowsHostNetwork | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: WinDSR | ||||
|   versionedSpecs: | ||||
|   - default: false | ||||
|     lockToDefault: false | ||||
|     preRelease: Alpha | ||||
|     version: "" | ||||
| - name: WinOverlay | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: false | ||||
|     preRelease: Beta | ||||
|     version: "" | ||||
| - name: ZeroLimitedNominalConcurrencyShares | ||||
|   versionedSpecs: | ||||
|   - default: true | ||||
|     lockToDefault: true | ||||
|     preRelease: GA | ||||
|     version: "" | ||||
| @@ -0,0 +1 @@ | ||||
| [] | ||||
		Reference in New Issue
	
	Block a user
	 Siyuan Zhang
					Siyuan Zhang