Feat: rework vela up to support specified revision (#3634)

* Feat: rework vela up to support specified revision

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: add legacy compatibility

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Feat: fix test

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
Somefive
2022-04-13 22:20:07 +08:00
committed by GitHub
parent 68500b3f17
commit d657ea4daf
15 changed files with 716 additions and 80 deletions

116
pkg/cmd/builder.go Normal file
View File

@@ -0,0 +1,116 @@
/*
Copyright 2022 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 cmd
import (
"github.com/spf13/cobra"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/term"
)
// Builder build command with factory
type Builder struct {
cmd *cobra.Command
f Factory
}
// NamespaceFlagConfig config for namespace flag in cmd
type NamespaceFlagConfig struct {
completion bool
usage string
loadEnv bool
}
// NamespaceFlagOption the option for configuring namespace flag in cmd
type NamespaceFlagOption interface {
ApplyToNamespaceFlagOptions(*NamespaceFlagConfig)
}
func newNamespaceFlagOptions(options ...NamespaceFlagOption) NamespaceFlagConfig {
cfg := NamespaceFlagConfig{
completion: true,
usage: usageNamespace,
loadEnv: true,
}
for _, option := range options {
option.ApplyToNamespaceFlagOptions(&cfg)
}
return cfg
}
// NamespaceFlagNoCompletionOption disable auto-completion for namespace flag
type NamespaceFlagNoCompletionOption struct{}
// ApplyToNamespaceFlagOptions .
func (option NamespaceFlagNoCompletionOption) ApplyToNamespaceFlagOptions(cfg *NamespaceFlagConfig) {
cfg.completion = false
}
// NamespaceFlagUsageOption the usage description for namespace flag
type NamespaceFlagUsageOption string
// ApplyToNamespaceFlagOptions .
func (option NamespaceFlagUsageOption) ApplyToNamespaceFlagOptions(cfg *NamespaceFlagConfig) {
cfg.usage = string(option)
}
// NamespaceFlagDisableEnvOption disable loading namespace from env
type NamespaceFlagDisableEnvOption struct{}
// ApplyToNamespaceFlagOptions .
func (option NamespaceFlagDisableEnvOption) ApplyToNamespaceFlagOptions(cfg *NamespaceFlagConfig) {
cfg.loadEnv = false
}
// WithNamespaceFlag add namespace flag to the command, by default, it will also add env flag to the command
func (builder *Builder) WithNamespaceFlag(options ...NamespaceFlagOption) *Builder {
cfg := newNamespaceFlagOptions(options...)
builder.cmd.Flags().StringP(flagNamespace, "n", "", cfg.usage)
if cfg.completion {
cmdutil.CheckErr(builder.cmd.RegisterFlagCompletionFunc(
flagNamespace,
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return GetNamespacesForCompletion(cmd.Context(), builder.f, toComplete)
}))
}
if cfg.loadEnv {
return builder.WithEnvFlag()
}
return builder
}
// WithEnvFlag add env flag to the command
func (builder *Builder) WithEnvFlag() *Builder {
builder.cmd.PersistentFlags().StringP(flagEnv, "e", "", usageEnv)
return builder
}
// WithResponsiveWriter format the command outputs
func (builder *Builder) WithResponsiveWriter() *Builder {
builder.cmd.SetOut(term.NewResponsiveWriter(builder.cmd.OutOrStdout()))
return builder
}
// Build construct the command
func (builder *Builder) Build() *cobra.Command {
return builder.cmd
}
// NewCommandBuilder builder for command
func NewCommandBuilder(f Factory, cmd *cobra.Command) *Builder {
return &Builder{cmd: cmd, f: f}
}

72
pkg/cmd/completion.go Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright 2022 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 cmd
import (
"context"
"strings"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
)
func listObjectNamesForCompletion(ctx context.Context, f Factory, gvk schema.GroupVersionKind, listOptions []client.ListOption, toComplete string) ([]string, cobra.ShellCompDirective) {
uns := &unstructured.UnstructuredList{}
uns.SetGroupVersionKind(gvk)
if err := f.Client().List(ctx, uns, listOptions...); err != nil {
return nil, cobra.ShellCompDirectiveError
}
var candidates []string
for _, obj := range uns.Items {
if name := obj.GetName(); strings.HasPrefix(name, toComplete) {
candidates = append(candidates, name)
}
}
return candidates, cobra.ShellCompDirectiveNoFileComp
}
// GetNamespacesForCompletion auto-complete the namespace
func GetNamespacesForCompletion(ctx context.Context, f Factory, toComplete string) ([]string, cobra.ShellCompDirective) {
return listObjectNamesForCompletion(ctx, f, corev1.SchemeGroupVersion.WithKind("Namespace"), nil, toComplete)
}
// GetRevisionForCompletion auto-complete the revision according to the application
func GetRevisionForCompletion(ctx context.Context, f Factory, appName string, namespace string, toComplete string) ([]string, cobra.ShellCompDirective) {
var options []client.ListOption
if namespace != "" {
options = append(options, client.InNamespace(namespace))
}
if appName != "" {
options = append(options, client.MatchingLabels{oam.LabelAppName: appName})
}
return listObjectNamesForCompletion(ctx, f, v1beta1.SchemeGroupVersion.WithKind(v1beta1.ApplicationRevisionKind), options, toComplete)
}
// GetApplicationsForCompletion auto-complete application
func GetApplicationsForCompletion(ctx context.Context, f Factory, namespace string, toComplete string) ([]string, cobra.ShellCompDirective) {
var options []client.ListOption
if namespace != "" {
options = append(options, client.InNamespace(namespace))
}
return listObjectNamesForCompletion(ctx, f, v1beta1.SchemeGroupVersion.WithKind(v1beta1.ApplicationKind), options, toComplete)
}

46
pkg/cmd/factory.go Normal file
View File

@@ -0,0 +1,46 @@
/*
Copyright 2022 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 cmd
import (
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Factory client factory for running command
type Factory interface {
Client() client.Client
}
// ClientGetter function for getting client
type ClientGetter func() (client.Client, error)
type defaultFactory struct {
ClientGetter
}
// Client return the client for command line use, interrupt if error encountered
func (f *defaultFactory) Client() client.Client {
cli, err := f.ClientGetter()
cmdutil.CheckErr(err)
return cli
}
// NewDefaultFactory create a factory based on client getter function
func NewDefaultFactory(clientGetter ClientGetter) Factory {
return &defaultFactory{ClientGetter: clientGetter}
}

27
pkg/cmd/types.go Normal file
View File

@@ -0,0 +1,27 @@
/*
Copyright 2022 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 cmd
const (
flagNamespace = "namespace"
flagEnv = "env"
)
const (
usageNamespace = "If present, the namespace scope for this CLI request"
usageEnv = "The environment name for the CLI request"
)

52
pkg/cmd/utils.go Normal file
View File

@@ -0,0 +1,52 @@
/*
Copyright 2022 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 cmd
import (
"github.com/spf13/cobra"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/env"
)
// GetNamespace get namespace from command flags and env
func GetNamespace(f Factory, cmd *cobra.Command) string {
namespace, err := cmd.Flags().GetString(flagNamespace)
cmdutil.CheckErr(err)
if namespace != "" {
return namespace
}
// find namespace from env
envName, err := cmd.Flags().GetString(flagEnv)
if err != nil {
// ignore env if the command does not use the flag
return ""
}
cmdutil.CheckErr(common.SetGlobalClient(f.Client()))
var envMeta *types.EnvMeta
if envName != "" {
envMeta, err = env.GetEnvByName(envName)
} else {
envMeta, err = env.GetCurrentEnv()
}
if err != nil {
return ""
}
return envMeta.Namespace
}

View File

@@ -0,0 +1,51 @@
/*
Copyright 2021 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 utils
import (
"context"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
)
// FreezeApplication freeze application to disable the reconciling process for it
func FreezeApplication(ctx context.Context, cli client.Client, app *v1beta1.Application, mutate func()) (string, error) {
return oam.GetControllerRequirement(app), _updateApplicationWithControllerRequirement(ctx, cli, app, mutate, "Disabled")
}
// UnfreezeApplication unfreeze application to enable the reconciling process for it
func UnfreezeApplication(ctx context.Context, cli client.Client, app *v1beta1.Application, mutate func(), originalControllerRequirement string) error {
return _updateApplicationWithControllerRequirement(ctx, cli, app, mutate, originalControllerRequirement)
}
func _updateApplicationWithControllerRequirement(ctx context.Context, cli client.Client, app *v1beta1.Application, mutate func(), controllerRequirement string) error {
appKey := client.ObjectKeyFromObject(app)
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err := cli.Get(ctx, appKey, app); err != nil {
return err
}
oam.SetControllerRequirement(app, controllerRequirement)
if mutate != nil {
mutate()
}
return cli.Update(ctx, app)
})
}

View File

@@ -62,3 +62,34 @@ func GetLastAppliedTime(o client.Object) time.Time {
}
return o.GetCreationTimestamp().Time
}
// SetPublishVersion set PublishVersion for object
func SetPublishVersion(o client.Object, publishVersion string) {
annotations := o.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[AnnotationPublishVersion] = publishVersion
o.SetAnnotations(annotations)
}
// GetControllerRequirement get ControllerRequirement from object
func GetControllerRequirement(o client.Object) string {
if annotations := o.GetAnnotations(); annotations != nil {
return annotations[AnnotationControllerRequirement]
}
return ""
}
// SetControllerRequirement set ControllerRequirement for object
func SetControllerRequirement(o client.Object, controllerRequirement string) {
annotations := o.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[AnnotationControllerRequirement] = controllerRequirement
if controllerRequirement == "" {
delete(annotations, AnnotationControllerRequirement)
}
o.SetAnnotations(annotations)
}

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
@@ -442,6 +443,7 @@ func generateAddonInfo(name string, status pkgaddon.Status) string {
for c := range status.Clusters {
ic = append(ic, c)
}
sort.Strings(ic)
res += fmt.Sprintf("installedClusters: %s \n", ic)
}
return res

View File

@@ -28,6 +28,7 @@ import (
"k8s.io/klog"
"github.com/oam-dev/kubevela/apis/types"
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/helm"
"github.com/oam-dev/kubevela/pkg/utils/system"
@@ -66,6 +67,7 @@ func NewCommand() *cobra.Command {
commandArgs := common.Args{
Schema: common.Scheme,
}
f := velacmd.NewDefaultFactory(commandArgs.GetClient)
if err := system.InitDirs(); err != nil {
fmt.Println("InitDir err", err)
@@ -76,7 +78,7 @@ func NewCommand() *cobra.Command {
// Getting Start
NewEnvCommand(commandArgs, "3", ioStream),
NewInitCommand(commandArgs, "2", ioStream),
NewUpCommand(commandArgs, "1", ioStream),
NewUpCommand(f, "1"),
NewCapabilityShowCommand(commandArgs, ioStream),
// Manage Apps

View File

@@ -84,8 +84,10 @@ func NewRevisionListCommand(c common.Args) *cobra.Command {
status = "Succeeded"
case rev.Status.Workflow.Terminated || rev.Status.Workflow.Suspend || rev.Status.Workflow.Finished:
status = "Failed"
default:
case app.Status.LatestRevision != nil && app.Status.LatestRevision.Name == rev.Name:
status = "Executing"
default:
status = "Failed"
}
}
if labels := rev.GetLabels(); labels != nil {

View File

@@ -19,71 +19,272 @@ package cli
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
apitypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/pkg/component"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/oam"
utilcommon "github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/common"
)
// UpCommandOptions command args for vela up
type UpCommandOptions struct {
AppName string
Namespace string
File string
PublishVersion string
RevisionName string
}
// Complete fill the args for vela up
func (opt *UpCommandOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) {
if len(args) > 0 {
opt.AppName = args[0]
}
opt.Namespace = velacmd.GetNamespace(f, cmd)
}
// Validate if vela up args is valid, interrupt the command
func (opt *UpCommandOptions) Validate() error {
if opt.AppName != "" && opt.File != "" {
return errors.Errorf("cannot use app name and file at the same time")
}
if opt.AppName == "" && opt.File == "" {
return errors.Errorf("either app name or file should be set")
}
if opt.AppName != "" && opt.PublishVersion == "" {
return errors.Errorf("publish-version must be set if you want to force existing application to re-run")
}
if opt.AppName == "" && opt.RevisionName != "" {
return errors.Errorf("revision name must be used with application name")
}
return nil
}
// Run execute the vela up command
func (opt *UpCommandOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
if opt.File != "" {
return opt.deployApplicationFromFile(f, cmd)
}
if opt.RevisionName == "" {
return opt.deployExistingApp(f, cmd)
}
return opt.deployExistingAppUsingRevision(f, cmd)
}
func (opt *UpCommandOptions) deployExistingAppUsingRevision(f velacmd.Factory, cmd *cobra.Command) error {
ctx, cli := cmd.Context(), f.Client()
app := &v1beta1.Application{}
if err := cli.Get(ctx, apitypes.NamespacedName{Name: opt.AppName, Namespace: opt.Namespace}, app); err != nil {
return err
}
if publishVersion := oam.GetPublishVersion(app); publishVersion == opt.PublishVersion {
return errors.Errorf("current PublishVersion is %s", publishVersion)
}
// check revision
revs, err := application.GetSortedAppRevisions(ctx, cli, opt.AppName, opt.Namespace)
if err != nil {
return err
}
var matchedRev *v1beta1.ApplicationRevision
for _, rev := range revs {
if rev.Name == opt.RevisionName {
matchedRev = rev.DeepCopy()
}
}
if matchedRev == nil {
return errors.Errorf("failed to find revision %s matching application %s", opt.RevisionName, opt.AppName)
}
if app.Status.LatestRevision != nil && app.Status.LatestRevision.Name == opt.RevisionName {
return nil
}
// freeze the application
appKey := client.ObjectKeyFromObject(app)
controllerRequirement, err := utils.FreezeApplication(ctx, cli, app, func() {
app.Spec = matchedRev.Spec.Application.Spec
oam.SetPublishVersion(app, opt.PublishVersion)
})
if err != nil {
return errors.Wrapf(err, "failed to freeze application %s before update", appKey)
}
// create new revision based on the matched revision
revName, revisionNum := utils.GetAppNextRevision(app)
matchedRev.Name = revName
oam.SetPublishVersion(matchedRev, opt.PublishVersion)
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(matchedRev)
if err != nil {
return err
}
un := &unstructured.Unstructured{Object: obj}
component.ClearRefObjectForDispatch(un)
if err = cli.Create(ctx, un); err != nil {
return errors.Wrapf(err, "failed to update application %s to create new revision %s", appKey, revName)
}
// update application status to point to the new revision
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(ctx, appKey, app); err != nil {
return err
}
app.Status = apicommon.AppStatus{
LatestRevision: &apicommon.Revision{Name: revName, Revision: revisionNum, RevisionHash: matchedRev.GetLabels()[oam.LabelAppRevisionHash]},
}
return cli.Status().Update(ctx, app)
}); err != nil {
return errors.Wrapf(err, "failed to update application %s to use new revision %s", appKey, revName)
}
// unfreeze application
if err = utils.UnfreezeApplication(ctx, cli, app, nil, controllerRequirement); err != nil {
return errors.Wrapf(err, "failed to unfreeze application %s after update", appKey)
}
cmd.Printf("Application updated with new PublishVersion %s using revision %s\n", opt.PublishVersion, opt.RevisionName)
return nil
}
func (opt *UpCommandOptions) deployExistingApp(f velacmd.Factory, cmd *cobra.Command) error {
ctx, cli := cmd.Context(), f.Client()
app := &v1beta1.Application{}
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err := cli.Get(ctx, apitypes.NamespacedName{Name: opt.AppName, Namespace: opt.Namespace}, app); err != nil {
return err
}
if publishVersion := oam.GetPublishVersion(app); publishVersion == opt.PublishVersion {
return errors.Errorf("current PublishVersion is %s", publishVersion)
}
oam.SetPublishVersion(app, opt.PublishVersion)
return cli.Update(ctx, app)
}); err != nil {
return err
}
cmd.Printf("Application updated with new PublishVersion %s\n", opt.PublishVersion)
return nil
}
func (opt *UpCommandOptions) deployApplicationFromFile(f velacmd.Factory, cmd *cobra.Command) error {
cli := f.Client()
body, err := common.ReadRemoteOrLocalPath(opt.File)
if err != nil {
return err
}
ioStream := util.IOStreams{
In: cmd.InOrStdin(),
Out: cmd.OutOrStdout(),
ErrOut: cmd.ErrOrStderr(),
}
if common.IsAppfile(body) { // legacy compatibility
o := &common.AppfileOptions{Kubecli: cli, IO: ioStream, Namespace: opt.Namespace}
return o.Run(opt.File, o.Namespace, utilcommon.Args{Schema: utilcommon.Scheme})
}
var app v1beta1.Application
err = yaml.Unmarshal(body, &app)
if err != nil {
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
}
// Override namespace if namespace flag is set. We should check if namespace is `default` or not
// since GetFlagNamespaceOrEnv returns default namespace when failed to get current env.
if opt.Namespace != "" && opt.Namespace != types.DefaultAppNamespace {
app.SetNamespace(opt.Namespace)
}
if opt.PublishVersion != "" {
oam.SetPublishVersion(&app, opt.PublishVersion)
}
err = common.ApplyApplication(app, ioStream, cli)
if err != nil {
return err
}
cmd.Printf("Application %s/%s applied.\n", app.Namespace, app.Name)
return nil
}
var (
upLong = templates.LongDesc(i18n.T(`
Deploy one application
Deploy one application based on local files or re-deploy an existing application.
With the -n/--namespace flag, you can choose the location of the target application.
To apply application from file, use the -f/--file flag to specify the application
file location.
To give a particular version to this deploy, use the -v/--publish-version flag. When
you are deploying an existing application, the version name must be different from
the current name. You can also use a history revision for the deploy and override the
current application by using the -r/--revision flag.`))
upExample = templates.Examples(i18n.T(`
# Deploy an application from file
vela up -f ./app.yaml
# Deploy an application with a version name
vela up example-app -n example-ns --publish-version beta
# Deploy an application using existing revision
vela up example-app -n example-ns --publish-version beta --revision example-app-v2`))
)
// NewUpCommand will create command for applying an AppFile
func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cobra.Command {
appFilePath := new(string)
func NewUpCommand(f velacmd.Factory, order string) *cobra.Command {
o := &UpCommandOptions{}
cmd := &cobra.Command{
Use: "up",
DisableFlagsInUseLine: true,
Short: "Apply an appfile or application from file",
Long: "Create or update vela application from file or URL, both appfile or application object format are supported.",
Short: i18n.T("Deploy one application"),
Long: upLong,
Example: upExample,
Annotations: map[string]string{
types.TagCommandOrder: order,
types.TagCommandType: types.TypeStart,
},
RunE: func(cmd *cobra.Command, args []string) error {
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
if err != nil {
return err
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
o.Complete(f, cmd, args)
if o.File == "" {
return velacmd.GetApplicationsForCompletion(cmd.Context(), f, o.Namespace, toComplete)
}
kubecli, err := c.GetClient()
if err != nil {
return err
}
body, err := common.ReadRemoteOrLocalPath(*appFilePath)
if err != nil {
return err
}
if common.IsAppfile(body) {
o := &common.AppfileOptions{
Kubecli: kubecli,
IO: ioStream,
Namespace: namespace,
}
return o.Run(*appFilePath, o.Namespace, c)
}
var app corev1beta1.Application
err = yaml.Unmarshal(body, &app)
if err != nil {
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
}
// Override namespace if namespace flag is set. We should check if namespace is `default` or not
// since GetFlagNamespaceOrEnv returns default namespace when failed to get current env.
if namespace != "" && namespace != types.DefaultAppNamespace {
app.SetNamespace(namespace)
}
err = common.ApplyApplication(app, ioStream, kubecli)
if err != nil {
return err
}
return nil
return nil, cobra.ShellCompDirectiveDefault
},
Run: func(cmd *cobra.Command, args []string) {
o.Complete(f, cmd, args)
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run(f, cmd))
},
}
cmd.SetOut(ioStream.Out)
cmd.Flags().StringVarP(appFilePath, "file", "f", "", "specify file path for appfile or application, it could be a remote url.")
cmd.Flags().StringVarP(&o.File, "file", "f", o.File, "The file path for appfile or application. It could be a remote url.")
cmd.Flags().StringVarP(&o.PublishVersion, "publish-version", "v", o.PublishVersion, "The publish version for deploying application.")
cmd.Flags().StringVarP(&o.RevisionName, "revision", "r", o.RevisionName, "The revision to use for deploying the application, if empty, the current application configuration will be used.")
cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
"revision",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var appName string
if len(args) > 0 {
appName = args[0]
}
namespace := velacmd.GetNamespace(f, cmd)
return velacmd.GetRevisionForCompletion(cmd.Context(), f, appName, namespace, toComplete)
}))
addNamespaceAndEnvArg(cmd)
return cmd
return velacmd.NewCommandBuilder(f, cmd).
WithNamespaceFlag().
WithResponsiveWriter().
Build()
}

View File

@@ -31,7 +31,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/utils/util"
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/references/common"
)
@@ -128,7 +128,10 @@ spec:
}))
var buf bytes.Buffer
cmd := NewUpCommand(args, "", util.IOStreams{In: os.Stdin, Out: &buf, ErrOut: &buf})
cmd := NewUpCommand(velacmd.NewDefaultFactory(args.GetClient), "")
cmd.SetArgs([]string{})
cmd.SetOut(&buf)
cmd.SetErr(&buf)
if c.namespace != "" {
require.NoError(t, cmd.Flags().Set(FlagNamespace, c.namespace))
}

View File

@@ -22,7 +22,6 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -384,18 +383,12 @@ func rollbackApplicationWithPublishVersion(cmd *cobra.Command, cli client.Client
cmd.Printf("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n", rev.Name, publishVersion)
appKey := client.ObjectKeyFromObject(app)
var controllerRequirement string
// rollback application spec and freeze
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(ctx, appKey, app); err != nil {
return err
}
v1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationPublishVersion, publishVersion)
controllerRequirement = app.GetAnnotations()[oam.AnnotationControllerRequirement]
v1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationControllerRequirement, "Not Available")
controllerRequirement, err := utils.FreezeApplication(ctx, cli, app, func() {
app.Spec = rev.Spec.Application.Spec
return cli.Update(ctx, app)
}); err != nil {
oam.SetPublishVersion(app, publishVersion)
})
if err != nil {
return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
}
cmd.Printf("Application spec rollback successfully.\n")
@@ -435,19 +428,7 @@ func rollbackApplicationWithPublishVersion(cmd *cobra.Command, cli client.Client
}
// unfreeze application
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(ctx, appKey, app); err != nil {
return err
}
annotations := app.GetAnnotations()
if controllerRequirement != "" {
annotations[oam.AnnotationControllerRequirement] = controllerRequirement
} else {
delete(annotations, oam.AnnotationControllerRequirement)
}
app.SetAnnotations(annotations)
return cli.Update(ctx, app)
}); err != nil {
if err = utils.UnfreezeApplication(ctx, cli, app, nil, controllerRequirement); err != nil {
return errors.Wrapf(err, "failed to resume application to restart")
}
cmd.Printf("Application rollback completed.\n")

View File

@@ -209,9 +209,11 @@ var _ = Describe("Test velaQL rest api", func() {
req := apiv1.ApplicationRequest{
Components: appWithHelm.Spec.Components,
}
res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), req)
Expect(res).ShouldNot(BeNil())
Expect(res.StatusCode).Should(Equal(200))
Eventually(func(g Gomega) {
res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), req)
g.Expect(res).ShouldNot(BeNil())
g.Expect(res.StatusCode).Should(Equal(200))
}, 1*time.Minute).Should(Succeed())
newApp := new(v1beta1.Application)
Eventually(func() error {

View File

@@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils"
. "github.com/onsi/ginkgo"
@@ -447,5 +448,52 @@ var _ = Describe("Test multicluster scenario", func() {
g.Expect(k8sClient.Get(workerCtx, types.NamespacedName{Name: "test-busybox", Namespace: prodNamespace}, &appsv1.Deployment{})).Should(Succeed())
}, time.Minute).Should(Succeed())
})
It("Test re-deploy application with old revisions", func() {
By("apply application")
app := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "test-app-target"},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Name: "test-busybox",
Type: "webservice",
Properties: &runtime.RawExtension{Raw: []byte(`{"image":"busybox","cmd":["sleep","86400"]}`)},
}},
Policies: []v1beta1.AppPolicy{{
Name: "topology-local",
Type: "topology",
Properties: &runtime.RawExtension{Raw: []byte(fmt.Sprintf(`{"clusters":["local"],"namespace":"%s"}`, testNamespace))},
},
}}}
oam.SetPublishVersion(app, "alpha")
Expect(k8sClient.Create(hubCtx, app)).Should(Succeed())
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox", Namespace: testNamespace}, &appsv1.Deployment{})).Should(Succeed())
}, time.Minute).Should(Succeed())
By("update application to new version")
appKey := client.ObjectKeyFromObject(app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())
app.Spec.Components[0].Name = "test-busybox-v2"
oam.SetPublishVersion(app, "beta")
g.Expect(k8sClient.Update(hubCtx, app)).Should(Succeed())
}, 15*time.Second).Should(Succeed())
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox-v2", Namespace: testNamespace}, &appsv1.Deployment{})).Should(Succeed())
err := k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox", Namespace: testNamespace}, &appsv1.Deployment{})
g.Expect(kerrors.IsNotFound(err)).Should(BeTrue())
}, time.Minute).Should(Succeed())
By("Re-publish application to v1")
_, err := execCommand("up", appKey.Name, "-n", appKey.Namespace, "--revision", appKey.Name+"-v1", "--publish-version", "v1.0")
Expect(err).Should(Succeed())
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox", Namespace: testNamespace}, &appsv1.Deployment{})).Should(Succeed())
err := k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox-v2", Namespace: testNamespace}, &appsv1.Deployment{})
g.Expect(kerrors.IsNotFound(err)).Should(BeTrue())
}, 2*time.Minute).Should(Succeed())
})
})
})