Repository: digitalocean/doctl
Pull request #1431 (2023-10-09 03:06:58)

feat: add uptime alerts commands
PR for https://github.com/digitalocean/doctl/issues/1419 as part of #hacktoberfest

Checklist:
- [x] merge https://github.com/digitalocean/godo/pull/637 and vendor project
- [x] write tests

mikesmithgh: Hi @danaelhe this PR is ready for review. I am leaving it in draft until its dependent PR https://github.com/digitalocean/godo/pull/637 is merged. After that is merged, I'll run the vendor command and update this PR.

It looks like I made a lot of changes because I ran `make mocks` to generate the mocks for `uptime_alerts_test.go`. A new version of gomock was released recently which changes the signature on all the mocks causing all those updates.

Please let me know what you think. Thanks!
danaelhe: > Hi @danaelhe this PR is ready for review. I am leaving it in draft until its dependent PR [digitalocean/godo#637](https://github.com/digitalocean/godo/pull/637) is merged. After that is merged, I'll run the vendor command and update this PR.
> 
> It looks like I made a lot of changes because I ran `make mocks` to generate the mocks for `uptime_alerts_test.go`. A new version of gomock was released recently which changes the signature on all the mocks causing all those updates.
> 
> Please let me know what you think. Thanks!

Awesome! I'll cut a new release of godo as well and then review this PR. That's totally fine. Thank you for the update!
mikesmithgh: > Awesome! I'll cut a new release of godo as well and then review this PR. That's totally fine. Thank you for the update!

@danaelhe Thanks! I just merged the changes and it ready for review 🙂
mikesmithgh: > Look fantastic! This is an extremely high valuable contribution- thank you so much! I just had a few nit-picks on the documentation but other than that it's ready to go 🚀

Thanks! I'll make the updates and let you know 
mikesmithgh: > > Look fantastic! This is an extremely high valuable contribution- thank you so much! I just had a few nit-picks on the documentation but other than that it's ready to go 🚀
> 
> Thanks! I'll make the updates and let you know

@danaelhe done!

Patch:
diff --git a/args.go b/args.go
index 496e1ca7..6e2499ea 100644
--- a/args.go
+++ b/args.go
@@ -251,6 +251,23 @@ const (
 	// ArgUptimeCheckEnabled is whether or not an uptime check is enabled.
 	ArgUptimeCheckEnabled = "enabled"
 
+	// ArgUptimeAlertName is the name of an uptime alert.
+	ArgUptimeAlertName = "name"
+	// ArgUptimeAlertType is the type of an uptime alert.
+	ArgUptimeAlertType = "type"
+	// ArgUptimeAlertThreshold the threshold at which an uptime alert will trigger.
+	ArgUptimeAlertThreshold = "threshold"
+	// ArgUptimeAlertComparison is the uptime alert comparator.
+	ArgUptimeAlertComparison = "comparison"
+	// ArgUptimeAlertEmails are the emails to send uptime alerts to.
+	ArgUptimeAlertEmails = "emails"
+	// ArgUptimeAlertSlackChannels are the Slack channels to send uptime alerts to.
+	ArgUptimeAlertSlackChannels = "slack-channels"
+	// ArgUptimeAlertSlackURLs are the Slack URLs to send uptime alerts to.
+	ArgUptimeAlertSlackURLs = "slack-urls"
+	// ArgUptimeAlertPeriod is the time threshold at which an uptime alert will trigger.
+	ArgUptimeAlertPeriod = "period"
+
 	// ArgVolumeSize is the size of a volume.
 	ArgVolumeSize = "size"
 	// ArgVolumeDesc is the description of a volume.
diff --git a/commands/displayers/uptime_alert.go b/commands/displayers/uptime_alert.go
new file mode 100644
index 00000000..c63ee9f2
--- /dev/null
+++ b/commands/displayers/uptime_alert.go
@@ -0,0 +1,78 @@
+/*
+Copyright 2023 The Doctl Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package displayers
+
+import (
+	"io"
+	"strings"
+
+	"github.com/digitalocean/doctl/do"
+)
+
+type UptimeAlert struct {
+	UptimeAlerts []do.UptimeAlert
+}
+
+func (uc *UptimeAlert) JSON(out io.Writer) error {
+	return writeJSON(uc.UptimeAlerts, out)
+}
+
+func (uc *UptimeAlert) Cols() []string {
+	return []string{
+		"ID", "Name", "Type", "Threshold", "Comparison", "Period", "Emails", "Slack Channels",
+	}
+}
+
+func (ua *UptimeAlert) ColMap() map[string]string {
+	return map[string]string{
+		"ID":             "ID",
+		"Name":           "Name",
+		"Type":           "Type",
+		"Threshold":      "Threshold",
+		"Comparison":     "Comparison",
+		"Period":         "Period",
+		"Emails":         "Emails",
+		"Slack Channels": "Slack Channels",
+	}
+}
+
+func (ua *UptimeAlert) KV() []map[string]interface{} {
+	out := make([]map[string]interface{}, 0, len(ua.UptimeAlerts))
+	for _, uptimeAlert := range ua.UptimeAlerts {
+		emails := ""
+		if uptimeAlert.Notifications.Email != nil {
+			emails = strings.Join(uptimeAlert.Notifications.Email, ",")
+		}
+		slackChannels := make([]string, 0)
+		if uptimeAlert.Notifications.Slack != nil {
+			for _, v := range uptimeAlert.Notifications.Slack {
+				slackChannels = append(slackChannels, v.Channel)
+			}
+		}
+		slacks := strings.Join(slackChannels, ",")
+
+		m := map[string]interface{}{
+			"ID":             uptimeAlert.ID,
+			"Name":           uptimeAlert.Name,
+			"Type":           uptimeAlert.Type,
+			"Threshold":      uptimeAlert.Threshold,
+			"Comparison":     uptimeAlert.Comparison,
+			"Period":         uptimeAlert.Period,
+			"Emails":         emails,
+			"Slack Channels": slacks,
+		}
+		out = append(out, m)
+	}
+	return out
+}
diff --git a/commands/uptime_alerts.go b/commands/uptime_alerts.go
new file mode 100644
index 00000000..d3a49696
--- /dev/null
+++ b/commands/uptime_alerts.go
@@ -0,0 +1,347 @@
+/*
+Copyright 2023 The Doctl Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/digitalocean/doctl"
+	"github.com/digitalocean/doctl/commands/displayers"
+	"github.com/digitalocean/doctl/do"
+	"github.com/digitalocean/godo"
+	"github.com/spf13/cobra"
+)
+
+// UptimeAlert creates the UptimeAlert command
+func UptimeAlert() *Command {
+	cmd := &Command{
+		Command: &cobra.Command{
+			Use:   "alert",
+			Short: "Display commands to manage uptime alerts",
+			Long: `The sub-commands of ` + "`" + `doctl uptime alert` + "`" + ` manage your uptime alerts.
+
+DigitalOcean Uptime Alerts provide the ability to monitor your endpoints from around the world,
+and alert you when they're slow, unavailable, or SSL certificates are expiring.`,
+		},
+	}
+
+	cmdUptimeAlertsCreate := CmdBuilder(cmd, RunUptimeAlertsCreate, "create <uptime-check-id>", "Create an uptime alert", `Use this command to create an uptime alert on your account.
+
+You can use flags to specify the uptime alert, type, threshold, comparison, notifications, and period.`, Writer,
+		aliasOpt("c"), displayerType(&displayers.UptimeAlert{}))
+	AddStringFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertName, "", "", "Uptime alert name", requiredOpt())
+	AddStringFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertType, "", "", "Uptime alert type, must be one of latency, down, down_global or ssl_expiry", requiredOpt())
+	AddIntFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertThreshold, "", 0, "Uptime alert threshold at which the alert will enter a trigger state. The specific threshold is dependent on the alert type.")
+	AddStringFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertComparison, "", "", "Uptime alert comparison operator used against the alert's threshold. Must be either `greater_than` or `less_than`")
+	AddStringSliceFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertEmails, "", nil, "Emails to send uptime alerts to. Must be emails associated with the DigitalOcean account")
+	AddStringSliceFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertSlackChannels, "", nil, "Slack channels to send uptime alerts to.")
+	AddStringSliceFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertSlackURLs, "", nil, "Slack URLs to send uptime alerts to.")
+	AddStringFlag(cmdUptimeAlertsCreate, doctl.ArgUptimeAlertPeriod, "", "", "Uptime alert period of time the threshold must be exceeded to trigger the alert. Must be one of 2m, 3m, 5m, 10m, 15m, 30m or 1h", requiredOpt())
+
+	CmdBuilder(cmd, RunUptimeAlertsGet, "get <uptime-check-id> <uptime-alert-id>", "Get uptime alert", `Use this command to get an uptime alert on your account by ID.`, Writer,
+		aliasOpt("g"), displayerType(&displayers.UptimeAlert{}))
+
+	CmdBuilder(cmd, RunUptimeAlertsList, "list <uptime-check-id>", "List uptime alerts", `Use this command to list all of the uptime alerts for an uptime check.`, Writer,
+		aliasOpt("ls"), displayerType(&displayers.UptimeAlert{}))
+
+	cmdUptimeAlertsUpdate := CmdBuilder(cmd, RunUptimeAlertsUpdate, "update <uptime-check-id> <uptime-alert-id>", "Update an uptime alert", `Use this command to update an uptime alert on your account.
+
+You can use flags to specify the uptime alert, type, threshold, comparison, notifications, and period.`, Writer,
+		aliasOpt("u"), displayerType(&displayers.UptimeAlert{}))
+	AddStringFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertName, "", "", "Uptime alert name", requiredOpt())
+	AddStringFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertType, "", "", "Uptime alert type, must be one of latency, down, down_global or ssl_expiry", requiredOpt())
+	AddIntFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertThreshold, "", 0, "Uptime alert threshold at which the alert will enter a trigger state. The specific threshold is dependent on the alert type.")
+	AddStringFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertComparison, "", "", "Uptime alert comparison operator used against the alert's threshold. Must be either `greater_than` or `less_than`")
+	AddStringSliceFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertEmails, "", nil, "Emails to send uptime alerts to. Must be emails associated with the DigitalOcean account")
+	AddStringSliceFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertSlackChannels, "", nil, "Slack channels to send uptime alerts to.")
+	AddStringSliceFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertSlackURLs, "", nil, "Slack URLs to send uptime alerts to.")
+	AddStringFlag(cmdUptimeAlertsUpdate, doctl.ArgUptimeAlertPeriod, "", "", "Uptime alert period of time the threshold must be exceeded to trigger the alert. Must be one of 2m, 3m, 5m, 10m, 15m, 30m or 1h", requiredOpt())
+
+	CmdBuilder(cmd, RunUptimeAlertsDelete, "delete <uptime-check-id> <uptime-alert-id>", "Delete an uptime alert", `Use this command to delete an uptime check on your account by ID.`, Writer,
+		aliasOpt("d", "del", "rm"))
+
+	return cmd
+}
+
+func getUptimeAlertComparator(compareStr string) (godo.UptimeAlertComp, error) {
+	var compare godo.UptimeAlertComp
+	if strings.EqualFold("less_than", compareStr) {
+		compare = godo.UptimeAlertLessThan
+	} else if strings.EqualFold("greater_than", compareStr) {
+		compare = godo.UptimeAlertGreaterThan
+	} else {
+		return "", errors.New("comparator must be greater_than or less_than")
+	}
+	return compare, nil
+}
+
+func validateUptimeAlertType(t string) error {
+	validUptimeAlertTypes := map[string]struct{}{
+		"latency":     {},
+		"down":        {},
+		"down_global": {},
+		"ssl_expiry":  {},
+	}
+
+	_, ok := validUptimeAlertTypes[t]
+
+	if !ok {
+		return errors.New(fmt.Sprintf("'%s' is not a valid uptime alert type", t))
+	}
+
+	return nil
+}
+
+func validateUptimeAlertPeriod(t string) error {
+	validUptimeAlertPeriods := map[string]struct{}{
+		"2m":  {},
+		"3m":  {},
+		"5m":  {},
+		"10m": {},
+		"15m": {},
+		"30m": {},
+		"1h":  {},
+	}
+
+	_, ok := validUptimeAlertPeriods[t]
+
+	if !ok {
+		return errors.New(fmt.Sprintf("'%s' is not a valid uptime alert period", t))
+	}
+
+	return nil
+}
+
+// RunUptimeAlertsCreate creates an uptime alert.
+func RunUptimeAlertsCreate(c *CmdConfig) error {
+	if len(c.Args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	name, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertName)
+	if err != nil {
+		return err
+	}
+
+	alertType, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertType)
+	if err != nil {
+		return err
+	}
+
+	err = validateUptimeAlertType(alertType)
+	if err != nil {
+		return err
+	}
+
+	threshold, err := c.Doit.GetInt(c.NS, doctl.ArgUptimeAlertThreshold)
+	if err != nil {
+		return err
+	}
+
+	compareStr, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertComparison)
+	if err != nil {
+		return err
+	}
+	comparison, err := getUptimeAlertComparator(compareStr)
+	if err != nil {
+		return err
+	}
+
+	emails, err := c.Doit.GetStringSlice(c.NS, doctl.ArgUptimeAlertEmails)
+	if err != nil {
+		return err
+	}
+
+	slackChannels, err := c.Doit.GetStringSlice(c.NS, doctl.ArgUptimeAlertSlackChannels)
+	if err != nil {
+		return err
+	}
+
+	slackURLs, err := c.Doit.GetStringSlice(c.NS, doctl.ArgUptimeAlertSlackURLs)
+	if err != nil {
+		return err
+	}
+
+	if len(slackURLs) != len(slackChannels) {
+		return errors.New("must provide the same number of slack channels as slack URLs")
+	}
+
+	if len(emails) == 0 && len(slackURLs) == 0 {
+		return errors.New("must provide either emails or slack details to send the uptime alert to")
+	}
+
+	slacks := make([]godo.SlackDetails, len(slackChannels))
+	for i, channel := range slackChannels {
+		slacks[i] = godo.SlackDetails{Channel: channel, URL: slackURLs[i]}
+	}
+
+	period, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertPeriod)
+	if err != nil {
+		return err
+	}
+
+	err = validateUptimeAlertPeriod(period)
+	if err != nil {
+		return err
+	}
+
+	cuar := &godo.CreateUptimeAlertRequest{
+		Name:       name,
+		Type:       alertType,
+		Threshold:  threshold,
+		Comparison: comparison,
+		Notifications: &godo.Notifications{
+			Slack: slacks,
+			Email: emails,
+		},
+		Period: period,
+	}
+
+	uptimeAlert, err := c.UptimeChecks().CreateAlert(c.Args[0], cuar)
+	if err != nil {
+		return err
+	}
+
+	return c.Display(&displayers.UptimeAlert{UptimeAlerts: []do.UptimeAlert{*uptimeAlert}})
+}
+
+// RunUptimeAlertsGet gets an uptime alert by ID.
+func RunUptimeAlertsGet(c *CmdConfig) error {
+	if len(c.Args) < 2 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	uptimeAlert, err := c.UptimeChecks().GetAlert(c.Args[0], c.Args[1])
+	if err != nil {
+		return err
+	}
+	item := &displayers.UptimeAlert{UptimeAlerts: []do.UptimeAlert{*uptimeAlert}}
+	return c.Display(item)
+}
+
+// RunUptimeAlertsList returns a list of uptime alerts.
+func RunUptimeAlertsList(c *CmdConfig) error {
+	if len(c.Args) == 0 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+	uptimeAlerts, err := c.UptimeChecks().ListAlerts(c.Args[0])
+	if err != nil {
+		return err
+	}
+	item := &displayers.UptimeAlert{UptimeAlerts: uptimeAlerts}
+	return c.Display(item)
+}
+
+// RunUptimeAlertsUpdate updates an uptime alert by ID.
+func RunUptimeAlertsUpdate(c *CmdConfig) error {
+	if len(c.Args) < 2 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+
+	name, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertName)
+	if err != nil {
+		return err
+	}
+
+	alertType, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertType)
+	if err != nil {
+		return err
+	}
+
+	err = validateUptimeAlertType(alertType)
+	if err != nil {
+		return err
+	}
+
+	threshold, err := c.Doit.GetInt(c.NS, doctl.ArgUptimeAlertThreshold)
+	if err != nil {
+		return err
+	}
+
+	compareStr, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertComparison)
+	if err != nil {
+		return err
+	}
+	comparison, err := getUptimeAlertComparator(compareStr)
+	if err != nil {
+		return err
+	}
+
+	emails, err := c.Doit.GetStringSlice(c.NS, doctl.ArgUptimeAlertEmails)
+	if err != nil {
+		return err
+	}
+
+	slackChannels, err := c.Doit.GetStringSlice(c.NS, doctl.ArgUptimeAlertSlackChannels)
+	if err != nil {
+		return err
+	}
+
+	slackURLs, err := c.Doit.GetStringSlice(c.NS, doctl.ArgUptimeAlertSlackURLs)
+	if err != nil {
+		return err
+	}
+
+	if len(slackURLs) != len(slackChannels) {
+		return errors.New("must provide the same number of slack channels as slack URLs")
+	}
+
+	if len(emails) == 0 && len(slackURLs) == 0 {
+		return errors.New("must provide either emails or slack details to send the uptime alert to")
+	}
+
+	slacks := make([]godo.SlackDetails, len(slackChannels))
+	for i, channel := range slackChannels {
+		slacks[i] = godo.SlackDetails{Channel: channel, URL: slackURLs[i]}
+	}
+
+	period, err := c.Doit.GetString(c.NS, doctl.ArgUptimeAlertPeriod)
+	if err != nil {
+		return err
+	}
+
+	err = validateUptimeAlertPeriod(period)
+	if err != nil {
+		return err
+	}
+
+	uuar := &godo.UpdateUptimeAlertRequest{
+		Name:       name,
+		Type:       alertType,
+		Threshold:  threshold,
+		Comparison: comparison,
+		Notifications: &godo.Notifications{
+			Slack: slacks,
+			Email: emails,
+		},
+		Period: period,
+	}
+
+	uptimeAlert, err := c.UptimeChecks().UpdateAlert(c.Args[0], c.Args[1], uuar)
+	if err != nil {
+		return err
+	}
+
+	return c.Display(&displayers.UptimeAlert{UptimeAlerts: []do.UptimeAlert{*uptimeAlert}})
+}
+
+// RunUptimeAlertsDelete deletes an uptime alert by ID.
+func RunUptimeAlertsDelete(c *CmdConfig) error {
+	if len(c.Args) < 2 {
+		return doctl.NewMissingArgsErr(c.NS)
+	}
+	return c.UptimeChecks().DeleteAlert(c.Args[0], c.Args[1])
+}
diff --git a/commands/uptime_checks.go b/commands/uptime_checks.go
index 3998b04c..d7037ad1 100644
--- a/commands/uptime_checks.go
+++ b/commands/uptime_checks.go
@@ -64,6 +64,8 @@ You can use flags to specify an updated uptime check name, target, type, and reg
 	CmdBuilder(cmd, RunUptimeChecksDelete, "delete <uptime-check-id>", "Delete an uptime check", `Use this command to delete an uptime check on your account by ID.`, Writer,
 		aliasOpt("d", "del", "rm"))
 
+	cmd.AddCommand(UptimeAlert())
+
 	return cmd
 }
 
diff --git a/do/mocks/UptimeChecksService.go b/do/mocks/UptimeChecksService.go
index 7457eb7e..ca38f9e1 100644
--- a/do/mocks/UptimeChecksService.go
+++ b/do/mocks/UptimeChecksService.go
@@ -54,6 +54,21 @@ func (mr *MockUptimeChecksServiceMockRecorder) Create(arg0 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUptimeChecksService)(nil).Create), arg0)
 }
 
+// CreateAlert mocks base method.
+func (m *MockUptimeChecksService) CreateAlert(arg0 string, arg1 *godo.CreateUptimeAlertRequest) (*do.UptimeAlert, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateAlert", arg0, arg1)
+	ret0, _ := ret[0].(*do.UptimeAlert)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateAlert indicates an expected call of CreateAlert.
+func (mr *MockUptimeChecksServiceMockRecorder) CreateAlert(arg0, arg1 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAlert", reflect.TypeOf((*MockUptimeChecksService)(nil).CreateAlert), arg0, arg1)
+}
+
 // Delete mocks base method.
 func (m *MockUptimeChecksService) Delete(arg0 string) error {
 	m.ctrl.T.Helper()
@@ -68,6 +83,20 @@ func (mr *MockUptimeChecksServiceMockRecorder) Delete(arg0 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUptimeChecksService)(nil).Delete), arg0)
 }
 
+// DeleteAlert mocks base method.
+func (m *MockUptimeChecksService) DeleteAlert(arg0, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteAlert", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteAlert indicates an expected call of DeleteAlert.
+func (mr *MockUptimeChecksServiceMockRecorder) DeleteAlert(arg0, arg1 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAlert", reflect.TypeOf((*MockUptimeChecksService)(nil).DeleteAlert), arg0, arg1)
+}
+
 // Get mocks base method.
 func (m *MockUptimeChecksService) Get(arg0 string) (*do.UptimeCheck, error) {
 	m.ctrl.T.Helper()
@@ -83,6 +112,21 @@ func (mr *MockUptimeChecksServiceMockRecorder) Get(arg0 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockUptimeChecksService)(nil).Get), arg0)
 }
 
+// GetAlert mocks base method.
+func (m *MockUptimeChecksService) GetAlert(arg0, arg1 string) (*do.UptimeAlert, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetAlert", arg0, arg1)
+	ret0, _ := ret[0].(*do.UptimeAlert)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetAlert indicates an expected call of GetAlert.
+func (mr *MockUptimeChecksServiceMockRecorder) GetAlert(arg0, arg1 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAlert", reflect.TypeOf((*MockUptimeChecksService)(nil).GetAlert), arg0, arg1)
+}
+
 // GetState mocks base method.
 func (m *MockUptimeChecksService) GetState(arg0 string) (*do.UptimeCheckState, error) {
 	m.ctrl.T.Helper()
@@ -113,6 +157,21 @@ func (mr *MockUptimeChecksServiceMockRecorder) List() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockUptimeChecksService)(nil).List))
 }
 
+// ListAlerts mocks base method.
+func (m *MockUptimeChecksService) ListAlerts(arg0 string) ([]do.UptimeAlert, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListAlerts", arg0)
+	ret0, _ := ret[0].([]do.UptimeAlert)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListAlerts indicates an expected call of ListAlerts.
+func (mr *MockUptimeChecksServiceMockRecorder) ListAlerts(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAlerts", reflect.TypeOf((*MockUptimeChecksService)(nil).ListAlerts), arg0)
+}
+
 // Update mocks base method.
 func (m *MockUptimeChecksService) Update(arg0 string, arg1 *godo.UpdateUptimeCheckRequest) (*do.UptimeCheck, error) {
 	m.ctrl.T.Helper()
@@ -127,3 +186,18 @@ func (mr *MockUptimeChecksServiceMockRecorder) Update(arg0, arg1 any) *gomock.Ca
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUptimeChecksService)(nil).Update), arg0, arg1)
 }
+
+// UpdateAlert mocks base method.
+func (m *MockUptimeChecksService) UpdateAlert(arg0, arg1 string, arg2 *godo.UpdateUptimeAlertRequest) (*do.UptimeAlert, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateAlert", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*do.UptimeAlert)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateAlert indicates an expected call of UpdateAlert.
+func (mr *MockUptimeChecksServiceMockRecorder) UpdateAlert(arg0, arg1, arg2 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAlert", reflect.TypeOf((*MockUptimeChecksService)(nil).UpdateAlert), arg0, arg1, arg2)
+}
diff --git a/do/uptime_checks.go b/do/uptime_checks.go
index 60422691..a150efe3 100644
--- a/do/uptime_checks.go
+++ b/do/uptime_checks.go
@@ -24,12 +24,17 @@ type UptimeCheck struct {
 	*godo.UptimeCheck
 }
 
+// UptimeAlert is a wrapper for godo.UptimeAlert.
+type UptimeAlert struct {
+	*godo.UptimeAlert
+}
+
 // UptimeCheckState is a wrapper for godo.UptimeCheckState.
 type UptimeCheckState struct {
 	*godo.UptimeCheckState
 }
 
-// UptimeChecksService is an interface for interacting with DigitalOcean's volume api.
+// UptimeChecksService is an interface for interacting with DigitalOcean's uptime check api.
 type UptimeChecksService interface {
 	Create(*godo.CreateUptimeCheckRequest) (*UptimeCheck, error)
 	List() ([]UptimeCheck, error)
@@ -37,6 +42,11 @@ type UptimeChecksService interface {
 	GetState(string) (*UptimeCheckState, error)
 	Update(string, *godo.UpdateUptimeCheckRequest) (*UptimeCheck, error)
 	Delete(string) error
+	CreateAlert(string, *godo.CreateUptimeAlertRequest) (*UptimeAlert, error)
+	ListAlerts(string) ([]UptimeAlert, error)
+	GetAlert(string, string) (*UptimeAlert, error)
+	UpdateAlert(string, string, *godo.UpdateUptimeAlertRequest) (*UptimeAlert, error)
+	DeleteAlert(string, string) error
 }
 
 type uptimeChecksService struct {
@@ -116,3 +126,60 @@ func (ucs *uptimeChecksService) Delete(id string) error {
 	_, err := ucs.client.UptimeChecks.Delete(context.TODO(), id)
 	return err
 }
+
+func (ucs *uptimeChecksService) CreateAlert(id string, req *godo.CreateUptimeAlertRequest) (*UptimeAlert, error) {
+	uptimeAlert, _, err := ucs.client.UptimeChecks.CreateAlert(context.TODO(), id, req)
+	if err != nil {
+		return nil, err
+	}
+	return &UptimeAlert{uptimeAlert}, nil
+}
+
+func (ucs *uptimeChecksService) ListAlerts(id string) ([]UptimeAlert, error) {
+	f := func(opt *godo.ListOptions) ([]interface{}, *godo.Response, error) {
+		list, resp, err := ucs.client.UptimeChecks.ListAlerts(context.TODO(), id, opt)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		si := make([]interface{}, len(list))
+		for i := range list {
+			si[i] = list[i]
+		}
+
+		return si, resp, err
+	}
+
+	si, err := PaginateResp(f)
+	if err != nil {
+		return nil, err
+	}
+
+	list := make([]UptimeAlert, len(si))
+	for i := range si {
+		ua := si[i].(godo.UptimeAlert)
+		list[i] = UptimeAlert{UptimeAlert: &ua}
+	}
+	return list, nil
+}
+
+func (ucs *uptimeChecksService) GetAlert(checkID string, alertID string) (*UptimeAlert, error) {
+	uptimeAlert, _, err := ucs.client.UptimeChecks.GetAlert(context.TODO(), checkID, alertID)
+	if err != nil {
+		return nil, err
+	}
+	return &UptimeAlert{uptimeAlert}, nil
+}
+
+func (ucs *uptimeChecksService) UpdateAlert(checkID string, alertID string, req *godo.UpdateUptimeAlertRequest) (*UptimeAlert, error) {
+	uptimeAlert, _, err := ucs.client.UptimeChecks.UpdateAlert(context.TODO(), checkID, alertID, req)
+	if err != nil {
+		return nil, err
+	}
+	return &UptimeAlert{uptimeAlert}, nil
+}
+
+func (ucs *uptimeChecksService) DeleteAlert(checkID string, alertID string) error {
+	_, err := ucs.client.UptimeChecks.DeleteAlert(context.TODO(), checkID, alertID)
+	return err
+}


Test patch:
diff --git a/commands/uptime_alerts_test.go b/commands/uptime_alerts_test.go
new file mode 100644
index 00000000..a5c9f102
--- /dev/null
+++ b/commands/uptime_alerts_test.go
@@ -0,0 +1,136 @@
+/*
+Copyright 2023 The Doctl Authors All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package commands
+
+import (
+	"testing"
+
+	"github.com/digitalocean/doctl"
+	"github.com/digitalocean/doctl/do"
+	"github.com/digitalocean/godo"
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	testUptimeCheckID = "00000000-0000-4000-8000-000000000000"
+	testUptimeAlert   = do.UptimeAlert{
+		UptimeAlert: &godo.UptimeAlert{
+			ID:         "4b868c4e-4f95-4c29-a6d1-58115aa47b30",
+			Name:       "Test Alert",
+			Type:       "latency",
+			Threshold:  1000,
+			Comparison: "less_than",
+			Notifications: &godo.Notifications{
+				Email: []string{"bob@example.com", "mike@example.com"},
+				Slack: []godo.SlackDetails{{URL: "https://hooks.slack.com/services/T1234567/AAAAAAAAA/ZZZZZZ", Channel: "#alerts-test"}},
+			},
+			Period: "2m",
+		},
+	}
+	testUptimeAlertsList = []do.UptimeAlert{
+		testUptimeAlert,
+	}
+)
+
+func TestUptimeAlertCommand(t *testing.T) {
+	cmd := UptimeAlert()
+	assert.NotNil(t, cmd)
+	assertCommandNames(t, cmd, "create", "list", "get", "update", "delete")
+}
+
+func TestUptimeAlertsCreate(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tua := godo.CreateUptimeAlertRequest{
+			Name:       "Test Alert",
+			Type:       "latency",
+			Threshold:  1000,
+			Comparison: "less_than",
+			Notifications: &godo.Notifications{
+				Email: []string{"bob@example.com", "mike@example.com"},
+				Slack: []godo.SlackDetails{{URL: "https://hooks.slack.com/services/T1234567/AAAAAAAAA/ZZZZZZ", Channel: "#alerts-test"}},
+			},
+			Period: "2m",
+		}
+		tm.uptimeChecks.EXPECT().CreateAlert(testUptimeCheckID, &tua).Return(&testUptimeAlert, nil)
+
+		config.Args = append(config.Args, testUptimeCheckID)
+
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertName, "Test Alert")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertType, "latency")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertThreshold, 1000)
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertComparison, "less_than")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertEmails, []string{"bob@example.com", "mike@example.com"})
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertSlackURLs, "https://hooks.slack.com/services/T1234567/AAAAAAAAA/ZZZZZZ")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertSlackChannels, "#alerts-test")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertPeriod, "2m")
+
+		err := RunUptimeAlertsCreate(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestUptimeAlertsList(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.uptimeChecks.EXPECT().ListAlerts(testUptimeCheckID).Return(testUptimeAlertsList, nil)
+
+		config.Args = append(config.Args, testUptimeCheckID)
+
+		err := RunUptimeAlertsList(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestUptimeAlertsUpdate(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tua := godo.UpdateUptimeAlertRequest{
+			Name:       "Test Alert",
+			Type:       "latency",
+			Threshold:  2000,
+			Comparison: "greater_than",
+			Notifications: &godo.Notifications{
+				Email: []string{"bob@example.com", "mike@example.com"},
+				Slack: []godo.SlackDetails{{URL: "https://hooks.slack.com/services/T1234567/AAAAAAAAA/ZZZZZZ", Channel: "#alerts-test"}},
+			},
+			Period: "5m",
+		}
+		tm.uptimeChecks.EXPECT().UpdateAlert(testUptimeCheckID, testUptimeAlert.ID, &tua).Return(&testUptimeAlert, nil)
+
+		config.Args = append(config.Args, testUptimeCheckID)
+		config.Args = append(config.Args, testUptimeAlert.ID)
+
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertName, "Test Alert")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertType, "latency")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertThreshold, 2000)
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertComparison, "greater_than")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertEmails, []string{"bob@example.com", "mike@example.com"})
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertSlackURLs, "https://hooks.slack.com/services/T1234567/AAAAAAAAA/ZZZZZZ")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertSlackChannels, "#alerts-test")
+		config.Doit.Set(config.NS, doctl.ArgUptimeAlertPeriod, "5m")
+
+		err := RunUptimeAlertsUpdate(config)
+		assert.NoError(t, err)
+	})
+}
+
+func TestUptimeAlertsDelete(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		tm.uptimeChecks.EXPECT().DeleteAlert(testUptimeCheckID, testUptimeAlert.ID).Return(nil)
+
+		config.Args = append(config.Args, testUptimeCheckID)
+		config.Args = append(config.Args, testUptimeAlert.ID)
+
+		err := RunUptimeAlertsDelete(config)
+		assert.NoError(t, err)
+	})
+}
diff --git a/commands/uptime_checks_test.go b/commands/uptime_checks_test.go
index a12e0a49..f2c38abc 100644
--- a/commands/uptime_checks_test.go
+++ b/commands/uptime_checks_test.go
@@ -41,7 +41,7 @@ var (
 func TestUptimeCheckCommand(t *testing.T) {
 	cmd := UptimeCheck()
 	assert.NotNil(t, cmd)
-	assertCommandNames(t, cmd, "create", "list", "get", "update", "delete")
+	assertCommandNames(t, cmd, "create", "list", "get", "update", "delete", "alert")
 }
 
 func TestUptimeChecksCreate(t *testing.T) {