kubevela/references/cli/up.go

363 lines
13 KiB
Go

/*
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"
"fmt"
"os"
"time"
"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"
pkgUtils "github.com/oam-dev/kubevela/pkg/utils"
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
Wait bool
WaitTimeout 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)
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 := pkgUtils.ReadRemoteOrLocalPath(opt.File, true)
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}
if err = o.Run(opt.File, o.Namespace, utilcommon.Args{Schema: utilcommon.Scheme}); err != nil {
return err
}
opt.AppName = o.Name
} else {
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 applied.\n", green.Sprintf("%s/%s", 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
# Deploy an application from stdin
cat <<EOF | vela up -f -
... <app.yaml here> ...
EOF
`))
)
// 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{
WaitTimeout: "300s",
}
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{}
wargs := &WorkflowArgs{Args: c}
ctx := context.Background()
cmdutil.CheckErr(wargs.getWorkflowInstance(ctx, cmd, []string{o.AppName}))
if wargs.Type == instanceTypeWorkflowRun {
cmdutil.CheckErr(fmt.Errorf("please use `vela workflow debug <name>` instead"))
}
if wargs.App == nil {
cmdutil.CheckErr(fmt.Errorf("application %s not found", args[0]))
}
cmdutil.CheckErr(dOpts.debugApplication(ctx, wargs, c, ioStream))
}
if o.Wait {
dur, err := time.ParseDuration(o.WaitTimeout)
if err != nil {
cmdutil.CheckErr(fmt.Errorf("parse timeout duration err: %w", err))
}
status, err := printTrackingDeployStatus(c, ioStream, o.AppName, o.Namespace, dur)
if err != nil {
cmdutil.CheckErr(err)
}
if status != appDeployedHealthy {
os.Exit(1)
}
}
},
}
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")
cmd.Flags().BoolVarP(&o.Wait, "wait", "w", o.Wait, "Wait app to be healthy until timout, if no timeout specified, the default duration is 300s.")
cmd.Flags().StringVarP(&o.WaitTimeout, "timeout", "", o.WaitTimeout, "Set the timout for wait app to be healthy, if not specified, the default duration is 300s.")
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()
}