mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 18:28: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-api-groups.sh" | ||||||
|   "verify-boilerplate.sh" |   "verify-boilerplate.sh" | ||||||
|   "verify-external-dependencies-version.sh" |   "verify-external-dependencies-version.sh" | ||||||
|  |   "verify-featuregates.sh" | ||||||
|   "verify-fieldname-docs.sh" |   "verify-fieldname-docs.sh" | ||||||
|   "verify-gofmt.sh" |   "verify-gofmt.sh" | ||||||
|   "verify-imports.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