Repository: digitalocean/doctl
Pull request #1305 (2022-11-07 20:30:00)

Updates the triggers API endpoint and structure to use the new API.
Updates triggers request to use new API endpoints, and updates the trigger structure. 


Patch:
diff --git a/commands/displayers/triggers.go b/commands/displayers/triggers.go
index 6c396978..69a116d2 100644
--- a/commands/displayers/triggers.go
+++ b/commands/displayers/triggers.go
@@ -15,7 +15,6 @@ package displayers
 
 import (
 	"io"
-	"time"
 
 	"github.com/digitalocean/doctl/do"
 )
@@ -52,16 +51,16 @@ func (i *Triggers) ColMap() map[string]string {
 func (i *Triggers) KV() []map[string]interface{} {
 	out := make([]map[string]interface{}, 0, len(i.List))
 	for _, ii := range i.List {
-		lastRunTime, err := time.Parse(time.RFC3339, ii.LastRun)
 		lastRun := "_"
-		if err == nil {
-			lastRun = lastRunTime.Local().Format("01/02 03:04:05")
+		if ii.ScheduledRuns != nil && ii.ScheduledRuns.LastRunAt != nil && !ii.ScheduledRuns.LastRunAt.IsZero() {
+			lastRun = ii.ScheduledRuns.LastRunAt.String()
 		}
+
 		x := map[string]interface{}{
 			"Name":     ii.Name,
-			"Cron":     ii.Cron,
+			"Cron":     ii.ScheduledDetails.Cron,
 			"Function": ii.Function,
-			"Enabled":  ii.Enabled,
+			"Enabled":  ii.IsEnabled,
 			"LastRun":  lastRun,
 		}
 		out = append(out, x)
diff --git a/do/serverless.go b/do/serverless.go
index 871416df..bda3c3cb 100644
--- a/do/serverless.go
+++ b/do/serverless.go
@@ -29,6 +29,7 @@ import (
 	"path/filepath"
 	"runtime"
 	"strings"
+	"time"
 
 	"github.com/apache/openwhisk-client-go/whisk"
 	"github.com/digitalocean/doctl"
@@ -177,13 +178,25 @@ type ServerlessTriggerGetResponse struct {
 
 // ServerlessTrigger is the form used in list and get responses by the triggers API
 type ServerlessTrigger struct {
-	Name        string      `json:"name,omitempty"`
-	Function    string      `json:"function,omitempty"`
-	Enabled     bool        `json:"is_enabled"`
-	Cron        string      `json:"cron,omitempty"`
-	Created     string      `json:"created_at,omitempty"`
-	LastRun     string      `json:"last_run_at,omitempty"`
-	RequestBody interface{} `json:"body,omitempty"`
+	Namespace        string                   `json:"namespace,omitempty"`
+	Function         string                   `json:"function,omitempty"`
+	Type             string                   `json:"type,omitempty"`
+	Name             string                   `json:"name,omitempty"`
+	IsEnabled        bool                     `json:"is_enabled,omitempty"`
+	CreatedAt        time.Time                `json:"created_at,omitempty"`
+	UpdatedAt        time.Time                `json:"updated_at,omitempty"`
+	ScheduledDetails *TriggerScheduledDetails `json:"scheduled_details,omitempty"`
+	ScheduledRuns    *TriggerScheduledRuns    `json:"scheduled_runs,omitempty"`
+}
+
+type TriggerScheduledDetails struct {
+	Cron string                 `json:"cron,omitempty"`
+	Body map[string]interface{} `json:"body,omitempty"`
+}
+
+type TriggerScheduledRuns struct {
+	LastRunAt *time.Time `json:"last_run_at,omitempty"`
+	NextRunAt *time.Time `json:"next_run_at,omitempty"`
 }
 
 // ServerlessService is an interface for interacting with the sandbox plugin,
@@ -1021,7 +1034,7 @@ func (s *serverlessService) ListTriggers(ctx context.Context, fcn string) ([]Ser
 	if err != nil {
 		return empty, err
 	}
-	path := "v2/functions/triggers/" + creds.Namespace
+	path := fmt.Sprintf("v2/functions/namespaces/%s/triggers", creds.Namespace)
 	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
 	if err != nil {
 		return empty, err
@@ -1042,26 +1055,7 @@ func (s *serverlessService) ListTriggers(ctx context.Context, fcn string) ([]Ser
 		}
 		triggers = filtered
 	}
-	return fixBaseDates(triggers), nil
-}
-
-// fixBaseDates applies fixBaseDate to an array of triggers
-func fixBaseDates(list []ServerlessTrigger) []ServerlessTrigger {
-	ans := []ServerlessTrigger{}
-	for _, trigger := range list {
-		ans = append(ans, fixBaseDate(trigger))
-	}
-	return ans
-}
-
-// fixBaseDate fixes up the LastRun field of a trigger that has never been run.
-// It should properly contain blank but sometimes contain an encoding of the base date (a string
-// starting with "000").
-func fixBaseDate(trigger ServerlessTrigger) ServerlessTrigger {
-	if strings.HasPrefix(trigger.LastRun, "000") {
-		trigger.LastRun = "_"
-	}
-	return trigger
+	return triggers, nil
 }
 
 // GetTrigger gets the contents of a trigger for display
@@ -1075,7 +1069,7 @@ func (s *serverlessService) GetTrigger(ctx context.Context, name string) (Server
 	if err != nil {
 		return empty, err
 	}
-	path := "v2/functions/trigger/" + creds.Namespace + "/" + name
+	path := fmt.Sprintf("v2/functions/namespaces/%s/triggers/%s", creds.Namespace, name)
 	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
 	if err != nil {
 		return empty, err
@@ -1085,7 +1079,7 @@ func (s *serverlessService) GetTrigger(ctx context.Context, name string) (Server
 	if err != nil {
 		return empty, err
 	}
-	return fixBaseDate(decoded.Trigger), nil
+	return decoded.Trigger, nil
 }
 
 // Delete Trigger deletes a trigger from the namespace (used when undeploying triggers explicitly,
@@ -1096,7 +1090,7 @@ func (s *serverlessService) DeleteTrigger(ctx context.Context, name string) erro
 	if err != nil {
 		return err
 	}
-	path := "v2/functions/trigger/" + creds.Namespace + "/" + name
+	path := fmt.Sprintf("v2/functions/namespaces/%s/triggers/%s", creds.Namespace, name)
 	req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
 	if err != nil {
 		return err
@@ -1105,15 +1099,6 @@ func (s *serverlessService) DeleteTrigger(ctx context.Context, name string) erro
 	return err
 }
 
-// fixupCron detects the optional seconds field and removes it
-func fixupCron(cron string) string {
-	parts := strings.Split(cron, " ")
-	if len(parts) == 6 {
-		return strings.Join(parts[1:], " ")
-	}
-	return cron
-}
-
 func readTopLevel(project *ServerlessProject) error {
 	const (
 		Config   = "project.yml"


Test patch:
diff --git a/commands/triggers_test.go b/commands/triggers_test.go
index d7576825..059d6775 100644
--- a/commands/triggers_test.go
+++ b/commands/triggers_test.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"sort"
 	"testing"
+	"time"
 
 	"github.com/digitalocean/doctl/do"
 	"github.com/stretchr/testify/assert"
@@ -45,19 +46,42 @@ func TestTriggersGet(t *testing.T) {
 		config.Out = buf
 		config.Args = append(config.Args, "aTrigger")
 
+		nextRunAt := time.Date(2022, 11, 3, 17, 3, 2, 0, time.UTC)
 		theTrigger := do.ServerlessTrigger{
-			Name:     "firePoll1",
-			Function: "misc/pollStatus",
-			Cron:     "5 * * * *",
-			Enabled:  true,
-			LastRun:  "_",
+			Name:      "firePoll1",
+			Namespace: "123-456",
+			Function:  "misc/pollStatus",
+			Type:      "SCHEDULED",
+			IsEnabled: true,
+			CreatedAt: time.Date(2022, 10, 5, 13, 46, 59, 0, time.UTC),
+			UpdatedAt: time.Date(2022, 10, 17, 18, 41, 30, 0, time.UTC),
+			ScheduledDetails: &do.TriggerScheduledDetails{
+				Cron: "5 * * * *",
+				Body: map[string]interface{}{
+					"foo": "bar",
+				},
+			},
+			ScheduledRuns: &do.TriggerScheduledRuns{
+				NextRunAt: &nextRunAt,
+			},
 		}
 		expect := `{
-  "name": "firePoll1",
+  "namespace": "123-456",
   "function": "misc/pollStatus",
+  "type": "SCHEDULED",
+  "name": "firePoll1",
   "is_enabled": true,
-  "cron": "5 * * * *",
-  "last_run_at": "_"
+  "created_at": "2022-10-05T13:46:59Z",
+  "updated_at": "2022-10-17T18:41:30Z",
+  "scheduled_details": {
+    "cron": "5 * * * *",
+    "body": {
+      "foo": "bar"
+    }
+  },
+  "scheduled_runs": {
+    "next_run_at": "2022-11-03T17:03:02Z"
+  }
 }
 `
 		tm.serverless.EXPECT().GetTrigger(context.TODO(), "aTrigger").Return(theTrigger, nil)
@@ -70,24 +94,42 @@ func TestTriggersGet(t *testing.T) {
 }
 
 func TestTriggersList(t *testing.T) {
+	lastRunAt0 := time.Date(2022, 11, 3, 17, 3, 2, 0, time.UTC)
+
 	theList := []do.ServerlessTrigger{
 		{
-			Name:     "fireGC",
-			Function: "misc/garbageCollect",
-			Cron:     "* * * * *",
-			Enabled:  true,
+			Name:      "fireGC",
+			Namespace: "123-456",
+			Function:  "misc/garbageCollect",
+			Type:      "SCHEDULED",
+			ScheduledDetails: &do.TriggerScheduledDetails{
+				Cron: "* * * * *",
+				Body: map[string]interface{}{},
+			},
+			IsEnabled: true,
+			CreatedAt: time.Date(2022, 10, 5, 13, 46, 59, 0, time.UTC),
+			UpdatedAt: time.Date(2022, 10, 17, 18, 41, 30, 0, time.UTC),
+			ScheduledRuns: &do.TriggerScheduledRuns{
+				LastRunAt: &lastRunAt0,
+			},
 		},
 		{
 			Name:     "firePoll1",
 			Function: "misc/pollStatus",
-			Cron:     "5 * * * *",
-			Enabled:  true,
+			ScheduledDetails: &do.TriggerScheduledDetails{
+				Cron: "5 * * * *",
+			},
+			IsEnabled: true,
+			CreatedAt: time.Date(2022, 10, 5, 13, 46, 59, 0, time.UTC),
+			UpdatedAt: time.Date(2022, 10, 17, 18, 41, 30, 0, time.UTC),
 		},
 		{
 			Name:     "firePoll2",
 			Function: "misc/pollStatus",
-			Cron:     "10 * * * *",
-			Enabled:  false,
+			ScheduledDetails: &do.TriggerScheduledDetails{
+				Cron: "10 * * * *",
+			},
+			IsEnabled: false,
 		},
 	}
 	tests := []struct {
@@ -103,7 +145,7 @@ func TestTriggersList(t *testing.T) {
 				"no-header": "",
 			},
 			listResult: theList,
-			expectedOutput: `fireGC       * * * * *     misc/garbageCollect    true     _
+			expectedOutput: `fireGC       * * * * *     misc/garbageCollect    true     2022-11-03 17:03:02 +0000 UTC
 firePoll1    5 * * * *     misc/pollStatus        true     _
 firePoll2    10 * * * *    misc/pollStatus        false    _
 `,