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