github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/bundlediff.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package application 5 6 import ( 7 "github.com/juju/bundlechanges" 8 "github.com/juju/cmd" 9 "github.com/juju/errors" 10 "github.com/juju/gnuflag" 11 "gopkg.in/juju/charm.v6" 12 "gopkg.in/juju/charmrepo.v3" 13 csparams "gopkg.in/juju/charmrepo.v3/csclient/params" 14 "gopkg.in/yaml.v2" 15 16 "github.com/juju/juju/api/annotations" 17 "github.com/juju/juju/api/application" 18 "github.com/juju/juju/api/base" 19 "github.com/juju/juju/api/modelconfig" 20 "github.com/juju/juju/apiserver/params" 21 jujucmd "github.com/juju/juju/cmd" 22 "github.com/juju/juju/cmd/modelcmd" 23 "github.com/juju/juju/core/constraints" 24 "github.com/juju/juju/core/model" 25 ) 26 27 const bundleDiffDoc = ` 28 Bundle can be a local bundle file or the name of a bundle in 29 the charm store. The bundle can also be combined with overlays (in the 30 same way as the deploy command) before comparing with the model. 31 32 The map-machines option works similarly as for the deploy command, but 33 existing is always assumed, so it doesn't need to be specified. 34 35 Config values for comparison are always source from the "current" model 36 generation. 37 38 Examples: 39 juju diff-bundle localbundle.yaml 40 juju diff-bundle canonical-kubernetes 41 juju diff-bundle -m othermodel hadoop-spark 42 juju diff-bundle mongodb-cluster --channel beta 43 juju diff-bundle canonical-kubernetes --overlay local-config.yaml --overlay extra.yaml 44 juju diff-bundle localbundle.yaml --map-machines 3=4 45 46 See also: 47 deploy 48 ` 49 50 // NewBundleDiffCommand returns a command to compare a bundle against 51 // the selected model. 52 func NewBundleDiffCommand() cmd.Command { 53 return modelcmd.Wrap(&bundleDiffCommand{}) 54 } 55 56 // bundleDiffCommand compares a bundle to a model. 57 type bundleDiffCommand struct { 58 modelcmd.ModelCommandBase 59 bundle string 60 bundleOverlays []string 61 channel csparams.Channel 62 annotations bool 63 64 bundleMachines map[string]string 65 machineMap string 66 67 // These are set in tests to enable mocking out the API and the 68 // charm store. 69 _apiRoot base.APICallCloser 70 _charmStore BundleResolver 71 } 72 73 // IsSuperCommand is part of cmd.Command. 74 func (c *bundleDiffCommand) IsSuperCommand() bool { return false } 75 76 // AllowInterspersedFlags is part of cmd.Command. 77 func (c *bundleDiffCommand) AllowInterspersedFlags() bool { return true } 78 79 // Info is part of cmd.Command. 80 func (c *bundleDiffCommand) Info() *cmd.Info { 81 return jujucmd.Info(&cmd.Info{ 82 Name: "diff-bundle", 83 Args: "<bundle file or name>", 84 Purpose: "Compare a bundle with a model and report any differences.", 85 Doc: bundleDiffDoc, 86 }) 87 } 88 89 // SetFlags is part of cmd.Command. 90 func (c *bundleDiffCommand) SetFlags(f *gnuflag.FlagSet) { 91 c.ModelCommandBase.SetFlags(f) 92 f.StringVar((*string)(&c.channel), "channel", "", "Channel to use when getting the bundle from the charm store") 93 f.Var(cmd.NewAppendStringsValue(&c.bundleOverlays), "overlay", "Bundles to overlay on the primary bundle, applied in order") 94 f.StringVar(&c.machineMap, "map-machines", "", "Indicates how existing machines correspond to bundle machines") 95 f.BoolVar(&c.annotations, "annotations", false, "Include differences in annotations") 96 } 97 98 // Init is part of cmd.Command. 99 func (c *bundleDiffCommand) Init(args []string) error { 100 if len(args) < 1 { 101 return errors.New("no bundle specified") 102 } 103 c.bundle = args[0] 104 // UseExisting is assumed for diffing. 105 _, mapping, err := parseMachineMap(c.machineMap) 106 if err != nil { 107 return errors.Annotate(err, "error in --map-machines") 108 } 109 c.bundleMachines = mapping 110 111 return cmd.CheckEmpty(args[1:]) 112 } 113 114 // Run is part of cmd.Command. 115 func (c *bundleDiffCommand) Run(ctx *cmd.Context) error { 116 apiRoot, err := c.newAPIRoot() 117 if err != nil { 118 return errors.Trace(err) 119 } 120 defer apiRoot.Close() 121 122 // Load up the bundle data, with includes and overlays. 123 bundle, bundleDir, err := c.readBundle(ctx) 124 if err != nil { 125 return errors.Trace(err) 126 } 127 if err := composeBundle(bundle, ctx, bundleDir, c.bundleOverlays); err != nil { 128 return errors.Trace(err) 129 } 130 if err := verifyBundle(bundle, bundleDir); err != nil { 131 return errors.Trace(err) 132 } 133 134 // Extract the information from the current model. 135 model, err := c.readModel(apiRoot) 136 if err != nil { 137 return errors.Trace(err) 138 } 139 // Get the differences between them. 140 diff, err := bundlechanges.BuildDiff(bundlechanges.DiffConfig{ 141 Bundle: bundle, 142 Model: model, 143 Logger: logger, 144 IncludeAnnotations: c.annotations, 145 }) 146 147 if err != nil { 148 return errors.Trace(err) 149 } 150 151 encoder := yaml.NewEncoder(ctx.Stdout) 152 defer encoder.Close() 153 err = encoder.Encode(diff) 154 if err != nil { 155 return errors.Trace(err) 156 } 157 return nil 158 } 159 160 func (c *bundleDiffCommand) newAPIRoot() (base.APICallCloser, error) { 161 if c._apiRoot != nil { 162 return c._apiRoot, nil 163 } 164 return c.NewAPIRoot() 165 } 166 167 func (c *bundleDiffCommand) readBundle(ctx *cmd.Context) (*charm.BundleData, string, error) { 168 bundleData, bundleDir, err := readLocalBundle(ctx, c.bundle) 169 // NotValid means we should try interpreting it as a charm store 170 // bundle URL. 171 if err != nil && !errors.IsNotValid(err) { 172 return nil, "", errors.Trace(err) 173 } 174 if bundleData != nil { 175 return bundleData, bundleDir, nil 176 } 177 178 // Not a local bundle, so it must be from the charmstore. 179 charmStore, err := c.charmStore() 180 if err != nil { 181 return nil, "", errors.Trace(err) 182 } 183 bundleURL, _, err := resolveBundleURL( 184 charmStore, c.bundle, 185 ) 186 if err != nil && !errors.IsNotValid(err) { 187 return nil, "", errors.Trace(err) 188 } 189 if bundleURL == nil { 190 // This isn't a charmstore bundle either! Complain. 191 return nil, "", errors.Errorf("couldn't interpret %q as a local or charmstore bundle", c.bundle) 192 } 193 194 bundle, err := charmStore.GetBundle(bundleURL) 195 if err != nil { 196 return nil, "", errors.Trace(err) 197 } 198 199 return bundle.Data(), "", nil 200 } 201 202 func (c *bundleDiffCommand) charmStore() (BundleResolver, error) { 203 if c._charmStore != nil { 204 return c._charmStore, nil 205 } 206 controllerAPIRoot, err := c.NewControllerAPIRoot() 207 if err != nil { 208 return nil, errors.Trace(err) 209 } 210 defer controllerAPIRoot.Close() 211 csURL, err := getCharmStoreAPIURL(controllerAPIRoot) 212 if err != nil { 213 return nil, errors.Trace(err) 214 } 215 bakeryClient, err := c.BakeryClient() 216 if err != nil { 217 return nil, errors.Trace(err) 218 } 219 cstoreClient := newCharmStoreClient(bakeryClient, csURL).WithChannel(c.channel) 220 return charmrepo.NewCharmStoreFromClient(cstoreClient), nil 221 } 222 223 func (c *bundleDiffCommand) readModel(apiRoot base.APICallCloser) (*bundlechanges.Model, error) { 224 status, err := c.getStatus(apiRoot) 225 if err != nil { 226 return nil, errors.Annotate(err, "getting model status") 227 } 228 model, err := buildModelRepresentation(status, c.makeModelExtractor(apiRoot), true, c.bundleMachines) 229 return model, errors.Trace(err) 230 } 231 232 func (c *bundleDiffCommand) getStatus(apiRoot base.APICallCloser) (*params.FullStatus, error) { 233 // Ported from api.Client which is nigh impossible to test without 234 // a real api.Connection. 235 _, facade := base.NewClientFacade(apiRoot, "Client") 236 var result params.FullStatus 237 if err := facade.FacadeCall("FullStatus", params.StatusParams{}, &result); err != nil { 238 return nil, errors.Trace(err) 239 } 240 // We don't care about model type. 241 return &result, nil 242 } 243 244 func (c *bundleDiffCommand) makeModelExtractor(apiRoot base.APICallCloser) ModelExtractor { 245 return &extractorImpl{ 246 application: application.NewClient(apiRoot), 247 annotations: annotations.NewClient(apiRoot), 248 modelConfig: modelconfig.NewClient(apiRoot), 249 } 250 } 251 252 type extractorImpl struct { 253 application *application.Client 254 annotations *annotations.Client 255 modelConfig *modelconfig.Client 256 } 257 258 // GetAnnotations is part of ModelExtractor. 259 func (e *extractorImpl) GetAnnotations(tags []string) ([]params.AnnotationsGetResult, error) { 260 return e.annotations.Get(tags) 261 } 262 263 // GetConstraints is part of ModelExtractor. 264 func (e *extractorImpl) GetConstraints(applications ...string) ([]constraints.Value, error) { 265 return e.application.GetConstraints(applications...) 266 } 267 268 // GetConfig is part of ModelExtractor. 269 func (e *extractorImpl) GetConfig( 270 generation model.GenerationVersion, applications ...string, 271 ) ([]map[string]interface{}, error) { 272 return e.application.GetConfig(generation, applications...) 273 } 274 275 // Sequences is part of ModelExtractor. 276 func (e *extractorImpl) Sequences() (map[string]int, error) { 277 return e.modelConfig.Sequences() 278 } 279 280 // BundleResolver defines what we need from a charm store to resolve a 281 // bundle and read the bundle data. 282 type BundleResolver interface { 283 URLResolver 284 GetBundle(*charm.URL) (charm.Bundle, error) 285 }