diff --git a/commands/activations.go b/commands/activations.go index b8e77eddf..2362ad6dd 100644 --- a/commands/activations.go +++ b/commands/activations.go @@ -14,10 +14,26 @@ 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/digitalocean/doctl/commands/displayers" "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{ @@ -43,14 +59,16 @@ logs.`, AddStringFlag(get, "function", "f", "", "Fetch activations for a specific function") AddBoolFlag(get, "quiet", "q", false, "Suppress last activation information header") - list := CmdBuilder(cmd, RunActivationsList, "list []", "Lists Activations for which records exist", + list := CmdBuilder(cmd, RunActivationsList, "list []", "Lists Activations for which records exist", `Use `+"`"+`doctl serverless activations list`+"`"+` to list the activation records that are present in the cloud for previously invoked functions.`, - Writer, aliasOpt("ls")) - AddStringFlag(list, "limit", "l", "", "only return LIMIT number of activations (default 30, max 200)") - AddStringFlag(list, "skip", "s", "", "exclude the first SKIP number of activations from the result") - AddStringFlag(list, "since", "", "", "return activations with timestamps later than SINCE; measured in milliseconds since Th, 01, Jan 1970") - AddStringFlag(list, "upto", "", "", "return activations with timestamps earlier than UPTO; measured in milliseconds since Th, 01, Jan 1970") + Writer, + displayerType(&displayers.Activation{}), + aliasOpt("ls")) + AddIntFlag(list, "limit", "l", 30, "only return LIMIT number of activations (default 30, max 200)") + AddIntFlag(list, "skip", "s", 0, "exclude the first SKIP number of activations from the result") + AddIntFlag(list, "since", "", 0, "return activations with timestamps later than SINCE; measured in milliseconds since Th, 01, Jan 1970") + AddIntFlag(list, "upto", "", 0, "return activations with timestamps earlier than UPTO; measured in milliseconds since Th, 01, Jan 1970") AddBoolFlag(list, "count", "", false, "show only the total number of activations") AddBoolFlag(list, "full", "f", false, "include full activation description") @@ -85,25 +103,186 @@ 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] } - return c.PrintServerlessTextOutput(output) + 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, displayers.GetActivationStatus(activation.StatusCode), + end, activation.Name, activation.Version) + msg := text.NewStyled(body).Highlight() + fmt.Fprintln(writer, init.String()+msg.String()) +} + +// 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) + } +} + +// 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 = "" + } + 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 = "" + } + fmt.Fprintln(writer, msg) } // RunActivationsList supports the 'activations list' command func RunActivationsList(c *CmdConfig) error { argCount := len(c.Args) + if argCount > 1 { return doctl.NewTooManyArgsErr(c.NS) } - output, err := RunServerlessExec(activationList, c, []string{flagCount, flagFull}, []string{flagLimit, flagSkip, flagSince, flagUpto}) + sls := c.Serverless() + + var name string + if argCount > 0 { + name = c.Args[0] + } + + countFlags, _ := c.Doit.GetBool(c.NS, flagCount) + fullFlag, _ := c.Doit.GetBool(c.NS, flagFull) + skipFlag, _ := c.Doit.GetInt(c.NS, flagSkip) + sinceFlag, _ := c.Doit.GetInt(c.NS, flagSince) + upToFlag, _ := c.Doit.GetInt(c.NS, flagUpto) + limitFlag, _ := c.Doit.GetInt(c.NS, flagLimit) + + limit := limitFlag + if limitFlag > 200 { + limit = 200 + } + + if countFlags { + options := whisk.ActivationCountOptions{Since: int64(sinceFlag), Upto: int64(upToFlag), Name: name} + count, err := sls.GetActivationCount(options) + if err != nil { + return err + } + + if name != "" { + fmt.Fprintf(c.Out, "You have %d activations in this namespace for function %s \n", count.Activations, name) + } else { + fmt.Fprintf(c.Out, "You have %d activations in this namespace \n", count.Activations) + } + return nil + } + + options := whisk.ActivationListOptions{Limit: limit, Skip: skipFlag, Since: int64(sinceFlag), Upto: int64(upToFlag), Docs: fullFlag, Name: name} + + actv, err := sls.ListActivations(options) if err != nil { return err } - return c.PrintServerlessTextOutput(output) + + items := &displayers.Activation{Activations: actv} + if fullFlag { + return items.JSON(c.Out) + } + + return c.Display(items) } // RunActivationsLogs supports the 'activations logs' command @@ -142,12 +321,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/commands/activations_test.go b/commands/activations_test.go index 8987a5f6a..bf570a9f8 100644 --- a/commands/activations_test.go +++ b/commands/activations_test.go @@ -14,10 +14,15 @@ limitations under the License. package commands import ( + "bytes" "os/exec" "sort" + "strconv" + "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 +43,248 @@ func TestActivationsCommand(t *testing.T) { assert.Equal(t, expected, names) } +var hello1Result = whisk.Result(map[string]interface{}{ + "body": "Hello stranger!", +}) + +var hello2Result = whisk.Result(map[string]interface{}{ + "body": "Hello Archie!", +}) + +var hello3Result = whisk.Result(map[string]interface{}{ + "error": "Missing main/no code to execute.", +}) + +// 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: &hello1Result, + }, + 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: &hello2Result, + }, + 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: &hello3Result, + Status: "developer error", + Success: false, + }, + }, +} + +var theActivationCount = whisk.ActivationCount{ + Activations: 1738, +} + +// 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 +293,29 @@ func TestActivationsGet(t *testing.T) { } } - tm.serverless.EXPECT().CheckServerlessStatus().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()) }) }) } @@ -108,65 +323,68 @@ func TestActivationsGet(t *testing.T) { func TestActivationsList(t *testing.T) { tests := []struct { - name string - doctlArgs string - doctlFlags map[string]string - expectedNimArgs []string + name string + doctlArgs string + doctlFlags map[string]string }{ { - name: "no flags or args", - expectedNimArgs: []string{}, - }, - { - name: "full flag", - doctlFlags: map[string]string{"full": ""}, - expectedNimArgs: []string{"--full"}, - }, - { - name: "count flag", - doctlFlags: map[string]string{"count": ""}, - expectedNimArgs: []string{"--count"}, - }, - { - name: "limit flag", - doctlFlags: map[string]string{"limit": "10"}, - expectedNimArgs: []string{"--limit", "10"}, - }, - { - name: "since flag", - doctlFlags: map[string]string{"since": "1644866670085"}, - expectedNimArgs: []string{"--since", "1644866670085"}, + name: "no flags or args", + doctlArgs: "", }, { - name: "skip flag", - doctlFlags: map[string]string{"skip": "1"}, - expectedNimArgs: []string{"--skip", "1"}, + name: "function name argument", + doctlArgs: "my-package/hello4", }, { - name: "upto flag", - doctlFlags: map[string]string{"upto": "1644866670085"}, - expectedNimArgs: []string{"--upto", "1644866670085"}, + name: "count flag", + doctlArgs: "", + doctlFlags: map[string]string{"count": "true", "limit": "10"}, }, { - name: "multiple flags", - doctlFlags: map[string]string{"limit": "10", "count": ""}, - expectedNimArgs: []string{"--count", "--limit", "10"}, + name: "multiple flags and arg", + doctlArgs: "", + doctlFlags: map[string]string{"limit": "10", "skip": "100", "since": "1664538750000", "upto": "1664538850000"}, }, } 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) } + count := false + var limit interface{} + var since interface{} + var upto interface{} + var skip interface{} + if tt.doctlFlags != nil { for k, v := range tt.doctlFlags { + if k == "count" { + count = true + } + + if k == "limit" { + limit, _ = strconv.ParseInt(v, 0, 64) + } + + if k == "since" { + since, _ = strconv.ParseInt(v, 0, 64) + } + + if k == "upto" { + upto, _ = strconv.ParseInt(v, 0, 64) + } + + if k == "skip" { + skip, _ = strconv.ParseInt(v, 0, 64) + } + if v == "" { config.Doit.Set(config.NS, k, true) } else { @@ -175,9 +393,39 @@ func TestActivationsList(t *testing.T) { } } - tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil) - tm.serverless.EXPECT().Cmd("activation/list", tt.expectedNimArgs).Return(fakeCmd, nil) - tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil) + if count { + expectedListOptions := whisk.ActivationCountOptions{} + if since != nil { + expectedListOptions.Since = since.(int64) + } + + if upto != nil { + expectedListOptions.Upto = upto.(int64) + } + + tm.serverless.EXPECT().GetActivationCount(expectedListOptions).Return(theActivationCount, nil) + } else { + expectedListOptions := whisk.ActivationListOptions{} + if since != nil { + expectedListOptions.Since = since.(int64) + } + + if upto != nil { + expectedListOptions.Upto = upto.(int64) + } + + if len(config.Args) == 1 { + expectedListOptions.Name = config.Args[0] + } + if limit != nil { + expectedListOptions.Limit = int(limit.(int64)) + } + + if skip != nil { + expectedListOptions.Skip = int(skip.(int64)) + } + tm.serverless.EXPECT().ListActivations(expectedListOptions).Return(theActivations, nil) + } err := RunActivationsList(config) require.NoError(t, err) @@ -276,48 +524,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 +606,35 @@ func TestActivationsResult(t *testing.T) { } } - tm.serverless.EXPECT().CheckServerlessStatus().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/displayers/activations.go b/commands/displayers/activations.go new file mode 100644 index 000000000..f7d30fbf8 --- /dev/null +++ b/commands/displayers/activations.go @@ -0,0 +1,120 @@ +package displayers + +import ( + "fmt" + "io" + "strings" + "time" + + "github.com/apache/openwhisk-client-go/whisk" +) + +type Activation struct { + Activations []whisk.Activation +} + +var _ Displayable = &Activation{} + +// ColMap implements Displayable +func (a *Activation) ColMap() map[string]string { + return map[string]string{ + "Datetime": "Datetime", + "Status": "Status", + "Kind": "Kind", + "Version": "Version", + "ActivationID": "Activation ID", + "Start": "Start", + "Wait": "Wait", + "Duration": "Duration", + "Function": "Function", + } +} + +// Cols implements Displayable +func (a *Activation) Cols() []string { + return []string{ + "Datetime", + "Status", + "Kind", + "Version", + "ActivationID", + "Start", + "Wait", + "Duration", + "Function", + } +} + +// JSON implements Displayable +func (a *Activation) JSON(out io.Writer) error { + return writeJSON(a.Activations, out) +} + +// KV implements Displayable +func (a *Activation) KV() []map[string]interface{} { + out := make([]map[string]interface{}, 0, len(a.Activations)) + + for _, actv := range a.Activations { + o := map[string]interface{}{ + "Datetime": time.UnixMilli(actv.Start).Format("01/02 03:04:05"), + "Status": GetActivationStatus(actv.StatusCode), + "Kind": getActivationAnnotationValue(actv, "kind"), + "Version": actv.Version, + "ActivationID": actv.ActivationID, + "Start": getActivationStartType(actv), + "Wait": getActivationAnnotationValue(actv, "waitTime"), + "Duration": fmt.Sprintf("%dms", actv.Duration), + "Function": getActivationFunctionName(actv), + } + out = append(out, o) + } + return out +} + +func getActivationStartType(a whisk.Activation) string { + if getActivationAnnotationValue(a, "init") == "" { + return "cold" + } + return "warm" +} + +// Gets the full function name for the activation. +func getActivationFunctionName(a whisk.Activation) string { + name := a.Name + path := getActivationAnnotationValue(a, "path") + + if path == nil { + return name + } + + parts := strings.Split(path.(string), "/") + + if len(parts) == 3 { + return parts[1] + "/" + name + } + + return name +} + +func getActivationAnnotationValue(a whisk.Activation, key string) interface{} { + if a.Annotations == nil { + return nil + } + return a.Annotations.GetValue(key) +} + +// converts numeric status codes to typical string +func GetActivationStatus(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 "unknown" + } +} diff --git a/commands/functions.go b/commands/functions.go index 67244b602..987eef653 100644 --- a/commands/functions.go +++ b/commands/functions.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "os" + "sort" "strings" "github.com/apache/openwhisk-client-go/whisk" @@ -266,34 +267,48 @@ func RunFunctionsList(c *CmdConfig) error { if argCount > 1 { return doctl.NewTooManyArgsErr(c.NS) } + var pkg string + if argCount == 1 { + pkg = c.Args[0] + } // Determine if '--count' is requested since we will use simple text output in that case. // Count is mutually exclusive with the global format flag. count, _ := c.Doit.GetBool(c.NS, flagCount) if count && c.Doit.IsSet("format") { return errors.New("the --count and --format flags are mutually exclusive") } - // Add JSON flag so we can control output format - if !count { - c.Doit.Set(c.NS, flagJSON, true) - } - output, err := RunServerlessExec(actionList, c, []string{flagCount, flagNameSort, flagNameName, flagJSON}, []string{flagLimit, flagSkip}) + // Retrieve other flags + skip, _ := c.Doit.GetInt(c.NS, flagSkip) + limit, _ := c.Doit.GetInt(c.NS, flagLimit) + nameSort, _ := c.Doit.GetBool(c.NS, flagNameSort) + nameName, _ := c.Doit.GetBool(c.NS, flagNameName) + // Get information from backend + list, err := c.Serverless().ListFunctions(pkg, skip, limit) if err != nil { return err } if count { - return c.PrintServerlessTextOutput(output) + plural := "s" + are := "are" + if len(list) == 1 { + plural = "" + are = "is" + } + fmt.Fprintf(c.Out, "There %s %d function%s in this namespace.\n", are, len(list), plural) + return nil } - // Reparse the output to use a more specific type, which can then be passed to the displayer - rawOutput, err := json.Marshal(output.Entity) - if err != nil { - return err + if nameSort || nameName { + sortFunctionList(list) } - var formatted []whisk.Action - err = json.Unmarshal(rawOutput, &formatted) - if err != nil { - return err + return c.Display(&displayers.Functions{Info: list}) +} + +// sortFunctionList performs a sort of a function list (by name) +func sortFunctionList(list []whisk.Action) { + isLess := func(i, j int) bool { + return list[i].Name < list[j].Name } - return c.Display(&displayers.Functions{Info: formatted}) + sort.Slice(list, isLess) } // consolidateParams accepts parameters from a file, the command line, or both, and consolidates all diff --git a/commands/functions_test.go b/commands/functions_test.go index 1d2d98d2d..eb86d05fd 100644 --- a/commands/functions_test.go +++ b/commands/functions_test.go @@ -16,9 +16,10 @@ package commands import ( "bytes" "os" - "os/exec" "sort" + "strings" "testing" + "time" "github.com/apache/openwhisk-client-go/whisk" "github.com/digitalocean/doctl/do" @@ -250,54 +251,135 @@ 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{1664538810000, 1664538820000, 1664538830000} + symbols := []string{"%DATE1%", "%DATE2%", "%DATE3%"} + dates := []string{ + time.UnixMilli(timestamps[0]).Format("01/02 03:04:05"), + time.UnixMilli(timestamps[1]).Format("01/02 03:04:05"), + time.UnixMilli(timestamps[2]).Format("01/02 03:04:05"), + } + tests := []struct { - name string - doctlArgs string - doctlFlags map[string]string - expectedNimArgs []string + name string + doctlFlags map[string]string + doctlArg string + skip int + limit int + expectedOutput string }{ { - name: "no flags or args", - expectedNimArgs: []string{"--json"}, + name: "no flags or args", + skip: 0, + limit: 0, + expectedOutput: `%DATE1% 0.0.1 nodejs:14 daily/hello +%DATE2% 0.0.2 nodejs:14 daily/goodbye +%DATE3% 0.0.3 nodejs:14 sometimes/meAgain +`, + }, + { + name: "with package arg", + doctlArg: "daily", + skip: 0, + limit: 0, + expectedOutput: `%DATE1% 0.0.1 nodejs:14 daily/hello +%DATE2% 0.0.2 nodejs:14 daily/goodbye +`, + }, + { + name: "count flag", + doctlFlags: map[string]string{"count": ""}, + skip: 0, + limit: 0, + expectedOutput: "There are 3 functions in this namespace.\n", + }, + { + name: "limit flag", + doctlFlags: map[string]string{"limit": "1"}, + skip: 0, + limit: 1, + expectedOutput: "%DATE1% 0.0.1 nodejs:14 daily/hello\n", + }, + { + name: "name flag", + doctlFlags: map[string]string{"name": ""}, + skip: 0, + limit: 0, + expectedOutput: `%DATE2% 0.0.2 nodejs:14 daily/goodbye +%DATE1% 0.0.1 nodejs:14 daily/hello +%DATE3% 0.0.3 nodejs:14 sometimes/meAgain +`, }, { - name: "count flag", - doctlFlags: map[string]string{"count": ""}, - expectedNimArgs: []string{"--count"}, + name: "name-sort flag", + doctlFlags: map[string]string{"name-sort": ""}, + skip: 0, + limit: 0, + expectedOutput: `%DATE2% 0.0.2 nodejs:14 daily/goodbye +%DATE1% 0.0.1 nodejs:14 daily/hello +%DATE3% 0.0.3 nodejs:14 sometimes/meAgain +`, }, { - name: "limit flag", - doctlFlags: map[string]string{"limit": "1"}, - expectedNimArgs: []string{"--json", "--limit", "1"}, + name: "skip flag", + doctlFlags: map[string]string{"skip": "1"}, + skip: 1, + limit: 0, + expectedOutput: `%DATE2% 0.0.2 nodejs:14 daily/goodbye +%DATE3% 0.0.3 nodejs:14 sometimes/meAgain +`, }, + } + + theList := []whisk.Action{ { - name: "name flag", - doctlFlags: map[string]string{"name": ""}, - expectedNimArgs: []string{"--name", "--json"}, + Name: "hello", + Namespace: "theNamespace/daily", + Updated: timestamps[0], + Version: "0.0.1", + Annotations: whisk.KeyValueArr{ + whisk.KeyValue{ + Key: "exec", + Value: "nodejs:14", + }, + }, }, { - name: "name-sort flag", - doctlFlags: map[string]string{"name-sort": ""}, - expectedNimArgs: []string{"--name-sort", "--json"}, + Name: "goodbye", + Namespace: "theNamespace/daily", + Updated: timestamps[1], + Version: "0.0.2", + Annotations: whisk.KeyValueArr{ + whisk.KeyValue{ + Key: "exec", + Value: "nodejs:14", + }, + }, }, { - name: "skip flag", - doctlFlags: map[string]string{"skip": "1"}, - expectedNimArgs: []string{"--json", "--skip", "1"}, + Name: "meAgain", + Namespace: "theNamespace/sometimes", + Version: "0.0.3", + Updated: timestamps[2], + Annotations: whisk.KeyValueArr{ + whisk.KeyValue{ + Key: "exec", + Value: "nodejs:14", + }, + }, }, } 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) + if tt.doctlArg != "" { + config.Args = append(config.Args, tt.doctlArg) } - if tt.doctlFlags != nil { for k, v := range tt.doctlFlags { if v == "" { @@ -307,14 +389,40 @@ func TestFunctionsList(t *testing.T) { } } } + config.Doit.Set(config.NS, "no-header", true) - tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil) - tm.serverless.EXPECT().Cmd("action/list", tt.expectedNimArgs).Return(fakeCmd, nil) - tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{}, nil) + answer := selectPackage(theList, tt.doctlArg)[tt.skip:] + if tt.limit != 0 { + answer = answer[0:tt.limit] + } + tm.serverless.EXPECT().ListFunctions(tt.doctlArg, tt.skip, tt.limit).Return(answer, nil) err := RunFunctionsList(config) require.NoError(t, err) + expected := tt.expectedOutput + for i := range symbols { + expected = strings.Replace(expected, symbols[i], dates[i], 1) + } + assert.Equal(t, expected, buf.String()) }) }) } } + +// selectPackage is a testing support utility to trim a master list of functions by package membership +// Also ensures the array is copied, because the logic being tested may sort it in place. +func selectPackage(masterList []whisk.Action, pkg string) []whisk.Action { + if pkg == "" { + copiedList := make([]whisk.Action, len(masterList)) + copy(copiedList, masterList) + return copiedList + } + namespace := "theNamespace/" + pkg + answer := []whisk.Action{} + for _, action := range masterList { + if action.Namespace == namespace { + answer = append(answer, action) + } + } + return answer +} diff --git a/commands/serverless.go b/commands/serverless.go index fae997869..8c69e2de7 100644 --- a/commands/serverless.go +++ b/commands/serverless.go @@ -83,7 +83,7 @@ The install operation is long-running, and a network connection is required.`, CmdBuilder(cmd, RunServerlessUninstall, "uninstall", "Removes the serverless support", `Removes serverless support from `+"`"+`doctl`+"`", Writer) - CmdBuilder(cmd, RunServerlessConnect, "connect []", "Connects local serverless support to a functions namespace", + connect := CmdBuilder(cmd, RunServerlessConnect, "connect []", "Connects local serverless support to a functions namespace", `This command connects `+"`"+`doctl serverless`+"`"+` support to a functions namespace of your choice. The optional argument should be a (complete or partial) match to a namespace label or id. If there is no argument, all namespaces are matched. If the result is exactly one namespace, @@ -91,6 +91,12 @@ you are connected to it. If there are multiple namespaces, you have an opportun the one you want from a dialog. Use `+"`"+`doctl serverless namespaces`+"`"+` to create, delete, and list your namespaces.`, Writer) + // The apihost and auth flags will always be hidden. They support testing using doctl on clusters that are not in production + // and hence are unknown to the portal. + AddStringFlag(connect, "apihost", "", "", "") + AddStringFlag(connect, "auth", "", "", "") + connect.Flags().MarkHidden("apihost") + connect.Flags().MarkHidden("auth") status := CmdBuilder(cmd, RunServerlessStatus, "status", "Provide information about serverless support", `This command reports the status of serverless support and some details concerning its connected functions namespace. @@ -194,13 +200,34 @@ func RunServerlessConnect(c *CmdConfig) error { var ( err error ) + sls := c.Serverless() + + // Support the hidden capability to connect to non-production clusters to support various kinds of testing. + // The presence of 'auth' and 'apihost' flags trumps other parts of the syntax, but both must be present. + apihost, _ := c.Doit.GetString(c.NS, "apihost") + auth, _ := c.Doit.GetString(c.NS, "auth") + if len(apihost) > 0 && len(auth) > 0 { + namespace, err := sls.GetNamespaceFromCluster(apihost, auth) + if err != nil { + return err + } + credential := do.ServerlessCredential{Auth: auth} + creds := do.ServerlessCredentials{ + APIHost: apihost, + Namespace: namespace, + Credentials: map[string]map[string]do.ServerlessCredential{apihost: {namespace: credential}}, + } + return finishConnecting(sls, creds, "", c.Out) + } + if len(apihost) > 0 || len(auth) > 0 { + return fmt.Errorf("If either of 'apihost' or 'auth' is specified then both must be specified") + } + // Neither 'auth' nor 'apihost' was specified, so continue with other options. if len(c.Args) > 1 { return doctl.NewTooManyArgsErr(c.NS) } - sls := c.Serverless() - // Non-standard check for the connect command (only): it's ok to not be connected. err = sls.CheckServerlessStatus() if err != nil && err != do.ErrServerlessNotConnected { @@ -298,7 +325,8 @@ func finishConnecting(sls do.ServerlessService, creds do.ServerlessCredentials, // RunServerlessStatus gives a report on the status of the serverless (installed, up to date, connected) func RunServerlessStatus(c *CmdConfig) error { - status := c.Serverless().CheckServerlessStatus() + sls := c.Serverless() + status := sls.CheckServerlessStatus() if status == do.ErrServerlessNotInstalled { return status } @@ -321,20 +349,20 @@ func RunServerlessStatus(c *CmdConfig) error { } // Check the connected state more deeply (since this is a status command we want to // be more accurate; the connected check in checkServerlessStatus is lightweight and heuristic). - result, err := ServerlessExec(c, "auth/current", "--apihost", "--name") - if err != nil || len(result.Error) > 0 { - return do.ErrServerlessNotConnected + creds, err := sls.ReadCredentials() + if err != nil { + return nil } - if result.Entity == nil { - return errors.New("Could not retrieve information about the connected namespace") + auth := creds.Credentials[creds.APIHost][creds.Namespace].Auth + checkNS, err := sls.GetNamespaceFromCluster(creds.APIHost, auth) + if err != nil || checkNS != creds.Namespace { + return do.ErrServerlessNotConnected } - mapResult := result.Entity.(map[string]interface{}) - apiHost := mapResult["apihost"].(string) - fmt.Fprintf(c.Out, "Connected to functions namespace '%s' on API host '%s'\n", mapResult["name"], apiHost) + fmt.Fprintf(c.Out, "Connected to functions namespace '%s' on API host '%s'\n", creds.Namespace, creds.APIHost) fmt.Fprintf(c.Out, "Serverless software version is %s\n\n", do.GetMinServerlessVersion()) languages, _ := c.Doit.GetBool(c.NS, "languages") if languages { - return showLanguageInfo(c, apiHost) + return showLanguageInfo(c, creds.APIHost) } return nil } diff --git a/commands/serverless_test.go b/commands/serverless_test.go index 80f66412a..baa352d4e 100644 --- a/commands/serverless_test.go +++ b/commands/serverless_test.go @@ -127,18 +127,20 @@ func TestServerlessStatusWhenConnected(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { buf := &bytes.Buffer{} config.Out = buf - fakeCmd := &exec.Cmd{ - Stdout: config.Out, - } tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil) - tm.serverless.EXPECT().Cmd("auth/current", []string{"--apihost", "--name"}).Return(fakeCmd, nil) - tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{ - Entity: map[string]interface{}{ - "name": "hello", - "apihost": "https://api.example.com", + tm.serverless.EXPECT().ReadCredentials().Return(do.ServerlessCredentials{ + APIHost: "https://api.example.com", + Namespace: "hello", + Credentials: map[string]map[string]do.ServerlessCredential{ + "https://api.example.com": { + "hello": do.ServerlessCredential{ + Auth: "here-are-some-credentials", + }, + }, }, }, nil) + tm.serverless.EXPECT().GetNamespaceFromCluster("https://api.example.com", "here-are-some-credentials").Return("hello", nil) err := RunServerlessStatus(config) require.NoError(t, err) @@ -151,9 +153,6 @@ func TestServerlessStatusWithLanguages(t *testing.T) { buf := &bytes.Buffer{} config.Out = buf config.Doit.Set(config.NS, "languages", true) - fakeCmd := &exec.Cmd{ - Stdout: config.Out, - } fakeHostInfo := do.ServerlessHostInfo{ Runtimes: map[string][]do.ServerlessRuntime{ "go": { @@ -184,14 +183,20 @@ func TestServerlessStatusWithLanguages(t *testing.T) { ` tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil) - tm.serverless.EXPECT().Cmd("auth/current", []string{"--apihost", "--name"}).Return(fakeCmd, nil) - tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{ - Entity: map[string]interface{}{ - "name": "hello", - "apihost": "https://api.example.com", + tm.serverless.EXPECT().ReadCredentials().Return(do.ServerlessCredentials{ + APIHost: "https://api.example.com", + Namespace: "hello", + Credentials: map[string]map[string]do.ServerlessCredential{ + "https://api.example.com": { + "hello": do.ServerlessCredential{ + Auth: "here-are-some-credentials", + }, + }, }, }, nil) + tm.serverless.EXPECT().GetNamespaceFromCluster("https://api.example.com", "here-are-some-credentials").Return("hello", nil) tm.serverless.EXPECT().GetHostInfo("https://api.example.com").Return(fakeHostInfo, nil) + err := RunServerlessStatus(config) require.NoError(t, err) assert.Contains(t, buf.String(), expectedDisplay) @@ -200,15 +205,20 @@ func TestServerlessStatusWithLanguages(t *testing.T) { func TestServerlessStatusWhenNotConnected(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - fakeCmd := &exec.Cmd{ - Stdout: config.Out, - } tm.serverless.EXPECT().CheckServerlessStatus().MinTimes(1).Return(nil) - tm.serverless.EXPECT().Cmd("auth/current", []string{"--apihost", "--name"}).Return(fakeCmd, nil) - tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{ - Error: "403", + tm.serverless.EXPECT().ReadCredentials().Return(do.ServerlessCredentials{ + APIHost: "https://api.example.com", + Namespace: "hello", + Credentials: map[string]map[string]do.ServerlessCredential{ + "https://api.example.com": { + "hello": do.ServerlessCredential{ + Auth: "here-are-some-credentials", + }, + }, + }, }, nil) + tm.serverless.EXPECT().GetNamespaceFromCluster("https://api.example.com", "here-are-some-credentials").Return("not-hello", errors.New("an error")) err := RunServerlessStatus(config) require.Error(t, err) diff --git a/do/mocks/ServerlessService.go b/do/mocks/ServerlessService.go index 4c02b965b..d56985fb3 100644 --- a/do/mocks/ServerlessService.go +++ b/do/mocks/ServerlessService.go @@ -124,6 +124,66 @@ func (mr *MockServerlessServiceMockRecorder) Exec(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockServerlessService)(nil).Exec), arg0) } +// GetActivation mocks base method. +func (m *MockServerlessService) GetActivation(arg0 string) (whisk.Activation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActivation", arg0) + ret0, _ := ret[0].(whisk.Activation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// 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, "GetActivation", reflect.TypeOf((*MockServerlessService)(nil).GetActivation), arg0) +} + +// GetActivationCount mocks base method. +func (m *MockServerlessService) GetActivationCount(arg0 whisk.ActivationCountOptions) (whisk.ActivationCount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActivationCount", arg0) + ret0, _ := ret[0].(whisk.ActivationCount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetActivationCount indicates an expected call of GetActivationCount. +func (mr *MockServerlessServiceMockRecorder) GetActivationCount(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActivationCount", reflect.TypeOf((*MockServerlessService)(nil).GetActivationCount), 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. func (m *MockServerlessService) GetConnectedAPIHost() (string, error) { m.ctrl.T.Helper() @@ -185,6 +245,21 @@ func (mr *MockServerlessServiceMockRecorder) GetNamespace(arg0, arg1 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamespace", reflect.TypeOf((*MockServerlessService)(nil).GetNamespace), arg0, arg1) } +// GetNamespaceFromCluster mocks base method. +func (m *MockServerlessService) GetNamespaceFromCluster(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNamespaceFromCluster", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNamespaceFromCluster indicates an expected call of GetNamespaceFromCluster. +func (mr *MockServerlessServiceMockRecorder) GetNamespaceFromCluster(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamespaceFromCluster", reflect.TypeOf((*MockServerlessService)(nil).GetNamespaceFromCluster), arg0, arg1) +} + // GetServerlessNamespace mocks base method. func (m *MockServerlessService) GetServerlessNamespace(arg0 context.Context) (do.ServerlessCredentials, error) { m.ctrl.T.Helper() @@ -230,10 +305,10 @@ func (mr *MockServerlessServiceMockRecorder) InstallServerless(arg0, arg1 interf } // InvokeFunction mocks base method. -func (m *MockServerlessService) InvokeFunction(arg0 string, arg1 interface{}, arg2, arg3 bool) (map[string]interface{}, error) { +func (m *MockServerlessService) InvokeFunction(arg0 string, arg1 interface{}, arg2, arg3 bool) (interface{}, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InvokeFunction", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(map[string]interface{}) + ret0, _ := ret[0].(interface{}) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -245,7 +320,7 @@ func (mr *MockServerlessServiceMockRecorder) InvokeFunction(arg0, arg1, arg2, ar } // InvokeFunctionViaWeb mocks base method. -func (m *MockServerlessService) InvokeFunctionViaWeb(arg0 string, arg1 map[string]interface{}) error { +func (m *MockServerlessService) InvokeFunctionViaWeb(arg0 string, arg1 interface{}) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InvokeFunctionViaWeb", arg0, arg1) ret0, _ := ret[0].(error) @@ -258,6 +333,36 @@ 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() + ret := m.ctrl.Call(m, "ListFunctions", arg0, arg1, arg2) + ret0, _ := ret[0].([]whisk.Action) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListFunctions indicates an expected call of ListFunctions. +func (mr *MockServerlessServiceMockRecorder) ListFunctions(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFunctions", reflect.TypeOf((*MockServerlessService)(nil).ListFunctions), arg0, arg1, arg2) +} + // ListNamespaces mocks base method. func (m *MockServerlessService) ListNamespaces(arg0 context.Context) (do.NamespaceListResponse, error) { m.ctrl.T.Helper() diff --git a/do/serverless.go b/do/serverless.go index 1fb8ecb02..fde815f7f 100644 --- a/do/serverless.go +++ b/do/serverless.go @@ -206,6 +206,7 @@ type ServerlessService interface { GetServerlessNamespace(context.Context) (ServerlessCredentials, error) ListNamespaces(context.Context) (NamespaceListResponse, error) GetNamespace(context.Context, string) (ServerlessCredentials, error) + GetNamespaceFromCluster(string, string) (string, error) CreateNamespace(context.Context, string, string) (ServerlessCredentials, error) DeleteNamespace(context.Context, string) error ListTriggers(context.Context, string) ([]ServerlessTrigger, error) @@ -217,8 +218,14 @@ type ServerlessService interface { CheckServerlessStatus() error InstallServerless(string, bool) error GetFunction(string, bool) (whisk.Action, []FunctionParameter, error) - InvokeFunction(string, interface{}, bool, bool) (map[string]interface{}, error) - InvokeFunctionViaWeb(string, map[string]interface{}) error + ListFunctions(string, int, int) ([]whisk.Action, error) + InvokeFunction(string, interface{}, bool, bool) (interface{}, error) + InvokeFunctionViaWeb(string, interface{}) error + ListActivations(whisk.ActivationListOptions) ([]whisk.Activation, error) + GetActivationCount(whisk.ActivationCountOptions) (whisk.ActivationCount, 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) @@ -343,6 +350,10 @@ func initWhisk(s *serverlessService) error { if s.owClient != nil { return nil } + err := s.CheckServerlessStatus() + if err != nil { + return err + } creds, err := s.ReadCredentials() if err != nil { return err @@ -635,6 +646,27 @@ func (s *serverlessService) GetNamespace(ctx context.Context, name string) (Serv return executeNamespaceRequest(ctx, s, req) } +// GetNamespaceFromCluster obtains the namespace that uniquely owns a valid combination of API host and "auth" +// (uuid:key). This can be used to connect to clusters not known to the portal (e.g. dev clusters) or simply +// to check that credentials are valid. +func (s *serverlessService) GetNamespaceFromCluster(APIhost string, auth string) (string, error) { + // We do not use the shared client in serverlessService for this because it uses the stored + // credentials, not the passed ones. + config := whisk.Config{Host: APIhost, AuthToken: auth} + client, err := whisk.NewClient(http.DefaultClient, &config) + if err != nil { + return "", err + } + ns, _, err := client.Namespaces.List() + if err != nil { + return "", err + } + if len(ns) != 1 { + return "", fmt.Errorf("unexpected response when validating apihost and auth") + } + return ns[0].Name, nil +} + // CreateNamespace creates a new namespace and returns its credentials, given a label and region func (s *serverlessService) CreateNamespace(ctx context.Context, label string, region string) (ServerlessCredentials, error) { reqBody := newNamespaceRequest{Namespace: inputNamespace{Label: label, Region: region}} @@ -697,8 +729,25 @@ func (s *serverlessService) GetFunction(name string, fetchCode bool) (whisk.Acti return *action, parameters, nil } +// ListFunctions lists the functions of the connected namespace +func (s *serverlessService) ListFunctions(pkg string, skip int, limit int) ([]whisk.Action, error) { + err := initWhisk(s) + if err != nil { + return []whisk.Action{}, err + } + if limit == 0 { + limit = 30 + } + options := &whisk.ActionListOptions{ + Skip: skip, + Limit: limit, + } + list, _, err := s.owClient.Actions.List(pkg, options) + return list, err +} + // InvokeFunction invokes a function via POST with authentication -func (s *serverlessService) InvokeFunction(name string, params interface{}, blocking bool, result bool) (map[string]interface{}, error) { +func (s *serverlessService) InvokeFunction(name string, params interface{}, blocking bool, result bool) (interface{}, error) { var empty map[string]interface{} err := initWhisk(s) if err != nil { @@ -709,7 +758,7 @@ func (s *serverlessService) InvokeFunction(name string, params interface{}, bloc } // InvokeFunctionViaWeb invokes a function via GET using its web URL (or error if not a web function) -func (s *serverlessService) InvokeFunctionViaWeb(name string, params map[string]interface{}) error { +func (s *serverlessService) InvokeFunctionViaWeb(name string, params interface{}) error { // Get the function so we can use its metadata in formulating the request theFunction, _, err := s.GetFunction(name, false) if err != nil { @@ -741,7 +790,7 @@ func (s *serverlessService) InvokeFunctionViaWeb(name string, params map[string] // Add params, if any if params != nil { encoded := url.Values{} - for key, val := range params { + for key, val := range params.(map[string]interface{}) { stringVal, ok := val.(string) if !ok { return fmt.Errorf("the value of '%s' is not a string; web invocation is not possible", key) @@ -753,6 +802,61 @@ 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 +} + +// GetActivationCount drives the OpenWhisk API for getting the total number of activations in namespace +func (s *serverlessService) GetActivationCount(options whisk.ActivationCountOptions) (whisk.ActivationCount, error) { + err := initWhisk(s) + empty := whisk.ActivationCount{} + if err != nil { + return empty, err + } + resp, _, err := s.owClient.Activations.Count(&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) @@ -1001,8 +1105,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 @@ -1015,15 +1119,15 @@ func isServerlessConnected(credsDir string) bool { return err == nil } -// 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") diff --git a/go.mod b/go.mod index 41a30dfb0..c4dea4a38 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( require ( github.com/MakeNowJust/heredoc v1.0.0 - github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b + github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/charmbracelet/bubbles v0.13.1-0.20220731172002-8f6516082803 github.com/charmbracelet/bubbletea v0.22.0 diff --git a/go.sum b/go.sum index 3847c1d81..51192a4d3 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b h1:E0IprB8sveEHNP3WiZB/N/3nWzljyQCIDYFg0N3geGU= -github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b/go.mod h1:SAQU4bHGJ0sg6c1vQ8ojmQKXgGaneVnexWX4+2/KMr8= +github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d h1:8sh89OGDm1tx/D/nsFwunhX90NjEPTn2k/DDLhOjexs= +github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d/go.mod h1:SAQU4bHGJ0sg6c1vQ8ojmQKXgGaneVnexWX4+2/KMr8= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= diff --git a/vendor/github.com/apache/openwhisk-client-go/whisk/action.go b/vendor/github.com/apache/openwhisk-client-go/whisk/action.go index 539656837..367323600 100644 --- a/vendor/github.com/apache/openwhisk-client-go/whisk/action.go +++ b/vendor/github.com/apache/openwhisk-client-go/whisk/action.go @@ -287,8 +287,8 @@ func (s *ActionService) Delete(actionName string) (*http.Response, error) { return resp, nil } -func (s *ActionService) Invoke(actionName string, payload interface{}, blocking bool, result bool) (map[string]interface{}, *http.Response, error) { - var res map[string]interface{} +func (s *ActionService) Invoke(actionName string, payload interface{}, blocking bool, result bool) (interface{}, *http.Response, error) { + var res interface{} // Encode resource name as a path (with no query params) before inserting it into the URI // This way any '?' chars in the name won't be treated as the beginning of the query params diff --git a/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go b/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go index fdbe40cdd..2082bfa17 100644 --- a/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go +++ b/vendor/github.com/apache/openwhisk-client-go/whisk/activation.go @@ -20,11 +20,12 @@ package whisk import ( "errors" "fmt" - "github.com/apache/openwhisk-client-go/wski18n" "net/http" "net/url" "strconv" "time" + + "github.com/apache/openwhisk-client-go/wski18n" ) type ActivationService struct { @@ -48,6 +49,10 @@ type Activation struct { Publish *bool `json:"publish,omitempty"` } +type ActivationCount struct { + Activations int64 `json:"activations"` +} + type ActivationFilteredRow struct { Row Activation HeaderFmt string @@ -61,7 +66,7 @@ type Response struct { Result *Result `json:"result,omitempty"` } -type Result map[string]interface{} +type Result interface{} type ActivationListOptions struct { Name string `url:"name,omitempty"` @@ -72,7 +77,15 @@ type ActivationListOptions struct { Docs bool `url:"docs,omitempty"` } -//MWD - This structure may no longer be needed as the log format is now a string and not JSON +type ActivationCountOptions struct { + Name string `url:"name,omitempty"` + Skip int `url:"skip"` + Since int64 `url:"since,omitempty"` + Upto int64 `url:"upto,omitempty"` + Count bool `url:"count,omitempty"` +} + +// MWD - This structure may no longer be needed as the log format is now a string and not JSON type Log struct { Log string `json:"log,omitempty"` Stream string `json:"stream,omitempty"` @@ -118,7 +131,9 @@ func TruncateStr(str string, maxlen int) string { } // ToSummaryRowString() returns a compound string of required parameters for printing -// from CLI command `wsk activation list`. +// +// from CLI command `wsk activation list`. +// // ***Method of type Sortable*** func (activation ActivationFilteredRow) ToSummaryRowString() string { s := time.Unix(0, activation.Row.Start*1000000) @@ -189,6 +204,44 @@ func (s *ActivationService) List(options *ActivationListOptions) ([]Activation, return activations, resp, nil } +func (s *ActivationService) Count(options *ActivationCountOptions) (*ActivationCount, *http.Response, error) { + // TODO :: for some reason /activations only works with "_" as namespace + s.client.Namespace = "_" + route := "activations" + + options.Count = true + routeUrl, err := addRouteOptions(route, options) + + if err != nil { + Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err) + errStr := wski18n.T("Unable to append options '{{.options}}' to URL route '{{.route}}': {{.err}}", + map[string]interface{}{"options": fmt.Sprintf("%#v", options), "route": route, "err": err}) + werr := MakeWskErrorFromWskError(errors.New(errStr), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) + return nil, nil, werr + } + + req, err := s.client.NewRequestUrl("GET", routeUrl, nil, IncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired) + if err != nil { + Debug(DbgError, "http.NewRequestUrl(GET, %s, nil, IncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired) error: '%s'\n", route, err) + errStr := wski18n.T("Unable to create HTTP request for GET '{{.route}}': {{.err}}", + map[string]interface{}{"route": route, "err": err}) + werr := MakeWskErrorFromWskError(errors.New(errStr), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) + return nil, nil, werr + } + + Debug(DbgInfo, "Sending HTTP request - URL '%s'; req %#v\n", req.URL.String(), req) + + count := new(ActivationCount) + resp, err := s.client.Do(req, &count, ExitWithSuccessOnTimeout) + + if err != nil { + Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err) + return nil, resp, err + } + + return count, resp, nil +} + func (s *ActivationService) Get(activationID string) (*Activation, *http.Response, error) { // TODO :: for some reason /activations/:id only works with "_" as namespace s.client.Namespace = "_" diff --git a/vendor/modules.txt b/vendor/modules.txt index 30bb07151..96940e8f4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -33,7 +33,7 @@ github.com/Microsoft/hcsshim/internal/vmcompute github.com/Microsoft/hcsshim/internal/wclayer github.com/Microsoft/hcsshim/internal/winapi github.com/Microsoft/hcsshim/osversion -# github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b +# github.com/apache/openwhisk-client-go v0.0.0-20221014112704-1ca897633f2d ## explicit; go 1.15 github.com/apache/openwhisk-client-go/whisk github.com/apache/openwhisk-client-go/wski18n