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