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:
Siyuan Zhang 2024-04-19 15:25:54 -07:00
parent dbc2b0a5c7
commit 35488ef5c7
13 changed files with 2726 additions and 0 deletions

View File

@ -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
View 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
View 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

View 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

View 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}"
```

View 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")
}
}

View 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)
}
})
}
}

View 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())
}

View 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
}

View 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()
}

View 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

View 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: ""

View File

@ -0,0 +1 @@
[]