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  }