github.com/oam-dev/kubevela@v1.9.11/references/cli/livediff.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/pkg/errors"
    26  	"github.com/spf13/cobra"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  
    30  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    31  	"github.com/oam-dev/kubevela/apis/types"
    32  	"github.com/oam-dev/kubevela/pkg/appfile/dryrun"
    33  	"github.com/oam-dev/kubevela/pkg/utils/common"
    34  	cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
    35  )
    36  
    37  // LiveDiffCmdOptions contains the live-diff cmd options
    38  type LiveDiffCmdOptions struct {
    39  	cmdutil.IOStreams
    40  	ApplicationFile   string
    41  	DefinitionFile    string
    42  	AppName           string
    43  	Namespace         string
    44  	Revision          string
    45  	SecondaryRevision string
    46  	Context           int
    47  }
    48  
    49  // NewLiveDiffCommand creates `live-diff` command
    50  func NewLiveDiffCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
    51  	o := &LiveDiffCmdOptions{IOStreams: ioStreams}
    52  
    53  	cmd := &cobra.Command{
    54  		Use:                   "live-diff",
    55  		DisableFlagsInUseLine: true,
    56  		Short:                 "Compare application and revisions.",
    57  		Long:                  "Compare application and revisions.",
    58  		Example: "# compare the current application and the running revision\n" +
    59  			"> vela live-diff my-app\n" +
    60  			"# compare the current application and the specified revision\n" +
    61  			"> vela live-diff my-app --revision my-app-v1\n" +
    62  			"# compare two application revisions\n" +
    63  			"> vela live-diff --revision my-app-v1,my-app-v2\n" +
    64  			"# compare the application file and the specified revision\n" +
    65  			"> vela live-diff -f my-app.yaml -r my-app-v1 --context 10",
    66  		Annotations: map[string]string{
    67  			types.TagCommandOrder: order,
    68  			types.TagCommandType:  types.TypeApp,
    69  		},
    70  		Args: cobra.RangeArgs(0, 1),
    71  		RunE: func(cmd *cobra.Command, args []string) (err error) {
    72  			o.Namespace, err = GetFlagNamespaceOrEnv(cmd, c)
    73  			if err != nil {
    74  				return err
    75  			}
    76  			if err = o.loadAndValidate(args); err != nil {
    77  				return err
    78  			}
    79  			buff, err := LiveDiffApplication(o, c)
    80  			if err != nil {
    81  				return err
    82  			}
    83  			cmd.Println(buff.String())
    84  			return nil
    85  		},
    86  	}
    87  
    88  	cmd.Flags().StringVarP(&o.ApplicationFile, "file", "f", "", "application file name")
    89  	cmd.Flags().StringVarP(&o.DefinitionFile, "definition", "d", "", "specify a file or directory containing capability definitions, they will only be used in dry-run rather than applied to K8s cluster")
    90  	cmd.Flags().StringVarP(&o.Revision, "revision", "r", "", "specify one or two application revision name(s), by default, it will compare with the latest revision")
    91  	cmd.Flags().IntVarP(&o.Context, "context", "c", -1, "output number lines of context around changes, by default show all unchanged lines")
    92  	addNamespaceAndEnvArg(cmd)
    93  	return cmd
    94  }
    95  
    96  // LiveDiffApplication can return user what would change if upgrade an application.
    97  func LiveDiffApplication(cmdOption *LiveDiffCmdOptions, c common.Args) (bytes.Buffer, error) {
    98  	var buff = bytes.Buffer{}
    99  
   100  	newClient, err := c.GetClient()
   101  	if err != nil {
   102  		return buff, err
   103  	}
   104  	var objs []*unstructured.Unstructured
   105  	if cmdOption.DefinitionFile != "" {
   106  		objs, err = ReadDefinitionsFromFile(cmdOption.DefinitionFile, cmdOption.IOStreams)
   107  		if err != nil {
   108  			return buff, err
   109  		}
   110  	}
   111  	pd, err := c.GetPackageDiscover()
   112  	if err != nil {
   113  		return buff, err
   114  	}
   115  	config, err := c.GetConfig()
   116  	if err != nil {
   117  		return buff, err
   118  	}
   119  	liveDiffOption := dryrun.NewLiveDiffOption(newClient, config, pd, objs)
   120  	if cmdOption.ApplicationFile == "" {
   121  		return cmdOption.renderlessDiff(newClient, liveDiffOption)
   122  	}
   123  
   124  	app, err := readApplicationFromFile(cmdOption.ApplicationFile)
   125  	if err != nil {
   126  		return buff, errors.WithMessagef(err, "read application file: %s", cmdOption.ApplicationFile)
   127  	}
   128  	if app.Namespace == "" {
   129  		app.SetNamespace(cmdOption.Namespace)
   130  	}
   131  
   132  	appRevision := &v1beta1.ApplicationRevision{}
   133  	if cmdOption.Revision != "" {
   134  		// get the Revision if user specifies
   135  		if err := newClient.Get(context.Background(),
   136  			client.ObjectKey{Name: cmdOption.Revision, Namespace: app.Namespace}, appRevision); err != nil {
   137  			return buff, errors.Wrapf(err, "cannot get application Revision %q", cmdOption.Revision)
   138  		}
   139  	} else {
   140  		// get the latest Revision of the application
   141  		livingApp := &v1beta1.Application{}
   142  		if err := newClient.Get(context.Background(),
   143  			client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, livingApp); err != nil {
   144  			return buff, errors.Wrapf(err, "cannot get application %q", app.Name)
   145  		}
   146  		if livingApp.Status.LatestRevision != nil {
   147  			latestRevName := livingApp.Status.LatestRevision.Name
   148  			if err := newClient.Get(context.Background(),
   149  				client.ObjectKey{Name: latestRevName, Namespace: app.Namespace}, appRevision); err != nil {
   150  				return buff, errors.Wrapf(err, "cannot get application Revision %q", cmdOption.Revision)
   151  			}
   152  		} else {
   153  			// .status.latestRevision is nil, that means the app has not
   154  			// been rendered yet
   155  			return buff, fmt.Errorf("the application %q has no Revision in the cluster", app.Name)
   156  		}
   157  	}
   158  
   159  	diffResult, err := liveDiffOption.Diff(context.Background(), app, appRevision)
   160  	if err != nil {
   161  		return buff, errors.WithMessage(err, "cannot calculate diff")
   162  	}
   163  
   164  	reportDiffOpt := dryrun.NewReportDiffOption(cmdOption.Context, &buff)
   165  	reportDiffOpt.PrintDiffReport(diffResult)
   166  
   167  	return buff, nil
   168  }
   169  
   170  func (o *LiveDiffCmdOptions) loadAndValidate(args []string) error {
   171  	if len(args) > 0 {
   172  		o.AppName = args[0]
   173  	}
   174  	revisions := strings.Split(o.Revision, ",")
   175  	if len(revisions) > 2 {
   176  		return errors.Errorf("cannot use more than 2 revisions")
   177  	}
   178  	o.Revision = revisions[0]
   179  	if len(revisions) == 2 {
   180  		o.SecondaryRevision = revisions[1]
   181  	}
   182  	if (o.AppName == "" && len(revisions) == 1) && o.ApplicationFile == "" {
   183  		return errors.Errorf("either application name or application file must be set")
   184  	}
   185  	if (o.AppName != "" || len(revisions) > 1) && o.ApplicationFile != "" {
   186  		return errors.Errorf("cannot set application name and application file at the same time")
   187  	}
   188  	if o.AppName != "" && o.SecondaryRevision != "" {
   189  		return errors.Errorf("cannot use application name and two revisions at the same time")
   190  	}
   191  	if o.SecondaryRevision != "" && o.ApplicationFile != "" {
   192  		return errors.Errorf("cannot use application file and two revisions at the same time")
   193  	}
   194  	return nil
   195  }
   196  
   197  func (o *LiveDiffCmdOptions) renderlessDiff(cli client.Client, option *dryrun.LiveDiffOption) (bytes.Buffer, error) {
   198  	var base, comparor dryrun.LiveDiffObject
   199  	ctx := context.Background()
   200  	var buf bytes.Buffer
   201  	if o.AppName != "" {
   202  		app := &v1beta1.Application{}
   203  		if err := cli.Get(ctx, client.ObjectKey{Name: o.AppName, Namespace: o.Namespace}, app); err != nil {
   204  			return buf, errors.Wrapf(err, "cannot get application %s/%s", o.Namespace, o.AppName)
   205  		}
   206  		base = dryrun.LiveDiffObject{Application: app}
   207  		if o.Revision == "" {
   208  			if app.Status.LatestRevision == nil {
   209  				return buf, errors.Errorf("no latest application revision available for application %s/%s", o.Namespace, o.AppName)
   210  			}
   211  			o.Revision = app.Status.LatestRevision.Name
   212  		}
   213  	}
   214  	rev, secondaryRev := &v1beta1.ApplicationRevision{}, &v1beta1.ApplicationRevision{}
   215  	if err := cli.Get(ctx, client.ObjectKey{Name: o.Revision, Namespace: o.Namespace}, rev); err != nil {
   216  		return buf, errors.Wrapf(err, "cannot get application revision %s/%s", o.Namespace, o.Revision)
   217  	}
   218  	if o.SecondaryRevision == "" {
   219  		comparor = dryrun.LiveDiffObject{ApplicationRevision: rev}
   220  	} else {
   221  		if err := cli.Get(ctx, client.ObjectKey{Name: o.SecondaryRevision, Namespace: o.Namespace}, secondaryRev); err != nil {
   222  			return buf, errors.Wrapf(err, "cannot get application revision %s/%s", o.Namespace, o.SecondaryRevision)
   223  		}
   224  		base = dryrun.LiveDiffObject{ApplicationRevision: rev}
   225  		comparor = dryrun.LiveDiffObject{ApplicationRevision: secondaryRev}
   226  	}
   227  	diffResult, err := option.RenderlessDiff(ctx, base, comparor)
   228  	if err != nil {
   229  		return buf, errors.WithMessage(err, "cannot calculate diff")
   230  	}
   231  	reportDiffOpt := dryrun.NewReportDiffOption(o.Context, &buf)
   232  	reportDiffOpt.PrintDiffReport(diffResult)
   233  	return buf, nil
   234  }