Feat: Cue 0.14.1 Preupgrade CLI Check (#6983)

* Feat: Cue 0.14.1 Preupgrade CLI Check

Signed-off-by: Brian Kane <briankane1@gmail.com>

* Feat: Add Support for Repeat Operations

Signed-off-by: Brian Kane <briankane1@gmail.com>

---------

Signed-off-by: Brian Kane <briankane1@gmail.com>
This commit is contained in:
Brian Kane
2026-03-19 04:05:24 +00:00
committed by GitHub
parent 21640b55cd
commit e5779ec9ec
6 changed files with 1237 additions and 1 deletions

124
pkg/cue/upgrade/README.md Normal file
View File

@@ -0,0 +1,124 @@
# CUE Upgrade System
This package provides an extensible system for upgrading CUE templates to handle version compatibility as KubeVela evolves.
## Architecture
- **`upgrade.go`**: Core interface, registry, and main `Upgrade()` function
- **`upgrade_1_11.go`**: KubeVela 1.11+ specific upgrades (list concatenation)
- **`upgrade_test.go`**: Comprehensive test suite
- **Future**: `upgrade_1_12.go`, `upgrade_1_13.go`, etc.
## Usage
```go
import "github.com/oam-dev/kubevela/pkg/cue/upgrade"
// Upgrade using current KubeVela CLI version (auto-detected)
result, err := upgrade.Upgrade(cueTemplate)
if err != nil {
// If version detection fails, you'll get a helpful error message
// suggesting to use the --target-version flag
log.Fatal(err)
}
// Upgrade to specific KubeVela version
result, err := upgrade.Upgrade(cueTemplate, "1.11")
// Future: upgrade to newer versions
result, err := upgrade.Upgrade(cueTemplate, "1.12")
```
## Adding New Version Upgrades
To add support for KubeVela 1.12 (example):
1. **Create** `upgrade_1_12.go`:
```go
package upgrade
import "fmt"
func init() {
// Register your new upgrade functions
RegisterUpgrade("1.12", upgradeSomeNewFeature)
RegisterUpgrade("1.12", upgradeAnotherFeature)
}
// upgradeSomeNewFeature handles a breaking change in CUE 0.15
func upgradeSomeNewFeature(cueStr string) (string, error) {
// Your upgrade logic here
// Parse, transform, format, return
return transformedCue, nil
}
```
2. **Update** `upgrade.go` `supportedVersions`:
```go
// In the Upgrade() function
supportedVersions := []string{"1.11", "1.12"}
```
3. **Add tests** to `upgrade_test.go` for the new functionality
4. **Done!** The system automatically applies all relevant upgrades
## Current Upgrades
### 1.11 - List Concatenation
- **Problem**: `list1 + list2` syntax deprecated in underlying CUE version
- **Solution**: Converts to `list.Concat([list1, list2])`
- **Auto-imports**: Adds `import "list"` when needed
## Features
- **Version-aware**: Only applies upgrades up to target version
- **Composable**: Multiple upgrades can be registered per version
- **Safe**: Falls back gracefully on errors
- **Extensible**: Easy to add new version support
- **Well-tested**: Comprehensive test coverage
## Examples
### Before (Old CUE):
```cue
myList1: [1, 2, 3]
myList2: [4, 5, 6]
combined: myList1 + myList2
```
### After (CUE 0.14+):
```cue
import "list"
myList1: [1, 2, 3]
myList2: [4, 5, 6]
combined: list.Concat([myList1, myList2])
```
This system ensures KubeVela templates remain compatible as CUE continues to evolve.
## CLI Usage
The upgrade functionality is available through the KubeVela CLI:
```bash
# Upgrade using current KubeVela CLI version (auto-detected)
vela def upgrade my-definition.cue
# Save upgraded definition to a file
vela def upgrade my-definition.cue -o upgraded-definition.cue
# Upgrade for specific KubeVela version
vela def upgrade my-definition.cue --target-version=v1.11
# Also works with just the version number
vela def upgrade my-definition.cue --target-version=1.11
```
**Version Detection:**
- When no `--target-version` is specified, the system automatically detects the current KubeVela CLI version
- If version detection fails (e.g., in development builds), you'll get a helpful error message suggesting to use `--target-version=1.11`
- **Note:** Use `--target-version=` (with equals sign) for the version flag

136
pkg/cue/upgrade/upgrade.go Normal file
View File

@@ -0,0 +1,136 @@
/*
Copyright 2024 The KubeVela 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 upgrade
import (
"fmt"
"regexp"
"strings"
"github.com/oam-dev/kubevela/version"
)
// UpgradeFunc represents a function that upgrades CUE code for version compatibility
type UpgradeFunc func(string) (string, error)
// upgradeRegistry holds upgrade functions for different CUE versions
var upgradeRegistry = make(map[string][]UpgradeFunc)
// RegisterUpgrade registers an upgrade function for a specific KubeVela version
// version should be in format "1.11", "1.12", etc.
func RegisterUpgrade(version string, upgradeFunc UpgradeFunc) {
upgradeRegistry[version] = append(upgradeRegistry[version], upgradeFunc)
}
// getCurrentKubeVelaMinorVersion extracts the minor version (e.g., "1.11") from the full KubeVela version
func getCurrentKubeVelaMinorVersion() (string, error) {
versionStr := version.VelaVersion
if versionStr == "" || versionStr == "UNKNOWN" {
return "", fmt.Errorf("unable to determine KubeVela version (got %q). Please specify the target version explicitly using --target-version=1.11", versionStr)
}
// Remove 'v' prefix if present
versionStr = strings.TrimPrefix(versionStr, "v")
// Use regex to extract major.minor version (e.g., "1.11.2" -> "1.11")
re := regexp.MustCompile(`^(\d+\.\d+)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) >= 2 {
return matches[1], nil
}
return "", fmt.Errorf("unable to parse KubeVela version %q. Please specify the target version explicitly using --target-version=1.11", versionStr)
}
// Upgrade applies all registered upgrades for KubeVela versions up to and including the target version
// targetVersion should be in format "1.11", "1.12", etc.
// If targetVersion is empty, applies upgrades for the current KubeVela CLI version
func Upgrade(cueStr string, targetVersion ...string) (string, error) {
var version string
var err error
if len(targetVersion) > 0 && targetVersion[0] != "" {
version = targetVersion[0]
} else {
version, err = getCurrentKubeVelaMinorVersion() // Default to current CLI version
if err != nil {
return "", err
}
}
result := cueStr
// Apply upgrades for all versions up to and including the target version
// Currently we only support 1.11, but this can be extended
supportedVersions := []string{"1.11"}
for _, v := range supportedVersions {
if shouldApplyUpgrade(v, version) {
if upgrades, exists := upgradeRegistry[v]; exists {
for _, upgrade := range upgrades {
result, err = upgrade(result)
if err != nil {
return cueStr, fmt.Errorf("failed to apply upgrade for version %s: %w", v, err)
}
}
}
}
}
return result, nil
}
// shouldApplyUpgrade determines if upgrades for a given version should be applied
// based on the target version
func shouldApplyUpgrade(upgradeVersion, targetVersion string) bool {
// For now, simple string comparison works since we only have 1.11
// In the future, this could be enhanced with proper semantic version comparison
return upgradeVersion <= targetVersion
}
// GetSupportedVersions returns a list of supported upgrade versions
func GetSupportedVersions() []string {
versions := make([]string, 0, len(upgradeRegistry))
for version := range upgradeRegistry {
versions = append(versions, version)
}
return versions
}
// RequiresUpgrade checks if the CUE string requires upgrading to the target version
// Returns: (needsUpgrade bool, reasons []string, error)
// If targetVersion is empty, uses the current KubeVela CLI version
func RequiresUpgrade(cueStr string, targetVersion ...string) (bool, []string, error) {
var version string
var err error
if len(targetVersion) > 0 && targetVersion[0] != "" {
version = targetVersion[0]
} else {
version, err = getCurrentKubeVelaMinorVersion()
if err != nil {
return false, nil, err
}
}
// For now, we only check for v1.11 upgrades
// In the future, this can check against multiple version requirements
if version >= "1.11" {
return requires111Upgrade(cueStr)
}
return false, nil, nil
}

View File

@@ -0,0 +1,353 @@
/*
Copyright 2024 The KubeVela 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 upgrade
import (
"fmt"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/ast/astutil"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/parser"
"cuelang.org/go/cue/token"
)
func init() {
RegisterUpgrade("1.11", upgradeListConcatenation)
}
func requires111Upgrade(cueStr string) (bool, []string, error) {
file, err := parser.ParseFile("", cueStr, parser.ParseComments)
if err != nil {
return false, nil, fmt.Errorf("failed to parse CUE: %w", err)
}
var reasons []string
if hasOldListConcatenation(file) {
reasons = append(reasons, "contains deprecated list operators (+ or *) that need upgrading to list.Concat() or list.Repeat()")
}
return len(reasons) > 0, reasons, nil
}
func hasOldListConcatenation(file *ast.File) bool {
listRegistry := collectListDeclarations(file)
found := false
astutil.Apply(file, func(cursor astutil.Cursor) bool {
if binExpr, ok := cursor.Node().(*ast.BinaryExpr); ok {
if binExpr.Op.String() == "+" {
if isListExpression(binExpr.X, listRegistry) && isListExpression(binExpr.Y, listRegistry) {
found = true
return false
}
}
if binExpr.Op.String() == "*" {
if (isListExpression(binExpr.X, listRegistry) && isNumericExpression(binExpr.Y, listRegistry)) ||
(isNumericExpression(binExpr.X, listRegistry) && isListExpression(binExpr.Y, listRegistry)) {
found = true
return false
}
}
}
return true
}, nil)
return found
}
// upgradeListConcatenation handles:
// - list1 + list2 -> list.Concat([list1, list2])
// - list * n -> list.Repeat(list, n)
// - n * list -> list.Repeat(list, n)
func upgradeListConcatenation(cueStr string) (string, error) {
file, err := parser.ParseFile("", cueStr, parser.ParseComments)
if err != nil {
return "", fmt.Errorf("failed to parse CUE: %w", err)
}
transformed := upgradeListConcatenationAST(file)
result, err := format.Node(transformed)
if err != nil {
return "", fmt.Errorf("failed to format CUE: %w", err)
}
return string(result), nil
}
func upgradeListConcatenationAST(file *ast.File) *ast.File {
listRegistry := collectListDeclarations(file)
needsListImport := false
result := astutil.Apply(file, func(cursor astutil.Cursor) bool {
if binExpr, ok := cursor.Node().(*ast.BinaryExpr); ok {
if binExpr.Op.String() == "+" {
if isListExpression(binExpr.X, listRegistry) && isListExpression(binExpr.Y, listRegistry) {
callExpr := &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "list"},
Sel: &ast.Ident{Name: "Concat"},
},
Args: []ast.Expr{
&ast.ListLit{
Elts: []ast.Expr{binExpr.X, binExpr.Y},
},
},
}
cursor.Replace(callExpr)
needsListImport = true
}
}
if binExpr.Op.String() == "*" {
var listExpr, countExpr ast.Expr
if isListExpression(binExpr.X, listRegistry) && isNumericExpression(binExpr.Y, listRegistry) {
listExpr = binExpr.X
countExpr = binExpr.Y
} else if isNumericExpression(binExpr.X, listRegistry) && isListExpression(binExpr.Y, listRegistry) {
countExpr = binExpr.X
listExpr = binExpr.Y
}
if listExpr != nil && countExpr != nil {
ast.SetRelPos(listExpr, 0)
ast.SetRelPos(countExpr, 0)
callExpr := &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "list"},
Sel: &ast.Ident{Name: "Repeat"},
},
Args: []ast.Expr{listExpr, countExpr},
}
cursor.Replace(callExpr)
needsListImport = true
}
}
}
return true
}, nil)
if file, ok := result.(*ast.File); ok && needsListImport {
ensureListImport(file)
return file
}
return file
}
func ensureListImport(file *ast.File) {
for _, imp := range file.Imports {
if imp.Path != nil && imp.Path.Value == "\"list\"" {
return
}
}
for _, decl := range file.Decls {
if importDecl, ok := decl.(*ast.ImportDecl); ok {
for _, spec := range importDecl.Specs {
if spec.Path != nil && spec.Path.Value == "\"list\"" {
return
}
}
}
}
if file.Imports != nil || len(file.Decls) > 0 {
listImport := &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: "\"list\"",
},
}
file.Imports = append([]*ast.ImportSpec{listImport}, file.Imports...)
importDecl := &ast.ImportDecl{
Specs: []*ast.ImportSpec{listImport},
}
file.Decls = append([]ast.Decl{importDecl}, file.Decls...)
}
}
func collectListDeclarations(file *ast.File) map[string]bool {
listRegistry := make(map[string]bool)
astutil.Apply(file, func(cursor astutil.Cursor) bool {
if node, ok := cursor.Node().(*ast.Field); ok {
if label, ok := node.Label.(*ast.Ident); ok {
if isListLiteral(node.Value) {
listRegistry[label.Name] = true
} else if structLit, ok := node.Value.(*ast.StructLit); ok {
prefix := label.Name
collectNestedListDeclarationsFirstPass(structLit, prefix, listRegistry)
}
}
}
return true
}, nil)
// Second pass: iteratively collect fields that are results of list operations
changed := true
for changed {
changed = false
astutil.Apply(file, func(cursor astutil.Cursor) bool {
if node, ok := cursor.Node().(*ast.Field); ok {
if label, ok := node.Label.(*ast.Ident); ok {
if !listRegistry[label.Name] && isListOperationResult(node.Value, listRegistry) {
listRegistry[label.Name] = true
changed = true
} else if structLit, ok := node.Value.(*ast.StructLit); ok {
prefix := label.Name
if collectNestedListDeclarationsSecondPass(structLit, prefix, listRegistry) {
changed = true
}
}
}
}
return true
}, nil)
}
return listRegistry
}
func collectNestedListDeclarationsFirstPass(structLit *ast.StructLit, prefix string, listRegistry map[string]bool) {
for _, elt := range structLit.Elts {
if field, ok := elt.(*ast.Field); ok {
if label, ok := field.Label.(*ast.Ident); ok {
qualifiedName := prefix + "." + label.Name
if isListLiteral(field.Value) {
listRegistry[qualifiedName] = true
listRegistry[label.Name] = true
} else if nestedStruct, ok := field.Value.(*ast.StructLit); ok {
collectNestedListDeclarationsFirstPass(nestedStruct, qualifiedName, listRegistry)
}
}
}
}
}
func collectNestedListDeclarationsSecondPass(structLit *ast.StructLit, prefix string, listRegistry map[string]bool) bool {
changed := false
for _, elt := range structLit.Elts {
if field, ok := elt.(*ast.Field); ok {
if label, ok := field.Label.(*ast.Ident); ok {
qualifiedName := prefix + "." + label.Name
if !listRegistry[qualifiedName] && isListOperationResult(field.Value, listRegistry) {
listRegistry[qualifiedName] = true
listRegistry[label.Name] = true
changed = true
} else if nestedStruct, ok := field.Value.(*ast.StructLit); ok {
if collectNestedListDeclarationsSecondPass(nestedStruct, qualifiedName, listRegistry) {
changed = true
}
}
}
}
}
return changed
}
func isListLiteral(expr ast.Expr) bool {
switch e := expr.(type) {
case *ast.ListLit:
return true
case *ast.Comprehension:
return true
case *ast.Ellipsis:
return true
case *ast.CallExpr:
if sel, ok := e.Fun.(*ast.SelectorExpr); ok {
if id, ok := sel.X.(*ast.Ident); ok && id.Name == "list" {
return true
}
}
return false
}
return false
}
func isListExpression(expr ast.Expr, listRegistry map[string]bool) bool {
switch e := expr.(type) {
case *ast.ListLit:
return true
case *ast.CallExpr:
if sel, ok := e.Fun.(*ast.SelectorExpr); ok {
if id, ok := sel.X.(*ast.Ident); ok && id.Name == "list" {
return true
}
}
return false
case *ast.Ident:
return listRegistry[e.Name]
case *ast.SelectorExpr:
if base, ok := e.X.(*ast.Ident); ok {
if sel, ok := e.Sel.(*ast.Ident); ok {
qualifiedName := base.Name + "." + sel.Name
return listRegistry[qualifiedName]
}
}
return false
}
return false
}
// isNumericExpression checks if an expression is a numeric literal or identifier that is not a known list.
func isNumericExpression(expr ast.Expr, listRegistry map[string]bool) bool {
switch e := expr.(type) {
case *ast.BasicLit:
return e.Kind == token.INT || e.Kind == token.FLOAT
case *ast.Ident:
return !listRegistry[e.Name]
case *ast.UnaryExpr:
return isNumericExpression(e.X, listRegistry)
}
return false
}
// isListOperationResult checks if an expression is the result of a list operation
func isListOperationResult(expr ast.Expr, listRegistry map[string]bool) bool {
if binExpr, ok := expr.(*ast.BinaryExpr); ok {
if binExpr.Op.String() == "+" {
return isListExpression(binExpr.X, listRegistry) && isListExpression(binExpr.Y, listRegistry)
}
if binExpr.Op.String() == "*" {
return (isListExpression(binExpr.X, listRegistry) && isNumericExpression(binExpr.Y, listRegistry)) ||
(isNumericExpression(binExpr.X, listRegistry) && isListExpression(binExpr.Y, listRegistry))
}
}
if callExpr, ok := expr.(*ast.CallExpr); ok {
if sel, ok := callExpr.Fun.(*ast.SelectorExpr); ok {
if id, ok := sel.X.(*ast.Ident); ok && id.Name == "list" {
if selName, ok := sel.Sel.(*ast.Ident); ok {
return selName.Name == "Concat" || selName.Name == "Repeat"
}
}
}
}
return false
}

View File

@@ -0,0 +1,509 @@
/*
Copyright 2024 The KubeVela 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 upgrade
import (
"strings"
"testing"
"github.com/oam-dev/kubevela/version"
)
func TestUpgrade(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "simple list concatenation",
input: `
myList1: [1, 2, 3]
myList2: [4, 5, 6]
combined: myList1 + myList2
`,
expected: "list.Concat",
wantErr: false,
},
{
name: "list concatenation in object",
input: `
object: {
items: baseItems + extraItems
baseItems: ["a", "b"]
extraItems: ["c", "d"]
}
`,
expected: "list.Concat",
wantErr: false,
},
{
name: "non-list addition should not be transformed",
input: `
number1: 5
number2: 10
sum: number1 + number2
`,
expected: "number1 + number2", // Should remain as is
wantErr: false,
},
{
name: "mixed with existing imports",
input: `
import "strings"
myList1: [1, 2, 3]
myList2: [4, 5, 6]
combined: myList1 + myList2
`,
expected: "list.Concat",
wantErr: false,
},
{
name: "simple list repeat",
input: `
myList: ["a", "b"]
repeated: myList * 3
`,
expected: "list.Repeat",
wantErr: false,
},
{
name: "reverse list repeat",
input: `
myList: ["x", "y", "z"]
repeated: 2 * myList
`,
expected: "list.Repeat",
wantErr: false,
},
{
name: "list repeat with field references",
input: `
parameter: {
items: ["item1", "item2"]
count: 5
repeated1: items * 2
repeated2: 3 * items
}
`,
expected: "list.Repeat",
wantErr: false,
},
{
name: "mixed concatenation and repeat",
input: `
list1: ["a", "b"]
list2: ["c", "d"]
concatenated: list1 + list2
repeated: concatenated * 2
`,
expected: "list.Concat",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Upgrade(tt.input, "1.11") // Explicitly provide version for tests
if (err != nil) != tt.wantErr {
t.Errorf("Upgrade() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
// Check if the expected transformation occurred
if tt.expected == "list.Concat" || tt.expected == "list.Repeat" {
if !strings.Contains(got, tt.expected) {
t.Errorf("Upgrade() did not transform to %s, got = %v", tt.expected, got)
}
if !strings.Contains(got, `import "list"`) {
t.Errorf("Upgrade() did not add list import, got = %v", got)
}
} else {
// Check that the expected string is still present (not transformed)
if !strings.Contains(got, tt.expected) {
t.Errorf("Upgrade() unexpectedly transformed non-list operation, got = %v", got)
}
}
})
}
}
func TestUpgradeWithComplexTemplate(t *testing.T) {
// Test with a template similar to what would be used in a workload definition
input := `
template: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: app: context.name
template: {
metadata: labels: app: context.name
spec: {
containers: [{
name: context.name
image: parameter.image
env: parameter.env + [{name: "EXTRA", value: "value"}]
}]
}
}
}
}
parameter: {
image: string
env: [...{name: string, value: string}]
}
output: template
`
got, err := Upgrade(input, "1.11") // Explicitly provide version for tests
if err != nil {
t.Fatalf("Upgrade() error = %v", err)
}
// Check that env concatenation was transformed
if !strings.Contains(got, "list.Concat") {
t.Errorf("Upgrade() did not transform env list concatenation")
}
// Check that list import was added
if !strings.Contains(got, `import "list"`) {
t.Errorf("Upgrade() did not add list import")
}
t.Logf("Transformed template:\n%s", got)
}
func TestUpgradeWithStringsJoin(t *testing.T) {
// Test the specific case from test-component-lists.cue
input := `
import "strings"
template: {
output: {
spec: {
selector: matchLabels: "app.oam.dev/component": parameter.name
template: {
metadata: labels: "app.oam.dev/component": parameter.name
spec: containers: [{
name: parameter.name
image: parameter.image
}]
}
}
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: strings.Join(parameter.list1 + parameter.list2, "-")
}
}
outputs: {}
parameter: {
list1: [...string]
list2: [...string]
name: string
image: string
}
}
`
got, err := Upgrade(input, "1.11") // Explicitly provide version for tests
if err != nil {
t.Fatalf("Upgrade() error = %v", err)
}
// Check that the concatenation inside strings.Join was transformed
// list.Concat takes a list of lists as a single argument
if !strings.Contains(got, "strings.Join(list.Concat([") {
t.Errorf("Upgrade() did not transform list concatenation inside strings.Join")
t.Logf("Got:\n%s", got)
}
// Check that list import was added
if !strings.Contains(got, `import "list"`) {
t.Errorf("Upgrade() did not add list import")
}
// The original strings import should still be there
if !strings.Contains(got, `import "strings"`) {
t.Errorf("Upgrade() removed the strings import")
}
t.Logf("Transformed template:\n%s", got)
}
func TestUpgradeRegistry(t *testing.T) {
// Test that the registry system works and can handle version-specific upgrades
// Test with explicit version (should apply 1.11 upgrades)
input := `
list1: [1, 2, 3]
list2: [4, 5, 6]
result: list1 + list2
`
result, err := Upgrade(input, "1.11") // Provide explicit version for test
if err != nil {
t.Fatalf("Upgrade() error = %v", err)
}
if !strings.Contains(result, "list.Concat") {
t.Errorf("Default upgrade should apply 1.11 list concatenation upgrade, got = %v", result)
}
// Test explicit version 1.11
result, err = Upgrade(input, "1.11")
if err != nil {
t.Fatalf("Upgrade() error = %v", err)
}
if !strings.Contains(result, "list.Concat") {
t.Errorf("1.11 upgrade should apply list concatenation upgrade, got = %v", result)
}
// Test future version (should still apply 1.11 upgrades)
result, err = Upgrade(input, "1.12")
if err != nil {
t.Fatalf("Upgrade() error = %v", err)
}
if !strings.Contains(result, "list.Concat") {
t.Errorf("Future version upgrade should still apply 1.11 upgrades, got = %v", result)
}
}
func TestGetSupportedVersions(t *testing.T) {
versions := GetSupportedVersions()
if len(versions) == 0 {
t.Error("Expected at least one supported version")
}
// Should include 1.11 since that's registered in init()
found := false
for _, v := range versions {
if v == "1.11" {
found = true
break
}
}
if !found {
t.Errorf("Expected 1.11 to be in supported versions, got %v", versions)
}
}
func TestGetCurrentKubeVelaMinorVersion(t *testing.T) {
tests := []struct {
name string
mockVersion string
expectedVersion string
expectError bool
}{
{
name: "full semantic version",
mockVersion: "v1.11.2",
expectedVersion: "1.11",
expectError: false,
},
{
name: "full semantic version without v prefix",
mockVersion: "1.12.0",
expectedVersion: "1.12",
expectError: false,
},
{
name: "dev version",
mockVersion: "v1.13.0-alpha.1+dev",
expectedVersion: "1.13",
expectError: false,
},
{
name: "unknown version error",
mockVersion: "UNKNOWN",
expectError: true,
},
{
name: "empty version error",
mockVersion: "",
expectError: true,
},
{
name: "invalid version error",
mockVersion: "invalid-version",
expectError: true,
},
}
// Save original version
originalVersion := version.VelaVersion
defer func() {
version.VelaVersion = originalVersion
}()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock the version
version.VelaVersion = tt.mockVersion
got, err := getCurrentKubeVelaMinorVersion()
if tt.expectError {
if err == nil {
t.Errorf("getCurrentKubeVelaMinorVersion() expected error but got none")
}
return
}
if err != nil {
t.Errorf("getCurrentKubeVelaMinorVersion() unexpected error: %v", err)
return
}
if got != tt.expectedVersion {
t.Errorf("getCurrentKubeVelaMinorVersion() = %v, want %v", got, tt.expectedVersion)
}
})
}
}
func TestRequiresUpgrade(t *testing.T) {
tests := []struct {
name string
input string
shouldRequire bool
expectReasons int
}{
{
name: "requires upgrade - list concatenation",
input: `
myList1: [1, 2, 3]
myList2: [4, 5, 6]
combined: myList1 + myList2
`,
shouldRequire: true,
expectReasons: 1,
},
{
name: "no upgrade needed - already uses list.Concat",
input: `
import "list"
myList1: [1, 2, 3]
myList2: [4, 5, 6]
combined: list.Concat([myList1, myList2])
`,
shouldRequire: false,
expectReasons: 0,
},
{
name: "no upgrade needed - numeric addition",
input: `
x: 1
y: 2
sum: x + y
`,
shouldRequire: false,
expectReasons: 0,
},
{
name: "requires upgrade - nested structure",
input: `
parameter: {
env: [...{name: string, value: string}]
extraEnv: [{name: "DEBUG", value: "true"}]
}
combined: parameter.env + parameter.extraEnv
`,
shouldRequire: true,
expectReasons: 1,
},
{
name: "requires upgrade - list repeat",
input: `
items: ["a", "b", "c"]
repeated: items * 5
`,
shouldRequire: true,
expectReasons: 1,
},
{
name: "no upgrade needed - already uses list.Repeat",
input: `
import "list"
items: ["a", "b", "c"]
repeated: list.Repeat(items, 5)
`,
shouldRequire: false,
expectReasons: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
needsUpgrade, reasons, err := RequiresUpgrade(tt.input, "1.11")
if err != nil {
t.Fatalf("RequiresUpgrade() error = %v", err)
}
if needsUpgrade != tt.shouldRequire {
t.Errorf("RequiresUpgrade() = %v, want %v", needsUpgrade, tt.shouldRequire)
}
if len(reasons) != tt.expectReasons {
t.Errorf("RequiresUpgrade() returned %d reasons, want %d. Reasons: %v",
len(reasons), tt.expectReasons, reasons)
}
})
}
}
func TestUpgradeWithUnknownVersionError(t *testing.T) {
// Save original version
originalVersion := version.VelaVersion
defer func() {
version.VelaVersion = originalVersion
}()
// Mock unknown version
version.VelaVersion = "UNKNOWN"
input := `
list1: [1, 2, 3]
list2: [4, 5, 6]
combined: list1 + list2
`
// Should return error when no version is specified and VelaVersion is UNKNOWN
_, err := Upgrade(input)
if err == nil {
t.Errorf("Upgrade() expected error when version is UNKNOWN but got none")
}
// Should contain helpful message
expectedMsg := "Please specify the target version explicitly using --target-version=1.11"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Error message should contain guidance about using --target-version flag, got: %v", err.Error())
}
// Should work when explicit version is provided
result, err := Upgrade(input, "1.11")
if err != nil {
t.Errorf("Upgrade() with explicit version should work even when VelaVersion is UNKNOWN, got error: %v", err)
}
if !strings.Contains(result, "list.Concat") {
t.Errorf("Upgrade() with explicit version should still apply transformations")
}
}

View File

@@ -401,7 +401,7 @@ func (opt *AdoptOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []
if opt.AppName != "" {
app := &v1beta1.Application{}
err := f.Client().Get(cmd.Context(), apitypes.NamespacedName{Namespace: opt.AppNamespace, Name: opt.AppName}, app)
if err == nil && app != nil {
if err == nil {
if !opt.Yes && opt.Apply {
userInput := NewUserInput()
confirm := userInput.AskBool(

View File

@@ -53,6 +53,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/cue/upgrade"
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/definition/gen_sdk"
"github.com/oam-dev/kubevela/pkg/definition/goloader"
@@ -94,6 +95,7 @@ func DefinitionCommandGroup(c common.Args, order string, ioStreams util.IOStream
NewDefinitionDelCommand(c),
NewDefinitionInitCommand(c),
NewDefinitionValidateCommand(c),
NewDefinitionUpgradeCommand(c, ioStreams),
NewDefinitionDocGenCommand(c, ioStreams),
NewCapabilityShowCommand(c, "", ioStreams),
NewDefinitionGenAPICommand(c),
@@ -1996,3 +1998,115 @@ func NewDefinitionGenDocCommand(_ common.Args, streams util.IOStreams) *cobra.Co
return cmd
}
// NewDefinitionUpgradeCommand create the `vela def upgrade` command to help user upgrade CUE templates for version compatibility
func NewDefinitionUpgradeCommand(c common.Args, ioStreams util.IOStreams) *cobra.Command {
var (
outputFile string
targetVersion string
checkOnly bool
quiet bool
)
cmd := &cobra.Command{
Use: "upgrade DEFINITION_FILE",
Short: "Upgrade CUE definition for version compatibility",
Long: "Upgrade CUE definition files to be compatible with a specific KubeVela version.\n" +
"This command automatically applies all necessary upgrades to ensure your definitions work with the target KubeVela version.\n" +
"If no version is specified, upgrades to the current CLI version.\n\n" +
"Currently supported upgrades:\n" +
"• List concatenation syntax compatibility\n" +
"• Import statement management\n" +
"• Template syntax modernization",
Example: "# Validate if definition needs upgrading (exit code 1 if upgrade needed)\n" +
"vela def upgrade my-definition.cue --validate\n\n" +
"# Silent validation for scripting (only exit code)\n" +
"vela def upgrade my-definition.cue --validate --quiet\n\n" +
"# Upgrade definition for current KubeVela version\n" +
"vela def upgrade my-definition.cue\n\n" +
"# Upgrade and save to specific file\n" +
"vela def upgrade my-definition.cue -o upgraded-definition.cue\n\n" +
"# Upgrade for specific KubeVela version\n" +
"vela def upgrade my-definition.cue --target-version=v1.11",
Annotations: map[string]string{
types.TagCommandType: types.TypeDefGeneration,
types.TagCommandOrder: "5",
},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
sourceFile := args[0]
// Read the source file
content, err := os.ReadFile(sourceFile) //nolint:gosec
if err != nil {
return fmt.Errorf("failed to read source file %s: %w", sourceFile, err)
}
// Prepare target version (strip 'v' prefix if present for consistency)
version := strings.TrimPrefix(targetVersion, "v")
// Check-only mode
if checkOnly {
var needsUpgrade bool
var reasons []string
if version != "" {
needsUpgrade, reasons, err = upgrade.RequiresUpgrade(string(content), version)
} else {
needsUpgrade, reasons, err = upgrade.RequiresUpgrade(string(content))
}
if err != nil {
return fmt.Errorf("failed to check upgrade requirements: %w", err)
}
if needsUpgrade {
if !quiet {
fmt.Fprintf(ioStreams.Out, "✗ Definition %s requires upgrade:\n", sourceFile)
for _, reason := range reasons {
fmt.Fprintf(ioStreams.Out, " - %s\n", reason)
}
}
os.Exit(1) // Non-zero exit code for scripts
} else if !quiet {
fmt.Fprintf(ioStreams.Out, "✓ Definition %s is up to date\n", sourceFile)
}
return nil
}
// Apply upgrades
var upgradedContent string
if version != "" {
upgradedContent, err = upgrade.Upgrade(string(content), version)
} else {
// Use default version (current KubeVela)
upgradedContent, err = upgrade.Upgrade(string(content))
}
if err != nil {
return fmt.Errorf("failed to upgrade CUE template: %w", err)
}
// Determine output destination
if outputFile != "" {
// Write to specified output file
if err := os.WriteFile(outputFile, []byte(upgradedContent), 0600); err != nil { //nolint:gosec
return fmt.Errorf("failed to write output file %s: %w", outputFile, err)
}
fmt.Fprintf(ioStreams.Out, "Successfully upgraded %s and saved to %s\n", sourceFile, outputFile)
} else {
// Write to stdout
fmt.Fprint(ioStreams.Out, upgradedContent)
}
return nil
},
}
cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file path. If not specified, outputs to stdout.")
cmd.Flags().StringVar(&targetVersion, "target-version", "", "Target KubeVela version (e.g., --target-version=v1.11). If not specified, uses current CLI version.")
cmd.Flags().BoolVar(&checkOnly, "validate", false, "Validate if definition needs upgrading without making changes (exit code 1 if upgrade required)")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Suppress output in validate mode, only return exit code")
return cmd
}