/* 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 cli import ( "context" "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" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" 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" velacmd "github.com/oam-dev/kubevela/pkg/cmd" cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util" "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 Debug bool } // 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) if opt.Debug { addDebugPolicy(app) } return cli.Update(ctx, app) }); err != nil { return err } cmd.Printf("Application updated with new PublishVersion %s\n", opt.PublishVersion) return nil } func addDebugPolicy(app *v1beta1.Application) { for _, policy := range app.Spec.Policies { if policy.Type == "debug" { return } } app.Spec.Policies = append(app.Spec.Policies, v1beta1.AppPolicy{ Name: "debug", Type: "debug", }) } 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) } opt.AppName = app.Name if opt.Debug { app.Spec.Policies = append(app.Spec.Policies, v1beta1.AppPolicy{ Name: "debug", Type: "debug", }) } 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(f velacmd.Factory, order string, c utilcommon.Args, ioStream util.IOStreams) *cobra.Command { o := &UpCommandOptions{} cmd := &cobra.Command{ Use: "up", DisableFlagsInUseLine: true, Short: i18n.T("Deploy one application"), Long: upLong, Example: upExample, Annotations: map[string]string{ types.TagCommandOrder: order, types.TagCommandType: types.TypeStart, }, 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) } 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)) if o.Debug { dOpts := &debugOpts{} cli := f.Client() app := &v1beta1.Application{} cmdutil.CheckErr(cli.Get(cmd.Context(), apitypes.NamespacedName{Name: o.AppName, Namespace: o.Namespace}, app)) cmdutil.CheckErr(dOpts.debugApplication(context.Background(), c, app, ioStream)) } }, } 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.") cmd.Flags().BoolVarP(&o.Debug, "debug", "", o.Debug, "Enable debug mode for application") 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) })) return velacmd.NewCommandBuilder(f, cmd). WithNamespaceFlag(). WithResponsiveWriter(). Build() }