Repository: digitalocean/doctl
Pull request #1270 (2022-10-04 14:16:16)

Avoid plugin in serverless activations [ get | result ]
This change alters the logic for `doctl sls actv get` and `doctl sls actv result` to use the OpenWhisk API directly rather than using the plugin.   The `list` and `logs` commands are still using the plugin (to be addressed in a future PR).


Patch:
diff --git a/commands/activations.go b/commands/activations.go
index 08023933..d621fd87 100644
--- a/commands/activations.go
+++ b/commands/activations.go
@@ -14,10 +14,25 @@ limitations under the License.
 package commands
 
 import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"regexp"
+	"time"
+
+	"github.com/apache/openwhisk-client-go/whisk"
 	"github.com/digitalocean/doctl"
+	"github.com/digitalocean/doctl/commands/charm/text"
 	"github.com/spf13/cobra"
 )
 
+// ShownActivation is what is actually shown as an activation ... it adds a date field which is a human-readable
+// version of the start field.
+type ShownActivation struct {
+	whisk.Activation
+	Date string `json:"date,omitempty"`
+}
+
 // Activations generates the serverless 'activations' subtree for addition to the doctl command
 func Activations() *Command {
 	cmd := &Command{
@@ -85,12 +100,146 @@ func RunActivationsGet(c *CmdConfig) error {
 	if argCount > 1 {
 		return doctl.NewTooManyArgsErr(c.NS)
 	}
-	replaceFunctionWithAction(c)
-	output, err := RunServerlessExec(activationGet, c, []string{flagLast, flagLogs, flagResult, flagQuiet}, []string{flagSkip, flagAction})
-	if err != nil {
-		return err
+	var id string
+	if argCount > 0 {
+		id = c.Args[0]
+	}
+	logsFlag, _ := c.Doit.GetBool(c.NS, flagLogs)
+	resultFlag, _ := c.Doit.GetBool(c.NS, flagResult)
+	quietFlag, _ := c.Doit.GetBool(c.NS, flagQuiet)
+	// There is also a 'last' flag, which is historical.  Since it's behavior is the
+	// default, and the past convention was to ignore it if a single id was specified,
+	// (rather than indicating an error), it is completely ignored here but accepted for
+	// backward compatibility.  In the aio implementation (incorporated in nim, previously
+	// incorporated here), the flag had to be set explicitly (rather than just implied) in
+	// order to get a "banner" (additional informational line)  when requesting logs or
+	// result only.  This seems pointless and we will always display the banner for a
+	// single logs or result output unless --quiet is specified.
+	skipFlag, _ := c.Doit.GetInt(c.NS, flagSkip) // 0 if not there
+	functionFlag, _ := c.Doit.GetString(c.NS, flagFunction)
+	sls := c.Serverless()
+	if id == "" {
+		// If there is no id, the convention is to retrieve the last activation, subject to possible
+		// filtering or skipping
+		options := whisk.ActivationListOptions{Limit: 1, Skip: skipFlag}
+		if functionFlag != "" {
+			options.Name = functionFlag
+		}
+		list, err := sls.ListActivations(options)
+		if err != nil {
+			return err
+		}
+		if len(list) == 0 {
+			return fmt.Errorf("no activations were returned")
+		}
+		activation := list[0]
+		id = activation.ActivationID
+		if !quietFlag && (logsFlag || resultFlag) {
+			makeBanner(c.Out, activation)
+		}
+	}
+	if logsFlag {
+		activation, err := sls.GetActivationLogs(id)
+		if err != nil {
+			return err
+		}
+		if len(activation.Logs) == 0 {
+			return fmt.Errorf("no logs available")
+		}
+		printLogs(c.Out, true, activation)
+	} else if resultFlag {
+		response, err := sls.GetActivationResult(id)
+		if err != nil {
+			return err
+		}
+		if response.Result == nil {
+			return fmt.Errorf("no result available")
+		}
+		printResult(c.Out, response.Result)
+	} else {
+		activation, err := sls.GetActivation(id)
+		if err != nil {
+			return err
+		}
+		printActivationRecord(c.Out, activation)
+	}
+	return nil
+}
+
+// makeBanner is a subroutine that prints a single "banner" line summarizing information about an
+// activation.  This is done in conjunction with a request to print only logs or only the result, since,
+// otherwise, it is difficult to know what activation is being talked about.
+func makeBanner(writer io.Writer, activation whisk.Activation) {
+	end := time.UnixMilli(activation.End).Format("01/02 03:04:05")
+	init := text.NewStyled("=== ").Muted()
+	body := fmt.Sprintf("%s %s %s %s:%s", activation.ActivationID, statusToString(activation.StatusCode),
+		end, activation.Name, activation.Version)
+	msg := text.NewStyled(body).Highlight()
+	fmt.Fprintln(writer, init.String()+msg.String())
+}
+
+// statusToString converts numeric status codes to typical string
+func statusToString(statusCode int) string {
+	switch statusCode {
+	case 0:
+		return "success"
+	case 1:
+		return "application error"
+	case 2:
+		return "developer error"
+	case 3:
+		return "system error"
+	default:
+		return "??"
+	}
+}
+
+// printLog is a subroutine for printing just the logs of an activation
+func printLogs(writer io.Writer, strip bool, activation whisk.Activation) {
+	for _, log := range activation.Logs {
+		if strip {
+			log = stripLog(log)
+		}
+		fmt.Fprintln(writer, log)
 	}
-	return c.PrintServerlessTextOutput(output)
+}
+
+// dtsRegex is a regular expression that matches the prefix of some activation log entries.
+// It is used by stripLog to remove that prefix
+var dtsRegex = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:.*: `)
+
+// stripLog strips the prefix from log entries
+func stripLog(entry string) string {
+	// `2019-10-11T19:08:57.298Z       stdout: login-success ::  { code: ...`
+	// should become: `login-success ::  { code: ...`
+	found := dtsRegex.FindString(entry)
+	return entry[len(found):]
+}
+
+// printResult is a subroutine for printing just the result of an activation
+func printResult(writer io.Writer, result *whisk.Result) {
+	var msg string
+	bytes, err := json.MarshalIndent(result, "", "  ")
+	if err == nil {
+		msg = string(bytes)
+	} else {
+		msg = "<unable to represent the result as JSON>"
+	}
+	fmt.Fprintln(writer, msg)
+}
+
+// printActivationRecord is a subroutine for printing the entire activation record
+func printActivationRecord(writer io.Writer, activation whisk.Activation) {
+	var msg string
+	date := time.UnixMilli(activation.Start).Format("2006-01-02 03:04:05")
+	toShow := ShownActivation{Activation: activation, Date: date}
+	bytes, err := json.MarshalIndent(toShow, "", "  ")
+	if err == nil {
+		msg = string(bytes)
+	} else {
+		msg = "<unable to represent the activation as JSON>"
+	}
+	fmt.Fprintln(writer, msg)
 }
 
 // RunActivationsList supports the 'activations list' command
@@ -142,12 +291,53 @@ func RunActivationsResult(c *CmdConfig) error {
 	if argCount > 1 {
 		return doctl.NewTooManyArgsErr(c.NS)
 	}
-	replaceFunctionWithAction(c)
-	output, err := RunServerlessExec(activationResult, c, []string{flagLast, flagQuiet}, []string{flagLimit, flagSkip, flagAction})
-	if err != nil {
-		return err
+	var id string
+	if argCount > 0 {
+		id = c.Args[0]
 	}
-	return c.PrintServerlessTextOutput(output)
+	quietFlag, _ := c.Doit.GetBool(c.NS, flagQuiet)
+	skipFlag, _ := c.Doit.GetInt(c.NS, flagSkip)   // 0 if not there
+	limitFlag, _ := c.Doit.GetInt(c.NS, flagLimit) // 0 if not there
+	functionFlag, _ := c.Doit.GetString(c.NS, flagFunction)
+	limit := 1
+	if limitFlag > 200 {
+		limit = 200
+	} else if limitFlag > 0 {
+		limit = limitFlag
+	}
+	options := whisk.ActivationListOptions{Limit: limit, Skip: skipFlag}
+	sls := c.Serverless()
+	var activations []whisk.Activation
+	if id == "" {
+		if functionFlag != "" {
+			options.Name = functionFlag
+		}
+		actv, err := sls.ListActivations(options)
+		if err != nil {
+			return err
+		}
+		activations = actv
+	} else {
+		activations = []whisk.Activation{
+			{ActivationID: id},
+		}
+	}
+	reversed := make([]whisk.Activation, len(activations))
+	for i, activation := range activations {
+		response, err := sls.GetActivationResult(activation.ActivationID)
+		if err != nil {
+			return err
+		}
+		activation.Result = response.Result
+		reversed[len(activations)-i-1] = activation
+	}
+	for _, activation := range reversed {
+		if !quietFlag && id == "" {
+			makeBanner(c.Out, activation)
+		}
+		printResult(c.Out, activation.Result)
+	}
+	return nil
 }
 
 // replaceFunctionWithAction detects that --function was specified and renames it to --action (which is what nim
diff --git a/do/mocks/ServerlessService.go b/do/mocks/ServerlessService.go
index c6a498cc..68a24429 100644
--- a/do/mocks/ServerlessService.go
+++ b/do/mocks/ServerlessService.go
@@ -124,18 +124,49 @@ func (mr *MockServerlessServiceMockRecorder) Exec(arg0 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockServerlessService)(nil).Exec), arg0)
 }
 
-// FireTrigger mocks base method.
-func (m *MockServerlessService) FireTrigger(arg0 context.Context, arg1 string) error {
+// GetActivation mocks base method.
+func (m *MockServerlessService) GetActivation(arg0 string) (whisk.Activation, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "FireTrigger", arg0, arg1)
-	ret0, _ := ret[0].(error)
-	return ret0
+	ret := m.ctrl.Call(m, "GetActivation", arg0)
+	ret0, _ := ret[0].(whisk.Activation)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
-// FireTrigger indicates an expected call of FireTrigger.
-func (mr *MockServerlessServiceMockRecorder) FireTrigger(arg0, arg1 interface{}) *gomock.Call {
+// GetActivation indicates an expected call of GetActivation.
+func (mr *MockServerlessServiceMockRecorder) GetActivation(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FireTrigger", reflect.TypeOf((*MockServerlessService)(nil).FireTrigger), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivation", reflect.TypeOf((*MockServerlessService)(nil).GetActivation), arg0)
+}
+
+// GetActivationLogs mocks base method.
+func (m *MockServerlessService) GetActivationLogs(arg0 string) (whisk.Activation, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetActivationLogs", arg0)
+	ret0, _ := ret[0].(whisk.Activation)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetActivationLogs indicates an expected call of GetActivationLogs.
+func (mr *MockServerlessServiceMockRecorder) GetActivationLogs(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivationLogs", reflect.TypeOf((*MockServerlessService)(nil).GetActivationLogs), arg0)
+}
+
+// GetActivationResult mocks base method.
+func (m *MockServerlessService) GetActivationResult(arg0 string) (whisk.Response, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetActivationResult", arg0)
+	ret0, _ := ret[0].(whisk.Response)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetActivationResult indicates an expected call of GetActivationResult.
+func (mr *MockServerlessServiceMockRecorder) GetActivationResult(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivationResult", reflect.TypeOf((*MockServerlessService)(nil).GetActivationResult), arg0)
 }
 
 // GetConnectedAPIHost mocks base method.
@@ -287,6 +318,21 @@ func (mr *MockServerlessServiceMockRecorder) InvokeFunctionViaWeb(arg0, arg1 int
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InvokeFunctionViaWeb", reflect.TypeOf((*MockServerlessService)(nil).InvokeFunctionViaWeb), arg0, arg1)
 }
 
+// ListActivations mocks base method.
+func (m *MockServerlessService) ListActivations(arg0 whisk.ActivationListOptions) ([]whisk.Activation, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListActivations", arg0)
+	ret0, _ := ret[0].([]whisk.Activation)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListActivations indicates an expected call of ListActivations.
+func (mr *MockServerlessServiceMockRecorder) ListActivations(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListActivations", reflect.TypeOf((*MockServerlessService)(nil).ListActivations), arg0)
+}
+
 // ListFunctions mocks base method.
 func (m *MockServerlessService) ListFunctions(arg0 string, arg1, arg2 int) ([]whisk.Action, error) {
 	m.ctrl.T.Helper()
@@ -362,21 +408,6 @@ func (mr *MockServerlessServiceMockRecorder) ReadProject(arg0, arg1 interface{})
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadProject", reflect.TypeOf((*MockServerlessService)(nil).ReadProject), arg0, arg1)
 }
 
-// SetTriggerEnablement mocks base method.
-func (m *MockServerlessService) SetTriggerEnablement(arg0 context.Context, arg1 string, arg2 bool) (do.ServerlessTrigger, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "SetTriggerEnablement", arg0, arg1, arg2)
-	ret0, _ := ret[0].(do.ServerlessTrigger)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// SetTriggerEnablement indicates an expected call of SetTriggerEnablement.
-func (mr *MockServerlessServiceMockRecorder) SetTriggerEnablement(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTriggerEnablement", reflect.TypeOf((*MockServerlessService)(nil).SetTriggerEnablement), arg0, arg1, arg2)
-}
-
 // Stream mocks base method.
 func (m *MockServerlessService) Stream(arg0 *exec.Cmd) error {
 	m.ctrl.T.Helper()
diff --git a/do/serverless.go b/do/serverless.go
index 9601b227..6e2c07d9 100644
--- a/do/serverless.go
+++ b/do/serverless.go
@@ -221,6 +221,10 @@ type ServerlessService interface {
 	ListFunctions(string, int, int) ([]whisk.Action, error)
 	InvokeFunction(string, interface{}, bool, bool) (map[string]interface{}, error)
 	InvokeFunctionViaWeb(string, map[string]interface{}) error
+	ListActivations(whisk.ActivationListOptions) ([]whisk.Activation, error)
+	GetActivation(string) (whisk.Activation, error)
+	GetActivationLogs(string) (whisk.Activation, error)
+	GetActivationResult(string) (whisk.Response, error)
 	GetConnectedAPIHost() (string, error)
 	ReadProject(*ServerlessProject, []string) (ServerlessOutput, error)
 	WriteProject(ServerlessProject) (string, error)
@@ -345,6 +349,10 @@ func initWhisk(s *serverlessService) error {
 	if s.owClient != nil {
 		return nil
 	}
+	err := s.CheckServerlessStatus(HashAccessToken(s.accessToken))
+	if err != nil {
+		return err
+	}
 	creds, err := s.ReadCredentials()
 	if err != nil {
 		return err
@@ -793,6 +801,50 @@ func (s *serverlessService) InvokeFunctionViaWeb(name string, params map[string]
 	return browser.OpenURL(theURL)
 }
 
+// ListActivations drives the OpenWhisk API for listing activations
+func (s *serverlessService) ListActivations(options whisk.ActivationListOptions) ([]whisk.Activation, error) {
+	empty := []whisk.Activation{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.List(&options)
+	return resp, err
+}
+
+// GetActivation drives the OpenWhisk API getting an activation
+func (s *serverlessService) GetActivation(id string) (whisk.Activation, error) {
+	empty := whisk.Activation{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.Get(id)
+	return *resp, err
+}
+
+// GetActivationLogs drives the OpenWhisk API getting the logs of an activation
+func (s *serverlessService) GetActivationLogs(id string) (whisk.Activation, error) {
+	empty := whisk.Activation{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.Logs(id)
+	return *resp, err
+}
+
+// GetActivationResult drives the OpenWhisk API getting the result of an activation
+func (s *serverlessService) GetActivationResult(id string) (whisk.Response, error) {
+	empty := whisk.Response{}
+	err := initWhisk(s)
+	if err != nil {
+		return empty, err
+	}
+	resp, _, err := s.owClient.Activations.Result(id)
+	return *resp, err
+}
+
 // GetConnectedAPIHost retrieves the API host to which the service is currently connected
 func (s *serverlessService) GetConnectedAPIHost() (string, error) {
 	err := initWhisk(s)
@@ -1041,8 +1093,8 @@ func (s *serverlessService) ReadCredentials() (ServerlessCredentials, error) {
 	return creds, err
 }
 
-// Determines whether the serverlessUptodate appears to be connected.  The purpose is
-// to fail fast (when feasible) on sandboxes that are clearly not connected.
+// Determines whether the serverless support appears to be connected.  The purpose is
+// to fail fast (when feasible) when it clearly is not connected.
 // However, it is important not to add excessive overhead on each call (e.g.
 // asking the plugin to validate credentials), so the test is not foolproof.
 // It merely tests whether a credentials directory has been created for the
@@ -1054,15 +1106,15 @@ func isServerlessConnected(leafCredsDir string, serverlessDir string) bool {
 	return !os.IsNotExist(err)
 }
 
-// serverlessUptodate answers whether the installed version of the serverlessUptodate is at least
+// serverlessUptodate answers whether the installed version of the serverless support is at least
 // what is required by doctl
 func serverlessUptodate(serverlessDir string) bool {
 	return GetCurrentServerlessVersion(serverlessDir) >= GetMinServerlessVersion()
 }
 
-// GetCurrentServerlessVersion gets the version of the current serverless.
-// To be called only when serverless is known to exist.
-// Returns "0" if the installed serverless pre-dates the versioning system
+// GetCurrentServerlessVersion gets the version of the current plugin.
+// To be called only when the plugin is known to exist.
+// Returns "0" if the installed plugin pre-dates the versioning system
 // Otherwise, returns the version string stored in the serverless directory.
 func GetCurrentServerlessVersion(serverlessDir string) string {
 	versionFile := filepath.Join(serverlessDir, "version")


Test patch:
diff --git a/commands/activations_test.go b/commands/activations_test.go
index 53fb9a81..f463bdf5 100644
--- a/commands/activations_test.go
+++ b/commands/activations_test.go
@@ -14,10 +14,14 @@ limitations under the License.
 package commands
 
 import (
+	"bytes"
 	"os/exec"
 	"sort"
+	"strings"
 	"testing"
+	"time"
 
+	"github.com/apache/openwhisk-client-go/whisk"
 	"github.com/digitalocean/doctl/do"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -38,55 +42,238 @@ func TestActivationsCommand(t *testing.T) {
 	assert.Equal(t, expected, names)
 }
 
+// theActivations is the set of activation assumed to be present, used to mock whisk API behavior
+var theActivations = []whisk.Activation{
+	{
+		Namespace:    "my-namespace",
+		Name:         "hello1",
+		Version:      "0.0.1",
+		ActivationID: "activation-1",
+		Start:        1664538810000,
+		End:          1664538820000,
+		Response: whisk.Response{
+			Status:     "success",
+			StatusCode: 0,
+			Success:    true,
+			Result: &whisk.Result{
+				"body": "Hello stranger!",
+			},
+		},
+		Logs: []string{
+			"2022-09-30T11:53:50.567914279Z stdout: Hello stranger!",
+		},
+	},
+	{
+		Namespace:    "my-namespace",
+		Name:         "hello2",
+		Version:      "0.0.2",
+		ActivationID: "activation-2",
+		Start:        1664538830000,
+		End:          1664538840000,
+		Response: whisk.Response{
+			Status:     "success",
+			StatusCode: 0,
+			Success:    true,
+			Result: &whisk.Result{
+				"body": "Hello Archie!",
+			},
+		},
+		Logs: []string{
+			"2022-09-30T11:53:50.567914279Z stdout: Hello Archie!",
+		},
+	},
+	{
+		Namespace:    "my-namespace",
+		Name:         "hello3",
+		Version:      "0.0.3",
+		ActivationID: "activation-3",
+		Start:        1664538850000,
+		End:          1664538860000,
+		Response: whisk.Response{
+			Result: &whisk.Result{
+				"error": "Missing main/no code to execute.",
+			},
+			Status:  "developer error",
+			Success: false,
+		},
+	},
+}
+
+// Timestamps in the activations are converted to dates using local time so, to make this test capable of running
+// in any timezone, we need to abstract things a bit.  Following the conventions in aio, the banner dates are computed
+// from End and the activation record dates from Start.
+var (
+	timestamps  = []int64{1664538810000, 1664538820000, 1664538830000, 1664538840000, 1664538850000, 1664538860000}
+	actvSymbols = []string{"%START1%", "%START2%", "%START3%"}
+	actvDates   = []string{
+		time.UnixMilli(timestamps[0]).Format("2006-01-02 03:04:05"),
+		time.UnixMilli(timestamps[2]).Format("2006-01-02 03:04:05"),
+		time.UnixMilli(timestamps[4]).Format("2006-01-02 03:04:05"),
+	}
+	bannerSymbols = []string{"%END1%", "%END2%", "%END3%"}
+	bannerDates   = []string{
+		time.UnixMilli(timestamps[1]).Format("01/02 03:04:05"),
+		time.UnixMilli(timestamps[3]).Format("01/02 03:04:05"),
+		time.UnixMilli(timestamps[5]).Format("01/02 03:04:05"),
+	}
+)
+
+// convertDates operates on the expected output (containing symbols) to substitute actual dates
+func convertDates(expected string) string {
+	for i, symbol := range actvSymbols {
+		expected = strings.Replace(expected, symbol, actvDates[i], 1)
+	}
+	for i, symbol := range bannerSymbols {
+		expected = strings.Replace(expected, symbol, bannerDates[i], 1)
+	}
+	return expected
+}
+
+// findActivation finds the activation with a given id (in these tests, assumed to be present)
+func findActivation(id string) whisk.Activation {
+	for _, activation := range theActivations {
+		if activation.ActivationID == id {
+			return activation
+		}
+	}
+	// Should not happen
+	panic("could not find " + id)
+}
+
 func TestActivationsGet(t *testing.T) {
 	tests := []struct {
-		name            string
-		doctlArgs       string
-		doctlFlags      map[string]string
-		expectedNimArgs []string
+		name           string
+		doctlArgs      string
+		doctlFlags     map[string]string
+		listOptions    whisk.ActivationListOptions
+		expectedOutput string
 	}{
 		{
-			name:            "no flags with ID",
-			doctlArgs:       "activationid",
-			expectedNimArgs: []string{"activationid"},
+			name:      "no flags with ID",
+			doctlArgs: "activation-2",
+			expectedOutput: `{
+  "namespace": "my-namespace",
+  "name": "hello2",
+  "version": "0.0.2",
+  "subject": "",
+  "activationId": "activation-2",
+  "start": 1664538830000,
+  "end": 1664538840000,
+  "duration": 0,
+  "statusCode": 0,
+  "response": {
+    "status": "success",
+    "statusCode": 0,
+    "success": true,
+    "result": {
+      "body": "Hello Archie!"
+    }
+  },
+  "logs": [
+    "2022-09-30T11:53:50.567914279Z stdout: Hello Archie!"
+  ],
+  "annotations": null,
+  "date": "%START2%"
+}
+`,
 		},
 		{
-			name:            "no flags or args",
-			expectedNimArgs: []string{},
+			name:        "no flags or args",
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `{
+  "namespace": "my-namespace",
+  "name": "hello1",
+  "version": "0.0.1",
+  "subject": "",
+  "activationId": "activation-1",
+  "start": 1664538810000,
+  "end": 1664538820000,
+  "duration": 0,
+  "statusCode": 0,
+  "response": {
+    "status": "success",
+    "statusCode": 0,
+    "success": true,
+    "result": {
+      "body": "Hello stranger!"
+    }
+  },
+  "logs": [
+    "2022-09-30T11:53:50.567914279Z stdout: Hello stranger!"
+  ],
+  "annotations": null,
+  "date": "%START1%"
+}
+`,
 		},
 		{
-			name:            "last flag",
-			doctlArgs:       "activationid",
-			doctlFlags:      map[string]string{"last": ""},
-			expectedNimArgs: []string{"activationid", "--last"},
+			name:        "logs flag",
+			doctlFlags:  map[string]string{"logs": ""},
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `=== activation-1 success %END1% hello1:0.0.1
+Hello stranger!
+`,
 		},
 		{
-			name:            "logs flag",
-			doctlArgs:       "activationid",
-			doctlFlags:      map[string]string{"logs": ""},
-			expectedNimArgs: []string{"activationid", "--logs"},
+			name:        "result flag",
+			doctlFlags:  map[string]string{"result": ""},
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `=== activation-1 success %END1% hello1:0.0.1
+{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "skip flag",
-			doctlArgs:       "activationid",
-			doctlFlags:      map[string]string{"skip": "10"},
-			expectedNimArgs: []string{"activationid", "--skip", "10"},
+			name:        "skip flag",
+			doctlFlags:  map[string]string{"skip": "2"},
+			listOptions: whisk.ActivationListOptions{Limit: 1, Skip: 2},
+			expectedOutput: `{
+  "namespace": "my-namespace",
+  "name": "hello3",
+  "version": "0.0.3",
+  "subject": "",
+  "activationId": "activation-3",
+  "start": 1664538850000,
+  "end": 1664538860000,
+  "duration": 0,
+  "statusCode": 0,
+  "response": {
+    "status": "developer error",
+    "statusCode": 0,
+    "success": false,
+    "result": {
+      "error": "Missing main/no code to execute."
+    }
+  },
+  "logs": null,
+  "annotations": null,
+  "date": "%START3%"
+}
+`,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-				fakeCmd := &exec.Cmd{
-					Stdout: config.Out,
-				}
+				buf := &bytes.Buffer{}
+				config.Out = buf
 
 				if tt.doctlArgs != "" {
 					config.Args = append(config.Args, tt.doctlArgs)
 				}
 
+				logs := false
+				result := false
 				if tt.doctlFlags != nil {
 					for k, v := range tt.doctlFlags {
+						if k == "logs" {
+							logs = true
+						}
+						if k == "result" {
+							result = true
+						}
 						if v == "" {
 							config.Doit.Set(config.NS, k, true)
 						} else {
@@ -95,12 +282,29 @@ func TestActivationsGet(t *testing.T) {
 					}
 				}
 
-				tm.serverless.EXPECT().CheckServerlessStatus(hashAccessToken(config)).MinTimes(1).Return(nil)
-				tm.serverless.EXPECT().Cmd("activation/get", tt.expectedNimArgs).Return(fakeCmd, nil)
-				tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil)
+				id := tt.doctlArgs
+				var activation whisk.Activation
+				if id != "" {
+					activation = findActivation(id)
+				}
+				if tt.listOptions.Limit > 0 {
+					fst := tt.listOptions.Skip
+					lnth := tt.listOptions.Limit + fst
+					tm.serverless.EXPECT().ListActivations(tt.listOptions).Return(theActivations[fst:lnth], nil)
+					activation = theActivations[fst]
+					id = activation.ActivationID
+				}
+				if logs {
+					tm.serverless.EXPECT().GetActivationLogs(id).Return(activation, nil)
+				} else if result {
+					tm.serverless.EXPECT().GetActivationResult(id).Return(activation.Response, nil)
+				} else {
+					tm.serverless.EXPECT().GetActivation(id).Return(activation, nil)
+				}
 
 				err := RunActivationsGet(config)
 				require.NoError(t, err)
+				assert.Equal(t, convertDates(tt.expectedOutput), buf.String())
 			})
 		})
 	}
@@ -276,48 +480,73 @@ func TestActivationsLogs(t *testing.T) {
 
 func TestActivationsResult(t *testing.T) {
 	tests := []struct {
-		name            string
-		doctlArgs       string
-		doctlFlags      map[string]string
-		expectedNimArgs []string
+		name           string
+		doctlArgs      string
+		doctlFlags     map[string]string
+		listOptions    whisk.ActivationListOptions
+		expectedOutput string
 	}{
 		{
-			name:            "no flags or args",
-			expectedNimArgs: []string{},
-		},
-		{
-			name:            "no flags with ID",
-			doctlArgs:       "activationid",
-			expectedNimArgs: []string{"activationid"},
+			name:        "no flags or args",
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `=== activation-1 success %END1% hello1:0.0.1
+{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "last flag",
-			doctlFlags:      map[string]string{"last": ""},
-			expectedNimArgs: []string{"--last"},
+			name:      "no flags with ID",
+			doctlArgs: "activation-2",
+			expectedOutput: `{
+  "body": "Hello Archie!"
+}
+`,
 		},
 		{
-			name:            "limit flag",
-			doctlFlags:      map[string]string{"limit": "10"},
-			expectedNimArgs: []string{"--limit", "10"},
+			name:        "limit flag",
+			doctlFlags:  map[string]string{"limit": "10"},
+			listOptions: whisk.ActivationListOptions{Limit: 10},
+			expectedOutput: `=== activation-3 success %END3% hello3:0.0.3
+{
+  "error": "Missing main/no code to execute."
+}
+=== activation-2 success %END2% hello2:0.0.2
+{
+  "body": "Hello Archie!"
+}
+=== activation-1 success %END1% hello1:0.0.1
+{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "quiet flag",
-			doctlFlags:      map[string]string{"quiet": ""},
-			expectedNimArgs: []string{"--quiet"},
+			name:        "quiet flag",
+			doctlFlags:  map[string]string{"quiet": ""},
+			listOptions: whisk.ActivationListOptions{Limit: 1},
+			expectedOutput: `{
+  "body": "Hello stranger!"
+}
+`,
 		},
 		{
-			name:            "skip flag",
-			doctlFlags:      map[string]string{"skip": "1"},
-			expectedNimArgs: []string{"--skip", "1"},
+			name:        "skip flag",
+			doctlFlags:  map[string]string{"skip": "1"},
+			listOptions: whisk.ActivationListOptions{Limit: 1, Skip: 1},
+			expectedOutput: `=== activation-2 success %END2% hello2:0.0.2
+{
+  "body": "Hello Archie!"
+}
+`,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
-				fakeCmd := &exec.Cmd{
-					Stdout: config.Out,
-				}
+				buf := &bytes.Buffer{}
+				config.Out = buf
 
 				if tt.doctlArgs != "" {
 					config.Args = append(config.Args, tt.doctlArgs)
@@ -333,12 +562,35 @@ func TestActivationsResult(t *testing.T) {
 					}
 				}
 
-				tm.serverless.EXPECT().CheckServerlessStatus(hashAccessToken(config)).MinTimes(1).Return(nil)
-				tm.serverless.EXPECT().Cmd("activation/result", tt.expectedNimArgs).Return(fakeCmd, nil)
-				tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil)
-
+				var ids []string
+				var activations []whisk.Activation
+				if tt.doctlArgs != "" {
+					ids = []string{tt.doctlArgs}
+					activations = []whisk.Activation{findActivation(ids[0])}
+				}
+				limit := tt.listOptions.Limit
+				if limit > 0 {
+					if limit > len(theActivations) {
+						limit = len(theActivations)
+					}
+					fst := tt.listOptions.Skip
+					lnth := limit + fst
+					// The command reverses the returned list in asking for the responses
+					chosen := theActivations[fst:lnth]
+					ids = make([]string, len(chosen))
+					activations = make([]whisk.Activation, len(chosen))
+					for i, activation := range chosen {
+						activations[len(chosen)-i-1] = activation
+						ids[len(chosen)-i-1] = activation.ActivationID
+					}
+					tm.serverless.EXPECT().ListActivations(tt.listOptions).Return(chosen, nil)
+				}
+				for i, id := range ids {
+					tm.serverless.EXPECT().GetActivationResult(id).Return(activations[i].Response, nil)
+				}
 				err := RunActivationsResult(config)
 				require.NoError(t, err)
+				assert.Equal(t, convertDates(tt.expectedOutput), buf.String())
 			})
 		})
 	}
diff --git a/commands/functions_test.go b/commands/functions_test.go
index f356eb54..eb86d05f 100644
--- a/commands/functions_test.go
+++ b/commands/functions_test.go
@@ -253,7 +253,7 @@ func TestFunctionsInvoke(t *testing.T) {
 func TestFunctionsList(t *testing.T) {
 	// The displayer for function list is time-zone sensitive so we need to pre-convert the timestamps using the local
 	// time-zone to get exact matches.
-	timestamps := []int64{1662610000, 1662620000, 1662630000}
+	timestamps := []int64{1664538810000, 1664538820000, 1664538830000}
 	symbols := []string{"%DATE1%", "%DATE2%", "%DATE3%"}
 	dates := []string{
 		time.UnixMilli(timestamps[0]).Format("01/02 03:04:05"),