From 35488ef5c7212a3d491b86e02b1ba05dbbc4b894 Mon Sep 17 00:00:00 2001 From: Siyuan Zhang Date: Fri, 19 Apr 2024 15:25:54 -0700 Subject: [PATCH] Verify: add static analysis to verify new feature gates are added as versioned feature specs. Signed-off-by: Siyuan Zhang --- hack/make-rules/verify.sh | 1 + hack/update-featuregates.sh | 30 + hack/verify-featuregates.sh | 31 + test/featuregates_linter/OWNERS | 14 + test/featuregates_linter/README.md | 8 + test/featuregates_linter/cmd/feature_gates.go | 477 +++++++++ .../cmd/feature_gates_test.go | 985 ++++++++++++++++++ test/featuregates_linter/cmd/root.go | 40 + test/featuregates_linter/cmd/util.go | 135 +++ test/featuregates_linter/main.go | 23 + test/featuregates_linter/test_data/OWNERS | 9 + .../test_data/unversioned_feature_list.yaml | 972 +++++++++++++++++ .../test_data/versioned_feature_list.yaml | 1 + 13 files changed, 2726 insertions(+) create mode 100755 hack/update-featuregates.sh create mode 100755 hack/verify-featuregates.sh create mode 100644 test/featuregates_linter/OWNERS create mode 100644 test/featuregates_linter/README.md create mode 100644 test/featuregates_linter/cmd/feature_gates.go create mode 100644 test/featuregates_linter/cmd/feature_gates_test.go create mode 100644 test/featuregates_linter/cmd/root.go create mode 100644 test/featuregates_linter/cmd/util.go create mode 100644 test/featuregates_linter/main.go create mode 100644 test/featuregates_linter/test_data/OWNERS create mode 100644 test/featuregates_linter/test_data/unversioned_feature_list.yaml create mode 100644 test/featuregates_linter/test_data/versioned_feature_list.yaml diff --git a/hack/make-rules/verify.sh b/hack/make-rules/verify.sh index 8862454af02..c6e2894e74b 100755 --- a/hack/make-rules/verify.sh +++ b/hack/make-rules/verify.sh @@ -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" diff --git a/hack/update-featuregates.sh b/hack/update-featuregates.sh new file mode 100755 index 00000000000..7e72b575806 --- /dev/null +++ b/hack/update-featuregates.sh @@ -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 diff --git a/hack/verify-featuregates.sh b/hack/verify-featuregates.sh new file mode 100755 index 00000000000..6aaf748ad1e --- /dev/null +++ b/hack/verify-featuregates.sh @@ -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 diff --git a/test/featuregates_linter/OWNERS b/test/featuregates_linter/OWNERS new file mode 100644 index 00000000000..53eefff488c --- /dev/null +++ b/test/featuregates_linter/OWNERS @@ -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 diff --git a/test/featuregates_linter/README.md b/test/featuregates_linter/README.md new file mode 100644 index 00000000000..8e1af3b0ba6 --- /dev/null +++ b/test/featuregates_linter/README.md @@ -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}" +``` diff --git a/test/featuregates_linter/cmd/feature_gates.go b/test/featuregates_linter/cmd/feature_gates.go new file mode 100644 index 00000000000..5442dc5b88d --- /dev/null +++ b/test/featuregates_linter/cmd/feature_gates.go @@ -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 ", + 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") + } +} diff --git a/test/featuregates_linter/cmd/feature_gates_test.go b/test/featuregates_linter/cmd/feature_gates_test.go new file mode 100644 index 00000000000..1ebb61672c3 --- /dev/null +++ b/test/featuregates_linter/cmd/feature_gates_test.go @@ -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) + } + }) + } +} diff --git a/test/featuregates_linter/cmd/root.go b/test/featuregates_linter/cmd/root.go new file mode 100644 index 00000000000..38ddd97dfa3 --- /dev/null +++ b/test/featuregates_linter/cmd/root.go @@ -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()) +} diff --git a/test/featuregates_linter/cmd/util.go b/test/featuregates_linter/cmd/util.go new file mode 100644 index 00000000000..059ef80dc32 --- /dev/null +++ b/test/featuregates_linter/cmd/util.go @@ -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 +} diff --git a/test/featuregates_linter/main.go b/test/featuregates_linter/main.go new file mode 100644 index 00000000000..f6b79cbe303 --- /dev/null +++ b/test/featuregates_linter/main.go @@ -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() +} diff --git a/test/featuregates_linter/test_data/OWNERS b/test/featuregates_linter/test_data/OWNERS new file mode 100644 index 00000000000..d6b721f280c --- /dev/null +++ b/test/featuregates_linter/test_data/OWNERS @@ -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 diff --git a/test/featuregates_linter/test_data/unversioned_feature_list.yaml b/test/featuregates_linter/test_data/unversioned_feature_list.yaml new file mode 100644 index 00000000000..f36232b3b72 --- /dev/null +++ b/test/featuregates_linter/test_data/unversioned_feature_list.yaml @@ -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: "" diff --git a/test/featuregates_linter/test_data/versioned_feature_list.yaml b/test/featuregates_linter/test_data/versioned_feature_list.yaml new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/test/featuregates_linter/test_data/versioned_feature_list.yaml @@ -0,0 +1 @@ +[]