Skip to content

Commit

Permalink
Various improvements to 'doctl serverless' (#1290)
Browse files Browse the repository at this point in the history
* Add support for typescript in the nodejs runtime (#1225)

* Eliminate plugin usage for sls fn invoke (#1226)

* Add doctl serverless trigger support (for scheduled functions) (#1232)

* Add support for triggers

* Add lastRun field to trigger list output

* Hide commands we won't be supporting in EA day 1

* Bump deployer version to pick up bug fix

* Fix error handling in services related to triggers

Many calls were not checking for errors.

* Switch to latest API

Change both the triggers command (native to doctl) and the deployer
version (which affects the semantics of deploy/undeploy).

* Pick up latest deployer (triggers bug fix)

* Remove support for prototype API and clean up code

* Fix unit tests

* Fix misleading comment

* Remove added complexity due to successive change

* Add filtering by function when listing triggers

* Fix omitted code in DeleteTrigger

* Guard triggers get/list with status check

Otherwise, the credentials read fails with a cryptic error instead of
an informative one when you are not connected to a namespace.

* Eliminate plugin usage in 'doctl sls fn list'

This completes the elimination of plugin usage in doctl sls fn and the
functions.go source file.

* Hidden flags enabling connection to dev clusters

* Fix unit test (date handling is timezone specific)

* Eliminate call to auth/current via the plugin

* Commit changed test (screwed it up last time)

* Remove accidental re-introduction of --beta flag

I believe this happened in merge conflict resolution during the recent
rebase.

* Bump min nim version to incorporate small bug fix

Affects what happens when a failure occurs in the middle of deleting
functions and triggers together.

* Avoid plugin in serverless activations [ get | result ] (#1270)

* WIP for converting activations to direct OW flows

* Finish recoding 'activations get' in native doctl

Tests still to come

* Convert the support for sls actv result

Tests not converted yet

* Generate latet mocks

* Fix some comments

* Use more realistic timestampes

* Revise tests for new paths.  Still no output check

* Tests are now doing meaningful output comparison

Fixed some bugs found once tests were really effective

* Updates activation list command to use the whisk client instead of the nim plugin

* Adds name filter to activations list

* moved getActivationStatus to utils

* re-generates mocks

* made changes from PR review and updated the unit tests

* Updates test

* default to json output when the full flag is set

* list command takes a function name not activation name

* Updates the apache-go-client library and updates the tests to reflect the changes

* Makes updates from PR review

Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
Co-authored-by: Davi DeBarros <[email protected]>
Co-authored-by: Davi DeBarros <[email protected]>
  • Loading branch information
4 people authored Oct 21, 2022
1 parent e3bb55e commit 242b333
Show file tree
Hide file tree
Showing 14 changed files with 1,280 additions and 221 deletions.
256 changes: 238 additions & 18 deletions commands/activations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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 [<activation_name>]", "Lists Activations for which records exist",
list := CmdBuilder(cmd, RunActivationsList, "list [<function_name>]", "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")

Expand Down Expand Up @@ -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 = "<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
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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 242b333

Please sign in to comment.