mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 13:37:30 +00:00
feat: Refactors featuregate lifecycle management script
- rename featuregate_linter to compatibility_lifecycle - add feature removal verify to follow N+3 rule - remove unversioned related operation - rename yaml folder name to "reference"
This commit is contained in:
parent
a4739df381
commit
088daf472b
@ -14,8 +14,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# This script updates test/featuregates_linter/test_data/unversioned_feature_list.yaml and
|
# This script updates test/compatibility_lifecycle/reference/versioned_feature_list.yaml
|
||||||
# test/featuregates_linter/test_data/versioned_feature_list.yaml with all the feature gate features.
|
# with all the feature gate features.
|
||||||
# Usage: `hack/update-featuregates.sh`.
|
# Usage: `hack/update-featuregates.sh`.
|
||||||
|
|
||||||
set -o errexit
|
set -o errexit
|
||||||
@ -27,4 +27,4 @@ source "${KUBE_ROOT}/hack/lib/init.sh"
|
|||||||
|
|
||||||
cd "${KUBE_ROOT}"
|
cd "${KUBE_ROOT}"
|
||||||
|
|
||||||
go run test/featuregates_linter/main.go feature-gates update
|
go run test/compatibility_lifecycle/main.go feature-gates update
|
||||||
|
@ -14,8 +14,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# This script checks test/featuregates_linter/test_data/unversioned_feature_list.yaml and
|
# This script checks test/compatibility_lifecycle/reference/versioned_feature_list.yaml
|
||||||
# test/featuregates_linter/test_data/versioned_feature_list.yaml are up to date with all the feature gate features.
|
# are up to date with all the feature gate features, and verifies no feature is removed before 3 versions post `lockedToDefault:true`.
|
||||||
# We should run `hack/update-featuregates.sh` if the list is out of date.
|
# We should run `hack/update-featuregates.sh` if the list is out of date.
|
||||||
# Usage: `hack/verify-featuregates.sh`.
|
# Usage: `hack/verify-featuregates.sh`.
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ kube::golang::setup_env
|
|||||||
|
|
||||||
cd "${KUBE_ROOT}"
|
cd "${KUBE_ROOT}"
|
||||||
|
|
||||||
if ! go run test/featuregates_linter/main.go feature-gates verify; then
|
if ! go run test/compatibility_lifecycle/main.go feature-gates verify; then
|
||||||
echo "Please run 'hack/update-featuregates.sh' to update the feature list."
|
echo "Please run 'hack/update-featuregates.sh' to update the feature list."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
10
test/compatibility_lifecycle/README.md
Normal file
10
test/compatibility_lifecycle/README.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
This directory contains commands for [compatibility lifecycle verification](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/4330-compatibility-versions/README.md)
|
||||||
|
|
||||||
|
Currently, the following commands are implemented:
|
||||||
|
```
|
||||||
|
# Verify feature gate list is up to date
|
||||||
|
go run test/compatibility_lifecycle/main.go feature-gates verify
|
||||||
|
|
||||||
|
# Update feature gate list
|
||||||
|
go run test/compatibility_lifecycle/main.go feature-gates update
|
||||||
|
```
|
@ -31,18 +31,25 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/version"
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
|
baseversion "k8s.io/component-base/version"
|
||||||
yaml "sigs.k8s.io/yaml/goyaml.v2"
|
yaml "sigs.k8s.io/yaml/goyaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
alphabeticalOrder bool
|
alphabeticalOrder bool
|
||||||
k8RootPath string
|
k8RootPath string
|
||||||
unversionedFeatureListFile = "test/featuregates_linter/test_data/unversioned_feature_list.yaml"
|
versionedFeatureListFile = "test/compatibility_lifecycle/reference/versioned_feature_list.yaml"
|
||||||
versionedFeatureListFile = "test/featuregates_linter/test_data/versioned_feature_list.yaml"
|
// thresholdVersion is the version after which we require emulation support for feature removal
|
||||||
|
// 1.31 is when we introduced emulation version support
|
||||||
|
thresholdVersion = version.MajorMinor(1, 31)
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
featureGatePkg = "\"k8s.io/component-base/featuregate\""
|
featureGatePkg = "\"k8s.io/component-base/featuregate\""
|
||||||
|
generatedFileWarning = `# This file is generated by compatibility_lifecycle tool.
|
||||||
|
# Do not edit manually. Run hack/update-featuregates.sh to regenerate.
|
||||||
|
|
||||||
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
type featureSpec struct {
|
type featureSpec struct {
|
||||||
@ -95,39 +102,33 @@ func NewUpdateFeatureListCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verifyFeatureListFunc(cmd *cobra.Command, args []string) {
|
func verifyFeatureListFunc(cmd *cobra.Command, args []string) {
|
||||||
if err := verifyOrUpdateFeatureList(k8RootPath, unversionedFeatureListFile, false, false); err != nil {
|
currentVersion := version.MustParse(baseversion.DefaultKubeBinaryVersion)
|
||||||
fmt.Fprintf(os.Stderr, "Failed to verify feature list: \n%s", err)
|
if err := verifyOrUpdateFeatureList(k8RootPath, versionedFeatureListFile, currentVersion, false); err != nil {
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := verifyOrUpdateFeatureList(k8RootPath, versionedFeatureListFile, false, true); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to verify versioned feature list: \n%s", err)
|
fmt.Fprintf(os.Stderr, "Failed to verify versioned feature list: \n%s", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateFeatureListFunc(cmd *cobra.Command, args []string) {
|
func updateFeatureListFunc(cmd *cobra.Command, args []string) {
|
||||||
if err := verifyOrUpdateFeatureList(k8RootPath, unversionedFeatureListFile, true, false); err != nil {
|
currentVersion := version.MustParse(baseversion.DefaultKubeBinaryVersion)
|
||||||
fmt.Fprintf(os.Stderr, "Failed to update feature list: \n%s", err)
|
if err := verifyOrUpdateFeatureList(k8RootPath, versionedFeatureListFile, currentVersion, true); err != nil {
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := verifyOrUpdateFeatureList(k8RootPath, versionedFeatureListFile, true, true); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to update versioned feature list: \n%s", err)
|
fmt.Fprintf(os.Stderr, "Failed to update versioned feature list: \n%s", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyOrUpdateFeatureList walks all the files under pkg/ and staging/ to find the list of all the features in
|
// 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.
|
// map[featuregate.Feature]featuregate.VersionedSpecs.
|
||||||
// It will then update the feature list in featureListFile, or verifies there is no change from the existing list.
|
// 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 {
|
func verifyOrUpdateFeatureList(rootPath, featureListFile string, currentVersion *version.Version, update bool) error {
|
||||||
featureList := []featureInfo{}
|
featureList := []featureInfo{}
|
||||||
features, err := searchPathForFeatures(filepath.Join(rootPath, "pkg"), versioned)
|
features, err := searchPathForFeatures(filepath.Join(rootPath, "pkg"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
featureList = append(featureList, features...)
|
featureList = append(featureList, features...)
|
||||||
|
|
||||||
features, err = searchPathForFeatures(filepath.Join(rootPath, "staging"), versioned)
|
features, err = searchPathForFeatures(filepath.Join(rootPath, "staging"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -153,29 +154,22 @@ func verifyOrUpdateFeatureList(rootPath, featureListFile string, update, version
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// only feature deletion is allowed for unversioned features.
|
if err := verifyFeatureRemoval(featureList, baseFeatureList, currentVersion, thresholdVersion); err != nil {
|
||||||
// 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
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
featureListBytes, err := yaml.Marshal(featureList)
|
featureListBytes, err := yaml.Marshal(featureList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
featureListBytes = []byte(generatedFileWarning + string(featureListBytes))
|
||||||
if update {
|
if update {
|
||||||
return os.WriteFile(filePath, featureListBytes, 0644)
|
return os.WriteFile(filePath, featureListBytes, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
if diff := cmp.Diff(featureListBytes, baseFeatureListBytes); diff != "" {
|
if diff := cmp.Diff(featureListBytes, baseFeatureListBytes); diff != "" {
|
||||||
if versioned {
|
|
||||||
return fmt.Errorf("detected diff in versioned feature list (%s), diff: \n%s", versionedFeatureListFile, diff)
|
return fmt.Errorf("detected diff in versioned feature list (%s), diff: \n%s", versionedFeatureListFile, diff)
|
||||||
} else {
|
|
||||||
return fmt.Errorf("detected diff in unversioned feature list (%s), diff: \n%s", unversionedFeatureListFile, diff)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -205,27 +199,70 @@ func dedupeFeatureList(featureList []featureInfo) ([]featureInfo, error) {
|
|||||||
return deduped, nil
|
return deduped, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyFeatureDeletionOnly(newFeatureList []featureInfo, oldFeatureList []featureInfo) error {
|
// verifyFeatureRemoval checks if removed features are allowed to be removed based on their lifecycle.
|
||||||
oldFeatureSet := make(map[string]*featureInfo)
|
// Alpha features can be removed anytime without error.
|
||||||
for _, f := range oldFeatureList {
|
// Returns error if:
|
||||||
oldFeatureSet[f.Name] = &f
|
// - Beta features are removed (not allowed)
|
||||||
|
// - GA/Deprecated features are removed without being locked to default
|
||||||
|
// - GA/Deprecated features locked after v1.31 are removed before 3 minor versions
|
||||||
|
// have passed (required for emulation support)
|
||||||
|
func verifyFeatureRemoval(featureList []featureInfo, baseFeatureList []featureInfo,
|
||||||
|
currentVersion *version.Version, thresholdVersion *version.Version) error {
|
||||||
|
if thresholdVersion == nil {
|
||||||
|
thresholdVersion = version.MajorMinor(0, 0)
|
||||||
}
|
}
|
||||||
newFeatures := []string{}
|
baseFeatures := make(map[string]featureInfo)
|
||||||
for _, f := range newFeatureList {
|
for _, f := range baseFeatureList {
|
||||||
oldSpecs, found := oldFeatureSet[f.Name]
|
baseFeatures[f.Name] = f
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
currentFeatures := make(map[string]featureInfo)
|
||||||
|
for _, f := range featureList {
|
||||||
|
currentFeatures[f.Name] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, baseFeature := range baseFeatures {
|
||||||
|
// Check if feature was removed
|
||||||
|
if _, found := currentFeatures[name]; found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature was removed, check if allowed
|
||||||
|
specs := baseFeature.VersionedSpecs
|
||||||
|
if len(specs) == 0 {
|
||||||
|
return fmt.Errorf("feature %s has no version specs", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSpec := specs[len(specs)-1]
|
||||||
|
switch lastSpec.PreRelease {
|
||||||
|
case "Alpha":
|
||||||
|
continue // can remove alpha features anytime
|
||||||
|
case "Beta":
|
||||||
|
return fmt.Errorf("feature %s cannot be removed while in beta", name)
|
||||||
|
case "GA", "Deprecated":
|
||||||
|
if !lastSpec.LockToDefault {
|
||||||
|
return fmt.Errorf("feature %s cannot be removed because it is in GA or Deprecated state and is not locked to default", name)
|
||||||
|
}
|
||||||
|
specVer, err := version.Parse(lastSpec.Version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid version \"%s\" for feature %s: %w", lastSpec.Version, name, err)
|
||||||
|
}
|
||||||
|
// we do not require the 3 version retention for features locked before the thresholdVersion.
|
||||||
|
// TODO: remove after 1.34
|
||||||
|
if !specVer.GreaterThan(thresholdVersion) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
minRemovalVer := specVer.AddMinor(3)
|
||||||
|
if currentVersion.LessThan(minRemovalVer) {
|
||||||
|
return fmt.Errorf("feature %s cannot be removed until version %s (required for emulation support)",
|
||||||
|
name, minRemovalVer)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
|
func searchPathForFeatures(path string) ([]featureInfo, error) {
|
||||||
allFeatures := []featureInfo{}
|
allFeatures := []featureInfo{}
|
||||||
// Create a FileSet to work with
|
// Create a FileSet to work with
|
||||||
fset := token.NewFileSet()
|
fset := token.NewFileSet()
|
||||||
@ -239,7 +276,12 @@ func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
|
|||||||
if strings.HasSuffix(path, "_test.go") {
|
if strings.HasSuffix(path, "_test.go") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
features, parseErr := extractFeatureInfoListFromFile(fset, path, versioned)
|
// exclude generated files
|
||||||
|
base := filepath.Base(path)
|
||||||
|
if strings.HasPrefix(base, "zz_generated") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
features, parseErr := extractFeatureInfoListFromFile(fset, path)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return parseErr
|
return parseErr
|
||||||
}
|
}
|
||||||
@ -249,9 +291,9 @@ func searchPathForFeatures(path string, versioned bool) ([]featureInfo, error) {
|
|||||||
return allFeatures, err
|
return allFeatures, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractFeatureInfoListFromFile extracts info all the the features from
|
// extractFeatureInfoListFromFile extracts info of all the features from
|
||||||
// map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate.VersionedSpecs from the given file.
|
// map[featuregate.Feature]featuregate.VersionedSpecs in the given file.
|
||||||
func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versioned bool) (allFeatures []featureInfo, err error) {
|
func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string) (allFeatures []featureInfo, err error) {
|
||||||
// Parse the file and create an AST
|
// Parse the file and create an AST
|
||||||
absFilePath, err := filepath.Abs(filePath)
|
absFilePath, err := filepath.Abs(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -274,7 +316,7 @@ func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versio
|
|||||||
if vspec, ok := spec.(*ast.ValueSpec); ok {
|
if vspec, ok := spec.(*ast.ValueSpec); ok {
|
||||||
for _, name := range vspec.Names {
|
for _, name := range vspec.Names {
|
||||||
for _, value := range vspec.Values {
|
for _, value := range vspec.Values {
|
||||||
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables, versioned)
|
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return allFeatures, err
|
return allFeatures, err
|
||||||
}
|
}
|
||||||
@ -291,7 +333,7 @@ func extractFeatureInfoListFromFile(fset *token.FileSet, filePath string, versio
|
|||||||
for _, stmt := range fd.Body.List {
|
for _, stmt := range fd.Body.List {
|
||||||
if st, ok := stmt.(*ast.ReturnStmt); ok {
|
if st, ok := stmt.(*ast.ReturnStmt); ok {
|
||||||
for _, value := range st.Results {
|
for _, value := range st.Results {
|
||||||
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables, versioned)
|
features, err := extractFeatureInfoList(filePath, value, aliasMap, variables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return allFeatures, err
|
return allFeatures, err
|
||||||
}
|
}
|
||||||
@ -332,8 +374,8 @@ func verifyAlphabeticOrder(keys []string, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractFeatureInfoList extracts the info all the the features from
|
// extractFeatureInfoList extracts the info all the the features from
|
||||||
// map[featuregate.Feature]featuregate.FeatureSpec or map[featuregate.Feature]featuregate.VersionedSpecs.
|
// 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) {
|
func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]string, variables map[string]ast.Expr) ([]featureInfo, error) {
|
||||||
keys := []string{}
|
keys := []string{}
|
||||||
features := []featureInfo{}
|
features := []featureInfo{}
|
||||||
cl, ok := v.(*ast.CompositeLit)
|
cl, ok := v.(*ast.CompositeLit)
|
||||||
@ -344,7 +386,7 @@ func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]str
|
|||||||
if !ok {
|
if !ok {
|
||||||
return features, nil
|
return features, nil
|
||||||
}
|
}
|
||||||
if !isFeatureSpecType(mt.Value, aliasMap, versioned) {
|
if !isFeatureSpecType(mt.Value, aliasMap) {
|
||||||
return features, nil
|
return features, nil
|
||||||
}
|
}
|
||||||
for _, elt := range cl.Elts {
|
for _, elt := range cl.Elts {
|
||||||
@ -352,7 +394,7 @@ func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]str
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info, err := parseFeatureInfo(variables, kv, versioned)
|
info, err := parseFeatureInfo(variables, kv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return features, err
|
return features, err
|
||||||
}
|
}
|
||||||
@ -368,11 +410,8 @@ func extractFeatureInfoList(filePath string, v ast.Expr, aliasMap map[string]str
|
|||||||
return features, nil
|
return features, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isFeatureSpecType(v ast.Expr, aliasMap map[string]string, versioned bool) bool {
|
func isFeatureSpecType(v ast.Expr, aliasMap map[string]string) bool {
|
||||||
typeName := "FeatureSpec"
|
typeName := "VersionedSpecs"
|
||||||
if versioned {
|
|
||||||
typeName = "VersionedSpecs"
|
|
||||||
}
|
|
||||||
alias, ok := aliasMap[featureGatePkg]
|
alias, ok := aliasMap[featureGatePkg]
|
||||||
if ok {
|
if ok {
|
||||||
typeName = alias + "." + typeName
|
typeName = alias + "." + typeName
|
||||||
@ -380,20 +419,16 @@ func isFeatureSpecType(v ast.Expr, aliasMap map[string]string, versioned bool) b
|
|||||||
return identifierName(v, false) == typeName
|
return identifierName(v, false) == typeName
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFeatureInfo(variables map[string]ast.Expr, kv *ast.KeyValueExpr, versioned bool) (featureInfo, error) {
|
func parseFeatureInfo(variables map[string]ast.Expr, kv *ast.KeyValueExpr) (featureInfo, error) {
|
||||||
info := featureInfo{
|
info := featureInfo{
|
||||||
Name: identifierName(kv.Key, true),
|
Name: identifierName(kv.Key, true),
|
||||||
FullName: identifierName(kv.Key, false),
|
FullName: identifierName(kv.Key, false),
|
||||||
VersionedSpecs: []featureSpec{},
|
VersionedSpecs: []featureSpec{},
|
||||||
}
|
}
|
||||||
specExps := []ast.Expr{}
|
specExps := []ast.Expr{}
|
||||||
if versioned {
|
|
||||||
if cl, ok := kv.Value.(*ast.CompositeLit); ok {
|
if cl, ok := kv.Value.(*ast.CompositeLit); ok {
|
||||||
specExps = append(specExps, cl.Elts...)
|
specExps = append(specExps, cl.Elts...)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
specExps = append(specExps, kv.Value)
|
|
||||||
}
|
|
||||||
for _, specExp := range specExps {
|
for _, specExp := range specExps {
|
||||||
spec, err := parseFeatureSpec(variables, specExp)
|
spec, err := parseFeatureSpec(variables, specExp)
|
||||||
if err != nil {
|
if err != nil {
|
@ -17,18 +17,26 @@ limitations under the License.
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/token"
|
"go/token"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const expectedHeader = `# This file is generated by compatibility_lifecycle tool.
|
||||||
|
# Do not edit manually. Run hack/update-featuregates.sh to regenerate.
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
var testCurrentVersion = version.MustParse("1.32")
|
||||||
|
|
||||||
func TestVerifyAlphabeticOrder(t *testing.T) {
|
func TestVerifyAlphabeticOrder(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -87,210 +95,8 @@ func TestVerifyAlphabeticOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
featureListFileContent 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},
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
featureListFileContent: featureListFileContent,
|
|
||||||
updatedFeatureListFileContent: featureListFileContent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "semantically equivalent, formatting wrong",
|
|
||||||
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},
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
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: ""
|
|
||||||
`,
|
|
||||||
updatedFeatureListFileContent: featureListFileContent,
|
|
||||||
expectVerifyErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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},
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
featureListFileContent: featureListFileContent,
|
|
||||||
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},
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
featureListFileContent: featureListFileContent,
|
|
||||||
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},
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
featureListFileContent: featureListFileContent,
|
|
||||||
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},
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
featureListFileContent: featureListFileContent,
|
|
||||||
expectVerifyErr: true,
|
|
||||||
expectUpdateErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
featureListFile := writeContentToTmpFile(t, "", "feature_list.yaml", tc.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("expectUpdateErr=%v, got err: %s", tc.expectUpdateErr, 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) {
|
func TestVerifyOrUpdateFeatureListVersioned(t *testing.T) {
|
||||||
featureListFileContent := `- name: APIListChunking
|
featureListFileContent := expectedHeader + `- name: APIListChunking
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: true
|
- default: true
|
||||||
lockToDefault: true
|
lockToDefault: true
|
||||||
@ -378,7 +184,7 @@ var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
featureListFileContent: `- name: APIListChunking
|
featureListFileContent: expectedHeader + `- name: APIListChunking
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: true
|
- default: true
|
||||||
lockToDefault: true
|
lockToDefault: true
|
||||||
@ -488,7 +294,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
`,
|
`,
|
||||||
expectVerifyErr: true,
|
expectVerifyErr: true,
|
||||||
featureListFileContent: featureListFileContent,
|
featureListFileContent: featureListFileContent,
|
||||||
updatedFeatureListFileContent: `- name: APIListChunking
|
updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: true
|
- default: true
|
||||||
lockToDefault: true
|
lockToDefault: true
|
||||||
@ -519,7 +325,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "remove feature",
|
name: "not allowed to remove feature",
|
||||||
goFileContent: `
|
goFileContent: `
|
||||||
package features
|
package features
|
||||||
|
|
||||||
@ -538,8 +344,9 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectVerifyErr: true,
|
expectVerifyErr: true,
|
||||||
|
expectUpdateErr: true,
|
||||||
featureListFileContent: featureListFileContent,
|
featureListFileContent: featureListFileContent,
|
||||||
updatedFeatureListFileContent: `- name: APIListChunking
|
updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: true
|
- default: true
|
||||||
lockToDefault: true
|
lockToDefault: true
|
||||||
@ -582,7 +389,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
`,
|
`,
|
||||||
expectVerifyErr: true,
|
expectVerifyErr: true,
|
||||||
featureListFileContent: featureListFileContent,
|
featureListFileContent: featureListFileContent,
|
||||||
updatedFeatureListFileContent: `- name: APIListChunking
|
updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: true
|
- default: true
|
||||||
lockToDefault: true
|
lockToDefault: true
|
||||||
@ -616,13 +423,13 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
featureListFile := writeContentToTmpFile(t, "", "feature_list.yaml", tc.featureListFileContent)
|
featureListFile := writeContentToTmpFile(t, "", "feature_list.yaml", tc.featureListFileContent)
|
||||||
tmpDir := filepath.Dir(featureListFile.Name())
|
tmpDir := filepath.Dir(featureListFile.Name())
|
||||||
_ = writeContentToTmpFile(t, tmpDir, "pkg/new_features.go", tc.goFileContent)
|
_ = writeContentToTmpFile(t, tmpDir, "pkg/new_features.go", tc.goFileContent)
|
||||||
err := verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), false, true)
|
err := verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), testCurrentVersion, false)
|
||||||
if tc.expectVerifyErr != (err != nil) {
|
if tc.expectVerifyErr != (err != nil) {
|
||||||
t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err)
|
t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err)
|
||||||
}
|
}
|
||||||
err = verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), true, true)
|
err = verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), testCurrentVersion, true)
|
||||||
if tc.expectUpdateErr != (err != nil) {
|
if tc.expectUpdateErr != (err != nil) {
|
||||||
t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err)
|
t.Errorf("expectUpdateErr=%v, got err: %s", tc.expectUpdateErr, err)
|
||||||
}
|
}
|
||||||
if tc.expectUpdateErr {
|
if tc.expectUpdateErr {
|
||||||
return
|
return
|
||||||
@ -638,162 +445,6 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func TestExtractFeatureInfoListFromFileVersioned(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -961,7 +612,7 @@ func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
newFile := writeContentToTmpFile(t, "", "new_features.go", tc.fileContent)
|
newFile := writeContentToTmpFile(t, "", "new_features.go", tc.fileContent)
|
||||||
fset := token.NewFileSet()
|
fset := token.NewFileSet()
|
||||||
features, err := extractFeatureInfoListFromFile(fset, newFile.Name(), true)
|
features, err := extractFeatureInfoListFromFile(fset, newFile.Name())
|
||||||
if tc.expectErr {
|
if tc.expectErr {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expect err")
|
t.Fatal("expect err")
|
||||||
@ -1003,7 +654,6 @@ func writeContentToTmpFile(t *testing.T, tmpDir, fileName, fileContent string) *
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Printf("sizhangDebug: Written tmp file %s\n", tmpfile.Name())
|
|
||||||
return tmpfile
|
return tmpfile
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1087,3 +737,160 @@ func TestParseFeatureSpec(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func TestVerifyFeatureRemoval(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
featureList []featureInfo
|
||||||
|
baseFeatureList []featureInfo
|
||||||
|
currentVersion *version.Version
|
||||||
|
thresholdVersion *version.Version
|
||||||
|
expectErr bool
|
||||||
|
expectedErrMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no features removed",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.1"),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alpha feature removed",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.1"),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "beta feature removed",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.1"),
|
||||||
|
expectErr: true,
|
||||||
|
expectedErrMsg: "feature FeatureB cannot be removed while in beta",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GA feature removed before allowed version",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.2"),
|
||||||
|
expectErr: true,
|
||||||
|
expectedErrMsg: "feature FeatureC cannot be removed until version 1.3 (required for emulation support)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GA feature removed after allowed version",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.4"),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature with no version specs",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureD", VersionedSpecs: []featureSpec{}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.1"),
|
||||||
|
expectErr: true,
|
||||||
|
expectedErrMsg: "feature FeatureD has no version specs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature with invalid version",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureE", VersionedSpecs: []featureSpec{{Version: "invalid", PreRelease: "GA", LockToDefault: true}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.1"),
|
||||||
|
expectErr: true,
|
||||||
|
expectedErrMsg: "invalid version \"invalid\" for feature FeatureE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GA feature not locked to default",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA"}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.4"),
|
||||||
|
expectErr: true,
|
||||||
|
expectedErrMsg: "feature FeatureC cannot be removed because it is in GA or Deprecated state and is not locked to default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Deprecated feature not locked to default",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureD", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Deprecated"}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.4"),
|
||||||
|
expectErr: true,
|
||||||
|
expectedErrMsg: "feature FeatureD cannot be removed because it is in GA or Deprecated state and is not locked to default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GA feature removed at threshold version",
|
||||||
|
featureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
},
|
||||||
|
baseFeatureList: []featureInfo{
|
||||||
|
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
|
||||||
|
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.4", PreRelease: "GA", LockToDefault: true}}},
|
||||||
|
},
|
||||||
|
currentVersion: version.MustParse("1.5"),
|
||||||
|
thresholdVersion: version.MustParse("1.4"),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := verifyFeatureRemoval(tc.featureList, tc.baseFeatureList, tc.currentVersion, tc.thresholdVersion)
|
||||||
|
if tc.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.expectedErrMsg) {
|
||||||
|
t.Fatalf("expected error message to contain %q, got %q", tc.expectedErrMsg, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -23,9 +23,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "static-analysis",
|
Use: "compatibility-lifecycle",
|
||||||
Short: "static-analysis",
|
Short: "compatibility-lifecycle",
|
||||||
Long: `static-analysis runs static analysis of go code.`,
|
Long: `compatibility-lifecycle runs verification for compatibility lifecycle.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "k8s.io/kubernetes/test/featuregates_linter/cmd"
|
import "k8s.io/kubernetes/test/compatibility_lifecycle/cmd"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd.Execute()
|
cmd.Execute()
|
@ -1,3 +1,6 @@
|
|||||||
|
# This file is generated by compatibility_lifecycle tool.
|
||||||
|
# Do not edit manually. Run hack/update-featuregates.sh to regenerate.
|
||||||
|
|
||||||
- name: AllowDNSOnlyNodeCSR
|
- name: AllowDNSOnlyNodeCSR
|
||||||
versionedSpecs:
|
versionedSpecs:
|
||||||
- default: true
|
- default: true
|
@ -1,8 +0,0 @@
|
|||||||
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}"
|
|
||||||
```
|
|
Loading…
Reference in New Issue
Block a user