mirror of
https://github.com/kubevela/kubevela.git
synced 2026-04-05 10:17:16 +00:00
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:
124
pkg/cue/upgrade/README.md
Normal file
124
pkg/cue/upgrade/README.md
Normal 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
136
pkg/cue/upgrade/upgrade.go
Normal 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
|
||||
}
|
||||
353
pkg/cue/upgrade/upgrade_1_11.go
Normal file
353
pkg/cue/upgrade/upgrade_1_11.go
Normal 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
|
||||
}
|
||||
509
pkg/cue/upgrade/upgrade_test.go
Normal file
509
pkg/cue/upgrade/upgrade_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user