1/* 2Copyright 2018 The Doctl Authors All rights reserved. 3Licensed under the Apache License, Version 2.0 (the "License"); 4you may not use this file except in compliance with the License. 5You may obtain a copy of the License at 6 http://www.apache.org/licenses/LICENSE-2.0 7Unless required by applicable law or agreed to in writing, software 8distributed under the License is distributed on an "AS IS" BASIS, 9WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10See the License for the specific language governing permissions and 11limitations under the License. 12*/ 13 14package commands 15 16import ( 17 "fmt" 18 "reflect" 19 "strconv" 20 "strings" 21 22 "github.com/digitalocean/doctl" 23 "github.com/digitalocean/doctl/commands/displayers" 24 "github.com/digitalocean/doctl/do" 25 "github.com/digitalocean/godo" 26 "github.com/spf13/cobra" 27) 28 29// LoadBalancer creates the load balancer command. 30func LoadBalancer() *Command { 31 cmd := &Command{ 32 Command: &cobra.Command{ 33 Use: "load-balancer", 34 Short: "Display commands to manage load balancers", 35 Long: `The sub-commands of ` + "`" + `doctl compute load-balancer` + "`" + ` manage your load balancers. 36 37With the load-balancer command, you can list, create, or delete load balancers, and manage their configuration details.`, 38 }, 39 } 40 lbDetail := ` 41 42- The load balancer's ID 43- The load balancer's name 44- The load balancer's IP address 45- The load balancer's traffic algorithm. Must 46 be either ` + "`" + `round_robin` + "`" + ` or ` + "`" + `least_connections` + "`" + ` 47- The current state of the load balancer. This can be ` + "`" + `new` + "`" + `, ` + "`" + `active` + "`" + `, or ` + "`" + `errored` + "`" + `. 48- The load balancer's creation date, in ISO8601 combined date and time format. 49- The load balancer's forwarding rules. See ` + "`" + `doctl compute load-balancer add-forwarding-rules --help` + "`" + ` for a list. 50- The ` + "`" + `health_check` + "`" + ` settings for the load balancer. 51- The ` + "`" + `sticky_sessions` + "`" + ` settings for the load balancer. 52- The datacenter region the load balancer is located in. 53- The Droplet tag corresponding to the Droplets assigned to the load balancer. 54- The IDs of the Droplets assigned to the load balancer. 55- Whether HTTP request to the load balancer on port 80 will be redirected to HTTPS on port 443. 56- Whether the PROXY protocol is in use on the load balancer. 57` 58 forwardingDetail := ` 59 60- ` + "`" + `entry_protocol` + "`" + `: The entry protocol used for traffic to the load balancer. Possible values are: ` + "`" + `http` + "`" + `, ` + "`" + `https` + "`" + `, ` + "`" + `http2` + "`" + `, or ` + "`" + `tcp` + "`" + `. 61- ` + "`" + `entry_port` + "`" + `: The entry port used for incoming traffic to the load balancer. 62- ` + "`" + `target_protocol` + "`" + `: The target protocol used for traffic from the load balancer to the backend Droplets. Possible values are: ` + "`" + `http` + "`" + `, ` + "`" + `https` + "`" + `, ` + "`" + `http2` + "`" + `, or ` + "`" + `tcp` + "`" + `. 63- ` + "`" + `target_port` + "`" + `: The target port used to send traffic from the load balancer to the backend Droplets. 64- ` + "`" + `certificate_id` + "`" + `: The ID of the TLS certificate used for SSL termination, if enabled. Can be obtained with ` + "`" + `doctl certificate list` + "`" + ` 65- ` + "`" + `tls_passthrough` + "`" + `: Whether SSL passthrough is enabled on the load balancer. 66` 67 forwardingRulesTxt := "A comma-separated list of key-value pairs representing forwarding rules, which define how traffic is routed, e.g.: `entry_protocol:tcp,entry_port:3306,target_protocol:tcp,target_port:3306`." 68 CmdBuilder(cmd, RunLoadBalancerGet, "get <id>", "Retrieve a load balancer", "Use this command to retrieve information about a load balancer instance, including:"+lbDetail, Writer, 69 aliasOpt("g"), displayerType(&displayers.LoadBalancer{})) 70 71 cmdRecordCreate := CmdBuilder(cmd, RunLoadBalancerCreate, "create", 72 "Create a new load balancer", "Use this command to create a new load balancer on your account. Valid forwarding rules are:"+forwardingDetail, Writer, aliasOpt("c")) 73 AddStringFlag(cmdRecordCreate, doctl.ArgLoadBalancerName, "", "", 74 "The load balancer's name", requiredOpt()) 75 AddStringFlag(cmdRecordCreate, doctl.ArgRegionSlug, "", "", 76 "The load balancer's region, e.g.: `nyc1`", requiredOpt()) 77 AddStringFlag(cmdRecordCreate, doctl.ArgSizeSlug, "", "", 78 fmt.Sprintf("The load balancer's size, e.g.: `lb-small`. Only one of %s and %s should be used", doctl.ArgSizeSlug, doctl.ArgSizeUnit)) 79 AddIntFlag(cmdRecordCreate, doctl.ArgSizeUnit, "", 0, 80 fmt.Sprintf("The load balancer's size, e.g.: 1. Only one of %s and %s should be used", doctl.ArgSizeUnit, doctl.ArgSizeSlug)) 81 AddStringFlag(cmdRecordCreate, doctl.ArgVPCUUID, "", "", "The UUID of the VPC to create the load balancer in") 82 AddStringFlag(cmdRecordCreate, doctl.ArgLoadBalancerAlgorithm, "", 83 "round_robin", "The algorithm to use when traffic is distributed across your Droplets; possible values: `round_robin` or `least_connections`") 84 AddBoolFlag(cmdRecordCreate, doctl.ArgRedirectHTTPToHTTPS, "", false, 85 "Redirects HTTP requests to the load balancer on port 80 to HTTPS on port 443") 86 AddBoolFlag(cmdRecordCreate, doctl.ArgEnableProxyProtocol, "", false, 87 "enable proxy protocol") 88 AddBoolFlag(cmdRecordCreate, doctl.ArgEnableBackendKeepalive, "", false, 89 "enable keepalive connections to backend target droplets") 90 AddBoolFlag(cmdRecordCreate, doctl.ArgDisableLetsEncryptDNSRecords, "", false, 91 "disable automatic DNS record creation for Let's Encrypt certificates that are added to the load balancer") 92 AddStringFlag(cmdRecordCreate, doctl.ArgTagName, "", "", "droplet tag name") 93 AddStringSliceFlag(cmdRecordCreate, doctl.ArgDropletIDs, "", []string{}, 94 "A comma-separated list of Droplet IDs to add to the load balancer, e.g.: `12,33`") 95 AddStringFlag(cmdRecordCreate, doctl.ArgStickySessions, "", "", 96 "A comma-separated list of key-value pairs representing a list of active sessions, e.g.: `type:cookies, cookie_name:DO-LB, cookie_ttl_seconds:5`") 97 AddStringFlag(cmdRecordCreate, doctl.ArgHealthCheck, "", "", 98 "A comma-separated list of key-value pairs representing recent health check results, e.g.: `protocol:http,port:80,path:/index.html,check_interval_seconds:10,response_timeout_seconds:5,healthy_threshold:5,unhealthy_threshold:3`") 99 AddStringFlag(cmdRecordCreate, doctl.ArgForwardingRules, "", "", 100 forwardingRulesTxt) 101 102 cmdRecordUpdate := CmdBuilder(cmd, RunLoadBalancerUpdate, "update <id>", 103 "Update a load balancer's configuration", `Use this command to update the configuration of a specified load balancer.`, Writer, aliasOpt("u")) 104 AddStringFlag(cmdRecordUpdate, doctl.ArgLoadBalancerName, "", "", 105 "The load balancer's name", requiredOpt()) 106 AddStringFlag(cmdRecordUpdate, doctl.ArgRegionSlug, "", "", 107 "The load balancer's region, e.g.: `nyc1`", requiredOpt()) 108 AddStringFlag(cmdRecordUpdate, doctl.ArgSizeSlug, "", "", 109 fmt.Sprintf("The load balancer's size, e.g.: `lb-small`. Only one of %s and %s should be used", doctl.ArgSizeSlug, doctl.ArgSizeUnit)) 110 AddIntFlag(cmdRecordUpdate, doctl.ArgSizeUnit, "", 0, 111 fmt.Sprintf("The load balancer's size, e.g.: 1. Only one of %s and %s should be used", doctl.ArgSizeUnit, doctl.ArgSizeSlug)) 112 AddStringFlag(cmdRecordUpdate, doctl.ArgVPCUUID, "", "", "The UUID of the VPC to create the load balancer in") 113 AddStringFlag(cmdRecordUpdate, doctl.ArgLoadBalancerAlgorithm, "", 114 "round_robin", "The algorithm to use when traffic is distributed across your Droplets; possible values: `round_robin` or `least_connections`") 115 AddBoolFlag(cmdRecordUpdate, doctl.ArgRedirectHTTPToHTTPS, "", false, 116 "Flag to redirect HTTP requests to the load balancer on port 80 to HTTPS on port 443") 117 AddBoolFlag(cmdRecordUpdate, doctl.ArgEnableProxyProtocol, "", false, 118 "enable proxy protocol") 119 AddBoolFlag(cmdRecordUpdate, doctl.ArgEnableBackendKeepalive, "", false, 120 "enable keepalive connections to backend target droplets") 121 AddStringFlag(cmdRecordUpdate, doctl.ArgTagName, "", "", "Assigns Droplets with the specified tag to the load balancer") 122 AddStringSliceFlag(cmdRecordUpdate, doctl.ArgDropletIDs, "", []string{}, 123 "A comma-separated list of Droplet IDs, e.g.: `215,378`") 124 AddStringFlag(cmdRecordUpdate, doctl.ArgStickySessions, "", "", 125 "A comma-separated list of key-value pairs representing a list of active sessions, e.g.: `type:cookies, cookie_name:DO-LB, cookie_ttl_seconds:5`") 126 AddStringFlag(cmdRecordUpdate, doctl.ArgHealthCheck, "", "", 127 "A comma-separated list of key-value pairs representing recent health check results, e.g.: `protocol:http, port:80, path:/index.html, check_interval_seconds:10, response_timeout_seconds:5, healthy_threshold:5, unhealthy_threshold:3`") 128 AddStringFlag(cmdRecordUpdate, doctl.ArgForwardingRules, "", "", forwardingRulesTxt) 129 AddBoolFlag(cmdRecordUpdate, doctl.ArgDisableLetsEncryptDNSRecords, "", false, 130 "disable automatic DNS record creation for Let's Encrypt certificates that are added to the load balancer") 131 132 CmdBuilder(cmd, RunLoadBalancerList, "list", "List load balancers", "Use this command to get a list of the load balancers on your account, including the following information for each:"+lbDetail, Writer, 133 aliasOpt("ls"), displayerType(&displayers.LoadBalancer{})) 134 135 cmdRunRecordDelete := CmdBuilder(cmd, RunLoadBalancerDelete, "delete <id>", 136 "Permanently delete a load balancer", `Use this command to permanently delete the specified load balancer. This is irreversible.`, Writer, aliasOpt("d", "rm")) 137 AddBoolFlag(cmdRunRecordDelete, doctl.ArgForce, doctl.ArgShortForce, false, 138 "Delete the load balancer without a confirmation prompt") 139 140 cmdAddDroplets := CmdBuilder(cmd, RunLoadBalancerAddDroplets, "add-droplets <id>", 141 "Add Droplets to a load balancer", `Use this command to add Droplets to a load balancer.`, Writer) 142 AddStringSliceFlag(cmdAddDroplets, doctl.ArgDropletIDs, "", []string{}, 143 "A comma-separated list of IDs of Droplet to add to the load balancer, example value: `12,33`") 144 145 cmdRemoveDroplets := CmdBuilder(cmd, RunLoadBalancerRemoveDroplets, 146 "remove-droplets <id>", "Remove Droplets from a load balancer", `Use this command to remove Droplets from a load balancer. This command does not destroy any Droplets.`, Writer) 147 AddStringSliceFlag(cmdRemoveDroplets, doctl.ArgDropletIDs, "", []string{}, 148 "A comma-separated list of IDs of Droplets to remove from the load balancer, example value: `12,33`") 149 150 cmdAddForwardingRules := CmdBuilder(cmd, RunLoadBalancerAddForwardingRules, 151 "add-forwarding-rules <id>", "Add forwarding rules to a load balancer", "Use this command to add forwarding rules to a load balancer, specified with the `--forwarding-rules` flag. Valid rules include:"+forwardingDetail, Writer) 152 AddStringFlag(cmdAddForwardingRules, doctl.ArgForwardingRules, "", "", forwardingRulesTxt) 153 154 cmdRemoveForwardingRules := CmdBuilder(cmd, RunLoadBalancerRemoveForwardingRules, 155 "remove-forwarding-rules <id>", "Remove forwarding rules from a load balancer", "Use this command to remove forwarding rules from a load balancer, specified with the `--forwarding-rules` flag. Valid rules include:"+forwardingDetail, Writer) 156 AddStringFlag(cmdRemoveForwardingRules, doctl.ArgForwardingRules, "", "", forwardingRulesTxt) 157 158 return cmd 159} 160 161// RunLoadBalancerGet retrieves an existing load balancer by its identifier. 162func RunLoadBalancerGet(c *CmdConfig) error { 163 err := ensureOneArg(c) 164 if err != nil { 165 return err 166 } 167 id := c.Args[0] 168 169 lbs := c.LoadBalancers() 170 lb, err := lbs.Get(id) 171 if err != nil { 172 return err 173 } 174 175 item := &displayers.LoadBalancer{LoadBalancers: do.LoadBalancers{*lb}} 176 return c.Display(item) 177} 178 179// RunLoadBalancerList lists load balancers. 180func RunLoadBalancerList(c *CmdConfig) error { 181 lbs := c.LoadBalancers() 182 list, err := lbs.List() 183 if err != nil { 184 return err 185 } 186 187 item := &displayers.LoadBalancer{LoadBalancers: list} 188 return c.Display(item) 189} 190 191// RunLoadBalancerCreate creates a new load balancer with a given configuration. 192func RunLoadBalancerCreate(c *CmdConfig) error { 193 r := new(godo.LoadBalancerRequest) 194 if err := buildRequestFromArgs(c, r); err != nil { 195 return err 196 } 197 198 lbs := c.LoadBalancers() 199 lb, err := lbs.Create(r) 200 if err != nil { 201 return err 202 } 203 204 item := &displayers.LoadBalancer{LoadBalancers: do.LoadBalancers{*lb}} 205 return c.Display(item) 206} 207 208// RunLoadBalancerUpdate updates an existing load balancer with new configuration. 209func RunLoadBalancerUpdate(c *CmdConfig) error { 210 if len(c.Args) == 0 { 211 return doctl.NewMissingArgsErr(c.NS) 212 } 213 lbID := c.Args[0] 214 215 r := new(godo.LoadBalancerRequest) 216 if err := buildRequestFromArgs(c, r); err != nil { 217 return err 218 } 219 220 lbs := c.LoadBalancers() 221 lb, err := lbs.Update(lbID, r) 222 if err != nil { 223 return err 224 } 225 226 item := &displayers.LoadBalancer{LoadBalancers: do.LoadBalancers{*lb}} 227 return c.Display(item) 228} 229 230// RunLoadBalancerDelete deletes a load balancer by its identifier. 231func RunLoadBalancerDelete(c *CmdConfig) error { 232 err := ensureOneArg(c) 233 if err != nil { 234 return err 235 } 236 lbID := c.Args[0] 237 238 force, err := c.Doit.GetBool(c.NS, doctl.ArgForce) 239 if err != nil { 240 return err 241 } 242 243 if force || AskForConfirmDelete("load balancer", 1) == nil { 244 lbs := c.LoadBalancers() 245 if err := lbs.Delete(lbID); err != nil { 246 return err 247 } 248 } else { 249 return errOperationAborted 250 } 251 252 return nil 253} 254 255// RunLoadBalancerAddDroplets adds droplets to a load balancer. 256func RunLoadBalancerAddDroplets(c *CmdConfig) error { 257 err := ensureOneArg(c) 258 if err != nil { 259 return err 260 } 261 lbID := c.Args[0] 262 263 dropletIDsList, err := c.Doit.GetStringSlice(c.NS, doctl.ArgDropletIDs) 264 if err != nil { 265 return err 266 } 267 268 dropletIDs, err := extractDropletIDs(dropletIDsList) 269 if err != nil { 270 return err 271 } 272 273 return c.LoadBalancers().AddDroplets(lbID, dropletIDs...) 274} 275 276// RunLoadBalancerRemoveDroplets removes droplets from a load balancer. 277func RunLoadBalancerRemoveDroplets(c *CmdConfig) error { 278 err := ensureOneArg(c) 279 if err != nil { 280 return err 281 } 282 lbID := c.Args[0] 283 284 dropletIDsList, err := c.Doit.GetStringSlice(c.NS, doctl.ArgDropletIDs) 285 if err != nil { 286 return err 287 } 288 289 dropletIDs, err := extractDropletIDs(dropletIDsList) 290 if err != nil { 291 return err 292 } 293 294 return c.LoadBalancers().RemoveDroplets(lbID, dropletIDs...) 295} 296 297// RunLoadBalancerAddForwardingRules adds forwarding rules to a load balancer. 298func RunLoadBalancerAddForwardingRules(c *CmdConfig) error { 299 err := ensureOneArg(c) 300 if err != nil { 301 return err 302 } 303 lbID := c.Args[0] 304 305 fra, err := c.Doit.GetString(c.NS, doctl.ArgForwardingRules) 306 if err != nil { 307 return err 308 } 309 310 forwardingRules, err := extractForwardingRules(fra) 311 if err != nil { 312 return err 313 } 314 315 return c.LoadBalancers().AddForwardingRules(lbID, forwardingRules...) 316} 317 318// RunLoadBalancerRemoveForwardingRules removes forwarding rules from a load balancer. 319func RunLoadBalancerRemoveForwardingRules(c *CmdConfig) error { 320 err := ensureOneArg(c) 321 if err != nil { 322 return err 323 } 324 lbID := c.Args[0] 325 326 fra, err := c.Doit.GetString(c.NS, doctl.ArgForwardingRules) 327 if err != nil { 328 return err 329 } 330 331 forwardingRules, err := extractForwardingRules(fra) 332 if err != nil { 333 return err 334 } 335 336 return c.LoadBalancers().RemoveForwardingRules(lbID, forwardingRules...) 337} 338 339func extractForwardingRules(s string) (forwardingRules []godo.ForwardingRule, err error) { 340 if len(s) == 0 { 341 return forwardingRules, err 342 } 343 344 list := strings.Split(s, " ") 345 346 for _, v := range list { 347 forwardingRule := new(godo.ForwardingRule) 348 if err := fillStructFromStringSliceArgs(forwardingRule, v); err != nil { 349 return nil, err 350 } 351 352 forwardingRules = append(forwardingRules, *forwardingRule) 353 } 354 355 return forwardingRules, err 356} 357 358func fillStructFromStringSliceArgs(obj interface{}, s string) error { 359 if len(s) == 0 { 360 return nil 361 } 362 363 kvs := strings.Split(s, ",") 364 m := map[string]string{} 365 366 for _, v := range kvs { 367 p := strings.Split(v, ":") 368 if len(p) == 2 { 369 m[p[0]] = p[1] 370 } else { 371 return fmt.Errorf("Unexpected input value %v: must be a key:value pair", p) 372 } 373 } 374 375 structValue := reflect.Indirect(reflect.ValueOf(obj)) 376 structType := structValue.Type() 377 378 for i := 0; i < structType.NumField(); i++ { 379 f := structValue.Field(i) 380 jv := strings.Split(structType.Field(i).Tag.Get("json"), ",")[0] 381 382 if val, exists := m[jv]; exists { 383 switch f.Kind() { 384 case reflect.Bool: 385 if v, err := strconv.ParseBool(val); err == nil { 386 f.Set(reflect.ValueOf(v)) 387 } 388 case reflect.Int: 389 if v, err := strconv.Atoi(val); err == nil { 390 f.Set(reflect.ValueOf(v)) 391 } 392 case reflect.String: 393 f.Set(reflect.ValueOf(val)) 394 default: 395 return fmt.Errorf("Unexpected type for struct field %v", val) 396 } 397 } 398 } 399 400 return nil 401} 402 403func buildRequestFromArgs(c *CmdConfig, r *godo.LoadBalancerRequest) error { 404 name, err := c.Doit.GetString(c.NS, doctl.ArgLoadBalancerName) 405 if err != nil { 406 return err 407 } 408 r.Name = name 409 410 region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug) 411 if err != nil { 412 return err 413 } 414 r.Region = region 415 416 size, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug) 417 if err != nil { 418 return err 419 } 420 r.SizeSlug = size 421 422 sizeUnit, err := c.Doit.GetInt(c.NS, doctl.ArgSizeUnit) 423 if err != nil { 424 return err 425 } 426 r.SizeUnit = uint32(sizeUnit) 427 428 algorithm, err := c.Doit.GetString(c.NS, doctl.ArgLoadBalancerAlgorithm) 429 if err != nil { 430 return err 431 } 432 r.Algorithm = algorithm 433 434 tag, err := c.Doit.GetString(c.NS, doctl.ArgTagName) 435 if err != nil { 436 return err 437 } 438 r.Tag = tag 439 440 vpcUUID, err := c.Doit.GetString(c.NS, doctl.ArgVPCUUID) 441 if err != nil { 442 return err 443 } 444 r.VPCUUID = vpcUUID 445 446 redirectHTTPToHTTPS, err := c.Doit.GetBool(c.NS, doctl.ArgRedirectHTTPToHTTPS) 447 if err != nil { 448 return err 449 } 450 r.RedirectHttpToHttps = redirectHTTPToHTTPS 451 452 enableProxyProtocol, err := c.Doit.GetBool(c.NS, doctl.ArgEnableProxyProtocol) 453 if err != nil { 454 return err 455 } 456 r.EnableProxyProtocol = enableProxyProtocol 457 458 enableBackendKeepalive, err := c.Doit.GetBool(c.NS, doctl.ArgEnableBackendKeepalive) 459 if err != nil { 460 return err 461 } 462 r.EnableBackendKeepalive = enableBackendKeepalive 463 464 disableLetsEncryptDNSRecords, err := c.Doit.GetBool(c.NS, doctl.ArgDisableLetsEncryptDNSRecords) 465 if err != nil { 466 return err 467 } 468 r.DisableLetsEncryptDNSRecords = &disableLetsEncryptDNSRecords 469 470 dropletIDsList, err := c.Doit.GetStringSlice(c.NS, doctl.ArgDropletIDs) 471 if err != nil { 472 return err 473 } 474 475 dropletIDs, err := extractDropletIDs(dropletIDsList) 476 if err != nil { 477 return err 478 } 479 r.DropletIDs = dropletIDs 480 481 ssa, err := c.Doit.GetString(c.NS, doctl.ArgStickySessions) 482 if err != nil { 483 return err 484 } 485 486 stickySession := new(godo.StickySessions) 487 if err := fillStructFromStringSliceArgs(stickySession, ssa); err != nil { 488 return err 489 } 490 r.StickySessions = stickySession 491 492 hca, err := c.Doit.GetString(c.NS, doctl.ArgHealthCheck) 493 if err != nil { 494 return err 495 } 496 497 healthCheck := new(godo.HealthCheck) 498 if err := fillStructFromStringSliceArgs(healthCheck, hca); err != nil { 499 return err 500 } 501 r.HealthCheck = healthCheck 502 503 fra, err := c.Doit.GetString(c.NS, doctl.ArgForwardingRules) 504 if err != nil { 505 return err 506 } 507 508 forwardingRules, err := extractForwardingRules(fra) 509 if err != nil { 510 return err 511 } 512 r.ForwardingRules = forwardingRules 513 514 return nil 515} 516