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 }