Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eliminate plugin usage in 'doctl sls fn invoke' #1226

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions commands/displayers/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import (
"strings"
"time"

"github.com/digitalocean/doctl/do"
"github.com/apache/openwhisk-client-go/whisk"
)

// Functions is the type of the displayer for functions list
type Functions struct {
Info []do.FunctionInfo
Info []whisk.Action
}

var _ Displayable = &Functions{}
Expand Down Expand Up @@ -67,7 +67,7 @@ func (i *Functions) KV() []map[string]interface{} {
}

// findRuntime finds the runtime string amongst the annotations of a function
func findRuntime(annots []do.Annotation) string {
func findRuntime(annots whisk.KeyValueArr) string {
for i := range annots {
if annots[i].Key == "exec" {
return annots[i].Value.(string)
Expand Down
60 changes: 41 additions & 19 deletions commands/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,18 +214,33 @@ func RunFunctionsInvoke(c *CmdConfig) error {
if err != nil {
return err
}
// Assemble args and flags except for "param"
args := getFlatArgsArray(c, []string{flagWeb, flagFull, flagNoWait, flagResult}, []string{flagParamFile})
// Add "param" with special handling if present
args, err = appendParams(c, args)
paramFile, _ := c.Doit.GetString(c.NS, flagParamFile)
paramFlags, _ := c.Doit.GetStringSlice(c.NS, flagParam)
params, err := consolidateParams(paramFile, paramFlags)
if err != nil {
return err
}
output, err := ServerlessExec(c, actionInvoke, args...)
web, _ := c.Doit.GetBool(c.NS, flagWeb)
if web {
var mapParams map[string]interface{} = nil
if params != nil {
p, ok := params.(map[string]interface{})
if !ok {
return fmt.Errorf("cannot invoke via web: parameters do not form a dictionary")
}
mapParams = p
}
return c.Serverless().InvokeFunctionViaWeb(c.Args[0], mapParams)
}
full, _ := c.Doit.GetBool(c.NS, flagFull)
noWait, _ := c.Doit.GetBool(c.NS, flagNoWait)
blocking := !noWait
result := blocking && !full
response, err := c.Serverless().InvokeFunction(c.Args[0], params, blocking, result)
if err != nil {
return err
}

output := do.ServerlessOutput{Entity: response}
return c.PrintServerlessTextOutput(output)
}

Expand Down Expand Up @@ -257,31 +272,38 @@ func RunFunctionsList(c *CmdConfig) error {
if err != nil {
return err
}
var formatted []do.FunctionInfo
var formatted []whisk.Action
err = json.Unmarshal(rawOutput, &formatted)
if err != nil {
return err
}
return c.Display(&displayers.Functions{Info: formatted})
}

// appendParams determines if there is a 'param' flag (value is a slice, elements
// of the slice should be in KEY:VALUE form), if so, transforms it into the form
// expected by 'nim' (each param is its own --param flag, KEY and VALUE are separate
// tokens). The 'args' argument is the result of getFlatArgsArray and is appended
// to.
func appendParams(c *CmdConfig, args []string) ([]string, error) {
params, err := c.Doit.GetStringSlice(c.NS, flagParam)
if err != nil || len(params) == 0 {
return args, nil // error here is not considered an error (and probably won't occur)
// consolidateParams accepts parameters from a file, the command line, or both, and consolidates all
// such parameters into a simple dictionary.
func consolidateParams(paramFile string, params []string) (interface{}, error) {
consolidated := map[string]interface{}{}
if len(paramFile) > 0 {
contents, err := os.ReadFile(paramFile)
if err != nil {
return nil, err
}
err = json.Unmarshal(contents, &consolidated)
if err != nil {
return nil, err
}
}
for _, param := range params {
parts := strings.Split(param, ":")
if len(parts) < 2 {
return args, errors.New("values for --params must have KEY:VALUE form")
return nil, fmt.Errorf("values for --params must have KEY:VALUE form")
}
parts1 := strings.Join(parts[1:], ":")
args = append(args, dashdashParam, parts[0], parts1)
consolidated[parts[0]] = parts1
}
if len(consolidated) > 0 {
return consolidated, nil
}
return args, nil
return nil, nil
}
65 changes: 34 additions & 31 deletions commands/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,50 +173,57 @@ func TestFunctionsGet(t *testing.T) {

func TestFunctionsInvoke(t *testing.T) {
tests := []struct {
name string
doctlArgs string
doctlFlags map[string]interface{}
expectedNimArgs []string
name string
doctlArgs string
doctlFlags map[string]interface{}
requestResult bool
passedParams interface{}
}{
{
name: "no flags",
doctlArgs: "hello",
expectedNimArgs: []string{"hello"},
name: "no flags",
doctlArgs: "hello",
requestResult: true,
passedParams: nil,
},
{
name: "full flag",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"full": ""},
expectedNimArgs: []string{"hello", "--full"},
name: "full flag",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"full": ""},
requestResult: false,
passedParams: nil,
},
{
name: "param flag",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"param": "name:world"},
expectedNimArgs: []string{"hello", "--param", "name", "world"},
name: "param flag",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"param": "name:world"},
requestResult: true,
passedParams: map[string]interface{}{"name": "world"},
},
{
name: "param flag list",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"param": []string{"name:world", "address:everywhere"}},
expectedNimArgs: []string{"hello", "--param", "name", "world", "--param", "address", "everywhere"},
name: "param flag list",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"param": []string{"name:world", "address:everywhere"}},
requestResult: true,
passedParams: map[string]interface{}{"name": "world", "address": "everywhere"},
},
{
name: "param flag colon-value",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"param": []string{"url:https://example.com"}},
expectedNimArgs: []string{"hello", "--param", "url", "https://example.com"},
name: "param flag colon-value",
doctlArgs: "hello",
doctlFlags: map[string]interface{}{"param": []string{"url:https://example.com"}},
requestResult: true,
passedParams: map[string]interface{}{"url": "https://example.com"},
},
}

expectedRemoteResult := map[string]interface{}{
"body": "Hello world!",
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
buf := &bytes.Buffer{}
config.Out = buf
fakeCmd := &exec.Cmd{
Stdout: config.Out,
}

config.Args = append(config.Args, tt.doctlArgs)
if tt.doctlFlags != nil {
Expand All @@ -229,11 +236,7 @@ func TestFunctionsInvoke(t *testing.T) {
}
}

tm.serverless.EXPECT().CheckServerlessStatus(hashAccessToken(config)).MinTimes(1).Return(nil)
tm.serverless.EXPECT().Cmd("action/invoke", tt.expectedNimArgs).Return(fakeCmd, nil)
tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{
Entity: map[string]interface{}{"body": "Hello world!"},
}, nil)
tm.serverless.EXPECT().InvokeFunction(tt.doctlArgs, tt.passedParams, true, tt.requestResult).Return(expectedRemoteResult, nil)
expectedOut := `{
"body": "Hello world!"
}
Expand Down
29 changes: 29 additions & 0 deletions do/mocks/ServerlessService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 60 additions & 19 deletions do/serverless.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand All @@ -31,6 +32,7 @@ import (
"github.com/digitalocean/doctl"
"github.com/digitalocean/doctl/pkg/extract"
"github.com/digitalocean/godo"
"github.com/pkg/browser"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -112,25 +114,6 @@ type ServerlessHostInfo struct {
Runtimes map[string][]ServerlessRuntime `json:"runtimes"`
}

// FunctionInfo is the type of an individual function in the output
// of doctl sls fn list. Only relevant fields are unmarshaled.
// Note: when we start replacing the sandbox plugin path with direct calls
// to backend controller operations, this will be replaced by declarations
// in the golang openwhisk client.
type FunctionInfo struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Updated int64 `json:"updated"`
Version string `json:"version"`
Annotations []Annotation `json:"annotations"`
}

// Annotation is a key/value type suitable for individual annotations
type Annotation struct {
Key string `json:"key"`
Value interface{} `json:"value"`
}

// ServerlessProject ...
type ServerlessProject struct {
ProjectPath string `json:"project_path"`
Expand Down Expand Up @@ -208,6 +191,8 @@ type ServerlessService interface {
CheckServerlessStatus(string) 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
GetConnectedAPIHost() (string, error)
ReadProject(*ServerlessProject, []string) (ServerlessOutput, error)
WriteProject(ServerlessProject) (string, error)
Expand Down Expand Up @@ -671,6 +656,62 @@ func (s *serverlessService) GetFunction(name string, fetchCode bool) (whisk.Acti
return *action, parameters, 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) {
var empty map[string]interface{}
err := initWhisk(s)
if err != nil {
return empty, err
}
resp, _, err := s.owClient.Actions.Invoke(name, params, blocking, result)
return resp, err
}

// 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 {
// Get the function so we can use its metadata in formulating the request
theFunction, _, err := s.GetFunction(name, false)
if err != nil {
return err
}
// Check that it's a web function
isWeb := false
for _, annot := range theFunction.Annotations {
if annot.Key == "web-export" {
isWeb = true
break
}
}
if !isWeb {
return fmt.Errorf("'%s' is not a web function", name)
}
// Formulate the invocation URL
host, err := s.GetConnectedAPIHost()
if err != nil {
return err
}
nsParts := strings.Split(theFunction.Namespace, "/")
namespace := nsParts[0]
pkg := "default"
if len(nsParts) > 1 {
pkg = nsParts[1]
}
theURL := fmt.Sprintf("%s/api/v1/web/%s/%s/%s", host, namespace, pkg, theFunction.Name)
// Add params, if any
if params != nil {
encoded := url.Values{}
for key, val := range params {
stringVal, ok := val.(string)
if !ok {
return fmt.Errorf("the value of '%s' is not a string; web invocation is not possible", key)
}
encoded.Add(key, stringVal)
}
theURL += "?" + encoded.Encode()
}
return browser.OpenURL(theURL)
}

// GetConnectedAPIHost retrieves the API host to which the service is currently connected
func (s *serverlessService) GetConnectedAPIHost() (string, error) {
err := initWhisk(s)
Expand Down
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ require (
sigs.k8s.io/yaml v1.2.0
)

require github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b
require (
github.com/apache/openwhisk-client-go v0.0.0-20211007130743-38709899040b
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

require (
github.com/Microsoft/go-winio v0.5.2 // indirect
Expand Down Expand Up @@ -86,7 +90,6 @@ require (
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
Expand Down
Loading