github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/addunit.go (about)

     1  // Copyright 2012-2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package application
     5  
     6  import (
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/gnuflag"
    13  	"gopkg.in/juju/names.v2"
    14  
    15  	"github.com/juju/juju/api/application"
    16  	"github.com/juju/juju/apiserver/params"
    17  	jujucmd "github.com/juju/juju/cmd"
    18  	"github.com/juju/juju/cmd/juju/block"
    19  	"github.com/juju/juju/cmd/juju/common"
    20  	"github.com/juju/juju/cmd/modelcmd"
    21  	"github.com/juju/juju/core/instance"
    22  	"github.com/juju/juju/core/model"
    23  )
    24  
    25  var usageAddUnitSummary = `Adds one or more units to a deployed application.`
    26  
    27  var usageAddUnitDetails = `
    28  The add-unit is used to scale out an application for improved performance or
    29  availability.
    30  
    31  The usage of this command differs depending on whether it is being used on a
    32  Kubernetes or cloud model.
    33  
    34  Many charms will seamlessly support horizontal scaling while others may need
    35  an additional application support (e.g. a separate load balancer). See the
    36  documentation for specific charms to check how scale-out is supported.
    37  
    38  For Kubernetes models the only valid argument is -n, --num-units.
    39  Anything additional will result in an error.
    40  
    41  Example:
    42  
    43  Add five units of mysql:
    44      juju add-unit mysql --num-units 5
    45  
    46  
    47  For cloud models, by default, units are deployed to newly provisioned machines
    48  in accordance with any application or model constraints.
    49  This command also supports the placement directive ("--to") for targeting
    50  specific machines or containers, which will bypass application and model
    51  constraints.
    52  
    53  Examples:
    54  
    55  Add five units of mysql on five new machines:
    56  
    57      juju add-unit mysql -n 5
    58  
    59  Add a unit of mysql to machine 23 (which already exists):
    60  
    61      juju add-unit mysql --to 23
    62  
    63  Add two units of mysql to existing machines 3 and 4:
    64  
    65     juju add-unit mysql -n 2 --to 3,4
    66  
    67  Add three units of mysql, one to machine 3 and the others to new
    68  machines:
    69  
    70      juju add-unit mysql -n 3 --to 3
    71  
    72  Add a unit of mysql into a new LXD container on machine 7:
    73  
    74      juju add-unit mysql --to lxd:7
    75  
    76  Add two units of mysql into two new LXD containers on machine 7:
    77  
    78      juju add-unit mysql -n 2 --to lxd:7,lxd:7
    79  
    80  Add a unit of mysql to LXD container number 3 on machine 24:
    81  
    82      juju add-unit mysql --to 24/lxd/3
    83  
    84  Add a unit of mysql to LXD container on a new machine:
    85  
    86      juju add-unit mysql --to lxd
    87  
    88  See also:
    89      remove-unit
    90  `[1:]
    91  
    92  // UnitCommandBase provides support for commands which deploy units. It handles the parsing
    93  // and validation of --to and --num-units arguments.
    94  type UnitCommandBase struct {
    95  	// PlacementSpec is the raw string command arg value used to specify placement directives.
    96  	PlacementSpec string
    97  	// Placement is the result of parsing the PlacementSpec arg value.
    98  	Placement []*instance.Placement
    99  	NumUnits  int
   100  	// AttachStorage is a list of storage IDs, identifying storage to
   101  	// attach to the unit created by deploy.
   102  	AttachStorage []string
   103  }
   104  
   105  func (c *UnitCommandBase) SetFlags(f *gnuflag.FlagSet) {
   106  	f.IntVar(&c.NumUnits, "num-units", 1, "")
   107  	f.StringVar(&c.PlacementSpec, "to", "", "The machine and/or container to deploy the unit in (bypasses constraints)")
   108  	f.Var(attachStorageFlag{&c.AttachStorage}, "attach-storage", "Existing storage to attach to the deployed unit (not available on kubernetes models)")
   109  }
   110  
   111  func (c *UnitCommandBase) Init(args []string) error {
   112  	if c.NumUnits < 1 {
   113  		return errors.New("--num-units must be a positive integer")
   114  	}
   115  	if len(c.AttachStorage) > 0 && c.NumUnits != 1 {
   116  		return errors.New("--attach-storage cannot be used with -n")
   117  	}
   118  	if c.PlacementSpec != "" {
   119  		placementSpecs := strings.Split(c.PlacementSpec, ",")
   120  		c.Placement = make([]*instance.Placement, len(placementSpecs))
   121  		for i, spec := range placementSpecs {
   122  			placement, err := parsePlacement(spec)
   123  			if err != nil {
   124  				return errors.Errorf("invalid --to parameter %q", spec)
   125  			}
   126  			c.Placement[i] = placement
   127  		}
   128  	}
   129  	if len(c.Placement) > c.NumUnits {
   130  		logger.Warningf("%d unit(s) will be deployed, extra placement directives will be ignored", c.NumUnits)
   131  	}
   132  	return nil
   133  }
   134  
   135  func parsePlacement(spec string) (*instance.Placement, error) {
   136  	if spec == "" {
   137  		return nil, nil
   138  	}
   139  	placement, err := instance.ParsePlacement(spec)
   140  	if err == instance.ErrPlacementScopeMissing {
   141  		spec = "model-uuid" + ":" + spec
   142  		placement, err = instance.ParsePlacement(spec)
   143  	}
   144  	if err != nil {
   145  		return nil, errors.Errorf("invalid --to parameter %q", spec)
   146  	}
   147  	return placement, nil
   148  }
   149  
   150  // NewAddUnitCommand returns a command that adds a unit[s] to an application.
   151  func NewAddUnitCommand() cmd.Command {
   152  	return modelcmd.Wrap(&addUnitCommand{})
   153  }
   154  
   155  // addUnitCommand is responsible adding additional units to an application.
   156  type addUnitCommand struct {
   157  	modelcmd.ModelCommandBase
   158  	UnitCommandBase
   159  	ApplicationName string
   160  	api             applicationAddUnitAPI
   161  
   162  	unknownModel bool
   163  }
   164  
   165  func (c *addUnitCommand) Info() *cmd.Info {
   166  	return jujucmd.Info(&cmd.Info{
   167  		Name:    "add-unit",
   168  		Args:    "<application name>",
   169  		Purpose: usageAddUnitSummary,
   170  		Doc:     usageAddUnitDetails,
   171  	})
   172  }
   173  
   174  // IncompatibleModel returns an error if the command is being run against
   175  // a model with which it is not compatible.
   176  func (c *addUnitCommand) IncompatibleModel(err error) error {
   177  	if err == nil {
   178  		return nil
   179  	}
   180  	msg := `
   181  add-unit is not allowed on Kubernetes models.
   182  Instead, use juju scale-application.
   183  See juju help scale-application.
   184  `[1:]
   185  	return errors.New(msg)
   186  }
   187  
   188  func (c *addUnitCommand) SetFlags(f *gnuflag.FlagSet) {
   189  	c.UnitCommandBase.SetFlags(f)
   190  	f.IntVar(&c.NumUnits, "n", 1, "Number of units to add")
   191  }
   192  
   193  func (c *addUnitCommand) Init(args []string) error {
   194  	switch len(args) {
   195  	case 1:
   196  		c.ApplicationName = args[0]
   197  	case 0:
   198  		return errors.New("no application specified")
   199  	}
   200  	if err := cmd.CheckEmpty(args[1:]); err != nil {
   201  		return err
   202  	}
   203  	if err := c.validateArgsByModelType(); err != nil {
   204  		if !errors.IsNotFound(err) {
   205  			return errors.Trace(err)
   206  		}
   207  		c.unknownModel = true
   208  	}
   209  
   210  	return c.UnitCommandBase.Init(args)
   211  }
   212  
   213  func (c *addUnitCommand) validateArgsByModelType() error {
   214  	modelType, err := c.ModelType()
   215  	if err != nil {
   216  		return err
   217  	}
   218  	if modelType == model.CAAS {
   219  		if c.PlacementSpec != "" || len(c.AttachStorage) != 0 {
   220  			return errors.New("Kubernetes models only support --num-units")
   221  		}
   222  	}
   223  	return nil
   224  }
   225  
   226  // applicationAddUnitAPI defines the methods on the client API
   227  // that the application add-unit command calls.
   228  type applicationAddUnitAPI interface {
   229  	BestAPIVersion() int
   230  	Close() error
   231  	ModelUUID() string
   232  	AddUnits(application.AddUnitsParams) ([]string, error)
   233  	ScaleApplication(application.ScaleApplicationParams) (params.ScaleApplicationResult, error)
   234  }
   235  
   236  func (c *addUnitCommand) getAPI() (applicationAddUnitAPI, error) {
   237  	if c.api != nil {
   238  		return c.api, nil
   239  	}
   240  	root, err := c.NewAPIRoot()
   241  	if err != nil {
   242  		return nil, errors.Trace(err)
   243  	}
   244  	return application.NewClient(root), nil
   245  }
   246  
   247  // Run connects to the environment specified on the command line
   248  // and calls AddUnits for the given application.
   249  func (c *addUnitCommand) Run(ctx *cmd.Context) error {
   250  	apiclient, err := c.getAPI()
   251  	if err != nil {
   252  		return err
   253  	}
   254  	defer apiclient.Close()
   255  
   256  	if c.unknownModel {
   257  		if err := c.validateArgsByModelType(); err != nil {
   258  			return errors.Trace(err)
   259  		}
   260  	}
   261  
   262  	modelType, err := c.ModelType()
   263  	if err != nil {
   264  		return err
   265  	}
   266  
   267  	if modelType == model.CAAS {
   268  		_, err = apiclient.ScaleApplication(application.ScaleApplicationParams{
   269  			ApplicationName: c.ApplicationName,
   270  			ScaleChange:     c.NumUnits,
   271  		})
   272  		if params.IsCodeUnauthorized(err) {
   273  			common.PermissionsMessage(ctx.Stderr, "scale an application")
   274  		}
   275  		return block.ProcessBlockedError(err, block.BlockChange)
   276  	}
   277  
   278  	if len(c.AttachStorage) > 0 && apiclient.BestAPIVersion() < 5 {
   279  		// AddUnitsPArams.AttachStorage is only supported from
   280  		// Application API version 5 and onwards.
   281  		return errors.New("this juju controller does not support --attach-storage")
   282  	}
   283  
   284  	for i, p := range c.Placement {
   285  		if p.Scope == "model-uuid" {
   286  			p.Scope = apiclient.ModelUUID()
   287  		}
   288  		c.Placement[i] = p
   289  	}
   290  	_, err = apiclient.AddUnits(application.AddUnitsParams{
   291  		ApplicationName: c.ApplicationName,
   292  		NumUnits:        c.NumUnits,
   293  		Placement:       c.Placement,
   294  		AttachStorage:   c.AttachStorage,
   295  	})
   296  	if params.IsCodeUnauthorized(err) {
   297  		common.PermissionsMessage(ctx.Stderr, "add a unit")
   298  	}
   299  	return block.ProcessBlockedError(err, block.BlockChange)
   300  }
   301  
   302  // deployTarget describes the format a machine or container target must match to be valid.
   303  const deployTarget = "^(" + names.ContainerTypeSnippet + ":)?" + names.MachineSnippet + "$"
   304  
   305  var validMachineOrNewContainer = regexp.MustCompile(deployTarget)
   306  
   307  // IsMachineOrNewContainer returns whether spec is a valid machine id
   308  // or new container definition.
   309  func IsMachineOrNewContainer(spec string) bool {
   310  	return validMachineOrNewContainer.MatchString(spec)
   311  }