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 }