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	"errors"
18	"fmt"
19	"strings"
20
21	"github.com/digitalocean/doctl"
22	"github.com/digitalocean/doctl/commands/displayers"
23	"github.com/digitalocean/doctl/do"
24	"github.com/digitalocean/godo"
25	"github.com/spf13/cobra"
26)
27
28// Monitoring creates the monitoring commands hierarchy.
29func Monitoring() *Command {
30	cmd := &Command{
31		Command: &cobra.Command{
32			Use:   "monitoring",
33			Short: "[Beta] Display commands to manage monitoring",
34			Long: `The sub-commands of ` + "`" + `doctl monitoring` + "`" + ` manage the monitoring on your account.
35
36An alert policy can be applied to resource(s) (currently Droplets)
37in order to alert on resource consumption.`,
38		},
39	}
40
41	cmd.AddCommand(alertPolicies())
42	return cmd
43}
44
45func alertPolicies() *Command {
46	cmd := &Command{
47		Command: &cobra.Command{
48			Use:     "alert",
49			Aliases: []string{"alerts", "a"},
50			Short:   "Display commands for managing alert policies",
51			Long:    "The commands under `doctl monitoring alert` are for the management of alert policies.",
52		},
53	}
54
55	cmdAlertPolicyCreate := CmdBuilder(cmd, RunCmdAlertPolicyCreate, "create", "Create an alert policy", `Use this command to create a new alert policy.`, Writer)
56	AddStringFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyDescription, "", "", "A description of the alert policy.")
57	AddStringFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyType, "", "", "The type of alert policy.")
58	AddStringFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyCompare, "", "", "The comparator of the alert policy. Either `GreaterThan` or `LessThan`")
59	AddStringFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyWindow, "", "5m", "The window to apply the alert policy conditions against.")
60	AddIntFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyValue, "", 0, "The value of the alert policy to compare against.")
61	AddBoolFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyEnabled, "", true, "Whether the alert policy is enabled.")
62	AddStringSliceFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyEmails, "", nil, "Emails to send alerts to.")
63	AddStringSliceFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyTags, "", nil, "Tags to apply the alert against.")
64	AddStringSliceFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicyEntities, "", nil, "Entities to apply the alert against. (e.g. a droplet ID for a droplet alert policy)")
65	AddStringSliceFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicySlackChannels, "", nil, "Slack channels to send alerts to.")
66	AddStringSliceFlag(cmdAlertPolicyCreate, doctl.ArgAlertPolicySlackURLs, "", nil, "Slack URLs to send alerts to.")
67
68	cmdAlertPolicyUpdate := CmdBuilder(cmd, RunCmdAlertPolicyUpdate, "update <alert-policy-uuid>...", "Update an alert policy", `Use this command to update an existing alert policy.`, Writer)
69	AddStringFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyDescription, "", "", "A description of the alert policy.")
70	AddStringFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyType, "", "", "The type of alert policy.")
71	AddStringFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyCompare, "", "", "The comparator of the alert policy. Either `GreaterThan` or `LessThan`")
72	AddStringFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyWindow, "", "5m", "The window to apply the alert policy conditions against.")
73	AddIntFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyValue, "", 0, "The value of the alert policy to compare against.")
74	AddBoolFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyEnabled, "", true, "Whether the alert policy is enabled.")
75	AddStringSliceFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyEmails, "", nil, "Emails to send alerts to.")
76	AddStringSliceFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyTags, "", nil, "Tags to apply the alert against.")
77	AddStringSliceFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicyEntities, "", nil, "Entities to apply the alert against. (e.g. a droplet ID for a droplet alert policy)")
78	AddStringSliceFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicySlackChannels, "", nil, "Slack channels to send alerts to.")
79	AddStringSliceFlag(cmdAlertPolicyUpdate, doctl.ArgAlertPolicySlackURLs, "", nil, "Slack URLs to send alerts to.")
80
81	CmdBuilder(cmd, RunCmdAlertPolicyGet, "get <alert-policy-uuid>", "Retrieve information about an alert policy", `Use this command to retrieve an alert policy and see its configuration.`, Writer,
82		displayerType(&displayers.AlertPolicy{}))
83
84	CmdBuilder(cmd, RunCmdAlertPolicyList, "list", "List all alert policies", `Use this command to retrieve a list of all the alert policies in your account.`, Writer,
85		aliasOpt("ls"), displayerType(&displayers.AlertPolicy{}))
86
87	cmdRunAlertPolicyDelete := CmdBuilder(cmd, RunCmdAlertPolicyDelete, "delete <alert-policy-uuid>...", "Delete an alert policy", `Use this command to delete an alert policy.`, Writer)
88	AddBoolFlag(cmdRunAlertPolicyDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete an alert policy without confirmation prompt")
89
90	return cmd
91}
92
93// RunCmdAlertPolicyCreate runs alert policy create.
94func RunCmdAlertPolicyCreate(c *CmdConfig) error {
95	ms := c.Monitoring()
96
97	desc, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyDescription)
98	if err != nil {
99		return err
100	}
101
102	alertType, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyType)
103	if err != nil {
104		return err
105	}
106	err = validateAlertPolicyType(alertType)
107	if err != nil {
108		return err
109	}
110
111	value, err := c.Doit.GetInt(c.NS, doctl.ArgAlertPolicyValue)
112	if err != nil {
113		return err
114	}
115
116	window, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyWindow)
117	if err != nil {
118		return err
119	}
120	err = validateAlertPolicyWindow(window)
121	if err != nil {
122		return err
123	}
124
125	entities, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicyEntities)
126	if err != nil {
127		return err
128	}
129
130	tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicyTags)
131	if err != nil {
132		return err
133	}
134
135	enabled, err := c.Doit.GetBool(c.NS, doctl.ArgAlertPolicyEnabled)
136	if err != nil {
137		return err
138	}
139
140	compareStr, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyCompare)
141	if err != nil {
142		return err
143	}
144
145	compare, err := getComparator(compareStr)
146	if err != nil {
147		return err
148	}
149
150	emails, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicyEmails)
151	if err != nil {
152		return err
153	}
154
155	slackChannels, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicySlackChannels)
156	if err != nil {
157		return err
158	}
159
160	slackURLs, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicySlackURLs)
161	if err != nil {
162		return err
163	}
164
165	if len(slackURLs) != len(slackChannels) {
166		return errors.New("must provide the same number of slack channels as slack URLs")
167	}
168
169	if len(emails) == 0 && len(slackURLs) == 0 {
170		return errors.New("must provide either emails or slack details to send the alert to")
171	}
172
173	slacks := make([]godo.SlackDetails, len(slackChannels))
174	for i, channel := range slackChannels {
175		slacks[i] = godo.SlackDetails{Channel: channel, URL: slackURLs[i]}
176	}
177
178	apcr := &godo.AlertPolicyCreateRequest{
179		Type:        alertType,
180		Description: desc,
181		Compare:     compare,
182		Value:       float32(value),
183		Window:      window,
184		Entities:    entities,
185		Tags:        tags,
186		Alerts: godo.Alerts{
187			Slack: slacks,
188			Email: emails,
189		},
190		Enabled: &enabled,
191	}
192	p, err := ms.CreateAlertPolicy(apcr)
193	if err != nil {
194		return err
195	}
196
197	return c.Display(&displayers.AlertPolicy{AlertPolicies: do.AlertPolicies{*p}})
198}
199
200// RunCmdAlertPolicyUpdate runs alert policy update.
201func RunCmdAlertPolicyUpdate(c *CmdConfig) error {
202	err := ensureOneArg(c)
203	if err != nil {
204		return err
205	}
206
207	uuid := c.Args[0]
208
209	ms := c.Monitoring()
210
211	desc, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyDescription)
212	if err != nil {
213		return err
214	}
215
216	alertType, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyType)
217	if err != nil {
218		return err
219	}
220	err = validateAlertPolicyType(alertType)
221	if err != nil {
222		return err
223	}
224
225	value, err := c.Doit.GetInt(c.NS, doctl.ArgAlertPolicyValue)
226	if err != nil {
227		return err
228	}
229
230	window, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyWindow)
231	if err != nil {
232		return err
233	}
234	err = validateAlertPolicyWindow(window)
235	if err != nil {
236		return err
237	}
238
239	entities, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicyEntities)
240	if err != nil {
241		return err
242	}
243
244	tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicyTags)
245	if err != nil {
246		return err
247	}
248
249	enabled, err := c.Doit.GetBool(c.NS, doctl.ArgAlertPolicyEnabled)
250	if err != nil {
251		return err
252	}
253
254	compareStr, err := c.Doit.GetString(c.NS, doctl.ArgAlertPolicyCompare)
255	if err != nil {
256		return err
257	}
258
259	compare, err := getComparator(compareStr)
260	if err != nil {
261		return err
262	}
263
264	emails, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicyEmails)
265	if err != nil {
266		return err
267	}
268
269	slackChannels, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicySlackChannels)
270	if err != nil {
271		return err
272	}
273
274	slackURLs, err := c.Doit.GetStringSlice(c.NS, doctl.ArgAlertPolicySlackURLs)
275	if err != nil {
276		return err
277	}
278
279	if len(slackURLs) != len(slackChannels) {
280		return errors.New("must provide the same number of slack channels as slack URLs")
281	}
282
283	if len(emails) == 0 && len(slackURLs) == 0 {
284		return errors.New("must provide either emails or slack details to send the alert to")
285	}
286
287	slacks := make([]godo.SlackDetails, len(slackChannels))
288	for i, channel := range slackChannels {
289		slacks[i] = godo.SlackDetails{Channel: channel, URL: slackURLs[i]}
290	}
291
292	apcr := &godo.AlertPolicyUpdateRequest{
293		Type:        alertType,
294		Description: desc,
295		Compare:     compare,
296		Value:       float32(value),
297		Window:      window,
298		Entities:    entities,
299		Tags:        tags,
300		Alerts: godo.Alerts{
301			Slack: slacks,
302			Email: emails,
303		},
304		Enabled: &enabled,
305	}
306	p, err := ms.UpdateAlertPolicy(uuid, apcr)
307	if err != nil {
308		return err
309	}
310
311	return c.Display(&displayers.AlertPolicy{AlertPolicies: do.AlertPolicies{*p}})
312}
313
314func getComparator(compareStr string) (godo.AlertPolicyComp, error) {
315	var compare godo.AlertPolicyComp
316	if strings.EqualFold("LessThan", compareStr) {
317		compare = godo.LessThan
318	} else if strings.EqualFold("GreaterThan", compareStr) {
319		compare = godo.GreaterThan
320	} else {
321		return "", errors.New("comparator must be GreaterThan or LessThan")
322	}
323	return compare, nil
324}
325
326func validateAlertPolicyType(t string) error {
327	switch t {
328	case godo.DropletCPUUtilizationPercent:
329		fallthrough
330	case godo.DropletMemoryUtilizationPercent:
331		fallthrough
332	case godo.DropletDiskUtilizationPercent:
333		fallthrough
334	case godo.DropletDiskReadRate:
335		fallthrough
336	case godo.DropletDiskWriteRate:
337		fallthrough
338	case godo.DropletOneMinuteLoadAverage:
339		fallthrough
340	case godo.DropletFiveMinuteLoadAverage:
341		fallthrough
342	case godo.DropletFifteenMinuteLoadAverage:
343		fallthrough
344	case godo.DropletPublicOutboundBandwidthRate:
345		return nil
346	default:
347		return errors.New(fmt.Sprintf("'%s' is not a valid alert policy type", t))
348	}
349}
350
351func validateAlertPolicyWindow(w string) error {
352	switch w {
353	case "5m":
354		fallthrough
355	case "10m":
356		fallthrough
357	case "30m":
358		fallthrough
359	case "1h":
360		return nil
361	default:
362		return errors.New(fmt.Sprintf("'%s' is not a valid alert policy window. Must be one of '5m', '10m', '30m', or '1h'", w))
363	}
364}
365
366// RunCmdAlertPolicyGet runs alert policy get.
367func RunCmdAlertPolicyGet(c *CmdConfig) error {
368	err := ensureOneArg(c)
369	if err != nil {
370		return err
371	}
372
373	uuid := c.Args[0]
374	ms := c.Monitoring()
375	p, err := ms.GetAlertPolicy(uuid)
376	if err != nil {
377		return err
378	}
379
380	return c.Display(&displayers.AlertPolicy{AlertPolicies: do.AlertPolicies{*p}})
381}
382
383// RunCmdAlertPolicyList runs alert policy list.
384func RunCmdAlertPolicyList(c *CmdConfig) error {
385	ms := c.Monitoring()
386	policies, err := ms.ListAlertPolicies()
387	if err != nil {
388		return err
389	}
390
391	return c.Display(&displayers.AlertPolicy{AlertPolicies: policies})
392}
393
394// RunCmdAlertPolicyDelete runs alert policy delete.
395func RunCmdAlertPolicyDelete(c *CmdConfig) error {
396	if len(c.Args) < 1 {
397		return doctl.NewMissingArgsErr(c.NS)
398	}
399
400	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
401	if err != nil {
402		return err
403	}
404
405	if force || AskForConfirmDelete("alert policy", len(c.Args)) == nil {
406		for id := range c.Args {
407			uuid := c.Args[id]
408			ms := c.Monitoring()
409			if err := ms.DeleteAlertPolicy(uuid); err != nil {
410				return err
411			}
412		}
413	} else {
414		return errOperationAborted
415	}
416
417	return nil
418}
419