Skip to content

Commit ac179f5

Browse files
Latest improvements to serverless (#1255)
* 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. Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
1 parent 1d61603 commit ac179f5

25 files changed

+1008
-101
lines changed

commands/command_config.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init
120120
c.Apps = func() do.AppsService { return do.NewAppsService(godoClient) }
121121
c.Monitoring = func() do.MonitoringService { return do.NewMonitoringService(godoClient) }
122122
c.Serverless = func() do.ServerlessService {
123-
return do.NewServerlessService(godoClient, getServerlessDirectory(), hashAccessToken(c))
123+
return do.NewServerlessService(godoClient, getServerlessDirectory(), accessToken)
124124
}
125125

126126
return nil

commands/displayers/functions.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ import (
1818
"strings"
1919
"time"
2020

21-
"github.com/digitalocean/doctl/do"
21+
"github.com/apache/openwhisk-client-go/whisk"
2222
)
2323

2424
// Functions is the type of the displayer for functions list
2525
type Functions struct {
26-
Info []do.FunctionInfo
26+
Info []whisk.Action
2727
}
2828

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

6969
// findRuntime finds the runtime string amongst the annotations of a function
70-
func findRuntime(annots []do.Annotation) string {
70+
func findRuntime(annots whisk.KeyValueArr) string {
7171
for i := range annots {
7272
if annots[i].Key == "exec" {
7373
return annots[i].Value.(string)

commands/displayers/triggers.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2018 The Doctl Authors All rights reserved.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package displayers
15+
16+
import (
17+
"io"
18+
"time"
19+
20+
"github.com/digitalocean/doctl/do"
21+
)
22+
23+
// Triggers is the type of the displayer for triggers list
24+
type Triggers struct {
25+
List []do.ServerlessTrigger
26+
}
27+
28+
var _ Displayable = &Triggers{}
29+
30+
// JSON is the displayer JSON method specialized for triggers list
31+
func (i *Triggers) JSON(out io.Writer) error {
32+
return writeJSON(i.List, out)
33+
}
34+
35+
// Cols is the displayer Cols method specialized for triggers list
36+
func (i *Triggers) Cols() []string {
37+
return []string{"Name", "Cron", "Function", "Enabled", "LastRun"}
38+
}
39+
40+
// ColMap is the displayer ColMap method specialized for triggers list
41+
func (i *Triggers) ColMap() map[string]string {
42+
return map[string]string{
43+
"Name": "Name",
44+
"Cron": "Cron Expression",
45+
"Function": "Invokes",
46+
"Enabled": "Enabled",
47+
"LastRun": "Last Run At",
48+
}
49+
}
50+
51+
// KV is the displayer KV method specialized for triggers list
52+
func (i *Triggers) KV() []map[string]interface{} {
53+
out := make([]map[string]interface{}, 0, len(i.List))
54+
for _, ii := range i.List {
55+
lastRunTime, err := time.Parse(time.RFC3339, ii.LastRun)
56+
lastRun := "_"
57+
if err == nil {
58+
lastRun = lastRunTime.Local().Format("01/02 03:04:05")
59+
}
60+
x := map[string]interface{}{
61+
"Name": ii.Name,
62+
"Cron": ii.Cron,
63+
"Function": ii.Function,
64+
"Enabled": ii.Enabled,
65+
"LastRun": lastRun,
66+
}
67+
out = append(out, x)
68+
}
69+
70+
return out
71+
}

commands/functions.go

+41-19
Original file line numberDiff line numberDiff line change
@@ -214,18 +214,33 @@ func RunFunctionsInvoke(c *CmdConfig) error {
214214
if err != nil {
215215
return err
216216
}
217-
// Assemble args and flags except for "param"
218-
args := getFlatArgsArray(c, []string{flagWeb, flagFull, flagNoWait, flagResult}, []string{flagParamFile})
219-
// Add "param" with special handling if present
220-
args, err = appendParams(c, args)
217+
paramFile, _ := c.Doit.GetString(c.NS, flagParamFile)
218+
paramFlags, _ := c.Doit.GetStringSlice(c.NS, flagParam)
219+
params, err := consolidateParams(paramFile, paramFlags)
221220
if err != nil {
222221
return err
223222
}
224-
output, err := ServerlessExec(c, actionInvoke, args...)
223+
web, _ := c.Doit.GetBool(c.NS, flagWeb)
224+
if web {
225+
var mapParams map[string]interface{} = nil
226+
if params != nil {
227+
p, ok := params.(map[string]interface{})
228+
if !ok {
229+
return fmt.Errorf("cannot invoke via web: parameters do not form a dictionary")
230+
}
231+
mapParams = p
232+
}
233+
return c.Serverless().InvokeFunctionViaWeb(c.Args[0], mapParams)
234+
}
235+
full, _ := c.Doit.GetBool(c.NS, flagFull)
236+
noWait, _ := c.Doit.GetBool(c.NS, flagNoWait)
237+
blocking := !noWait
238+
result := blocking && !full
239+
response, err := c.Serverless().InvokeFunction(c.Args[0], params, blocking, result)
225240
if err != nil {
226241
return err
227242
}
228-
243+
output := do.ServerlessOutput{Entity: response}
229244
return c.PrintServerlessTextOutput(output)
230245
}
231246

@@ -257,31 +272,38 @@ func RunFunctionsList(c *CmdConfig) error {
257272
if err != nil {
258273
return err
259274
}
260-
var formatted []do.FunctionInfo
275+
var formatted []whisk.Action
261276
err = json.Unmarshal(rawOutput, &formatted)
262277
if err != nil {
263278
return err
264279
}
265280
return c.Display(&displayers.Functions{Info: formatted})
266281
}
267282

268-
// appendParams determines if there is a 'param' flag (value is a slice, elements
269-
// of the slice should be in KEY:VALUE form), if so, transforms it into the form
270-
// expected by 'nim' (each param is its own --param flag, KEY and VALUE are separate
271-
// tokens). The 'args' argument is the result of getFlatArgsArray and is appended
272-
// to.
273-
func appendParams(c *CmdConfig, args []string) ([]string, error) {
274-
params, err := c.Doit.GetStringSlice(c.NS, flagParam)
275-
if err != nil || len(params) == 0 {
276-
return args, nil // error here is not considered an error (and probably won't occur)
283+
// consolidateParams accepts parameters from a file, the command line, or both, and consolidates all
284+
// such parameters into a simple dictionary.
285+
func consolidateParams(paramFile string, params []string) (interface{}, error) {
286+
consolidated := map[string]interface{}{}
287+
if len(paramFile) > 0 {
288+
contents, err := os.ReadFile(paramFile)
289+
if err != nil {
290+
return nil, err
291+
}
292+
err = json.Unmarshal(contents, &consolidated)
293+
if err != nil {
294+
return nil, err
295+
}
277296
}
278297
for _, param := range params {
279298
parts := strings.Split(param, ":")
280299
if len(parts) < 2 {
281-
return args, errors.New("values for --params must have KEY:VALUE form")
300+
return nil, fmt.Errorf("values for --params must have KEY:VALUE form")
282301
}
283302
parts1 := strings.Join(parts[1:], ":")
284-
args = append(args, dashdashParam, parts[0], parts1)
303+
consolidated[parts[0]] = parts1
304+
}
305+
if len(consolidated) > 0 {
306+
return consolidated, nil
285307
}
286-
return args, nil
308+
return nil, nil
287309
}

commands/functions_test.go

+34-31
Original file line numberDiff line numberDiff line change
@@ -173,50 +173,57 @@ func TestFunctionsGet(t *testing.T) {
173173

174174
func TestFunctionsInvoke(t *testing.T) {
175175
tests := []struct {
176-
name string
177-
doctlArgs string
178-
doctlFlags map[string]interface{}
179-
expectedNimArgs []string
176+
name string
177+
doctlArgs string
178+
doctlFlags map[string]interface{}
179+
requestResult bool
180+
passedParams interface{}
180181
}{
181182
{
182-
name: "no flags",
183-
doctlArgs: "hello",
184-
expectedNimArgs: []string{"hello"},
183+
name: "no flags",
184+
doctlArgs: "hello",
185+
requestResult: true,
186+
passedParams: nil,
185187
},
186188
{
187-
name: "full flag",
188-
doctlArgs: "hello",
189-
doctlFlags: map[string]interface{}{"full": ""},
190-
expectedNimArgs: []string{"hello", "--full"},
189+
name: "full flag",
190+
doctlArgs: "hello",
191+
doctlFlags: map[string]interface{}{"full": ""},
192+
requestResult: false,
193+
passedParams: nil,
191194
},
192195
{
193-
name: "param flag",
194-
doctlArgs: "hello",
195-
doctlFlags: map[string]interface{}{"param": "name:world"},
196-
expectedNimArgs: []string{"hello", "--param", "name", "world"},
196+
name: "param flag",
197+
doctlArgs: "hello",
198+
doctlFlags: map[string]interface{}{"param": "name:world"},
199+
requestResult: true,
200+
passedParams: map[string]interface{}{"name": "world"},
197201
},
198202
{
199-
name: "param flag list",
200-
doctlArgs: "hello",
201-
doctlFlags: map[string]interface{}{"param": []string{"name:world", "address:everywhere"}},
202-
expectedNimArgs: []string{"hello", "--param", "name", "world", "--param", "address", "everywhere"},
203+
name: "param flag list",
204+
doctlArgs: "hello",
205+
doctlFlags: map[string]interface{}{"param": []string{"name:world", "address:everywhere"}},
206+
requestResult: true,
207+
passedParams: map[string]interface{}{"name": "world", "address": "everywhere"},
203208
},
204209
{
205-
name: "param flag colon-value",
206-
doctlArgs: "hello",
207-
doctlFlags: map[string]interface{}{"param": []string{"url:https://example.com"}},
208-
expectedNimArgs: []string{"hello", "--param", "url", "https://example.com"},
210+
name: "param flag colon-value",
211+
doctlArgs: "hello",
212+
doctlFlags: map[string]interface{}{"param": []string{"url:https://example.com"}},
213+
requestResult: true,
214+
passedParams: map[string]interface{}{"url": "https://example.com"},
209215
},
210216
}
211217

218+
expectedRemoteResult := map[string]interface{}{
219+
"body": "Hello world!",
220+
}
221+
212222
for _, tt := range tests {
213223
t.Run(tt.name, func(t *testing.T) {
214224
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
215225
buf := &bytes.Buffer{}
216226
config.Out = buf
217-
fakeCmd := &exec.Cmd{
218-
Stdout: config.Out,
219-
}
220227

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

232-
tm.serverless.EXPECT().CheckServerlessStatus(hashAccessToken(config)).MinTimes(1).Return(nil)
233-
tm.serverless.EXPECT().Cmd("action/invoke", tt.expectedNimArgs).Return(fakeCmd, nil)
234-
tm.serverless.EXPECT().Exec(fakeCmd).Return(do.ServerlessOutput{
235-
Entity: map[string]interface{}{"body": "Hello world!"},
236-
}, nil)
239+
tm.serverless.EXPECT().InvokeFunction(tt.doctlArgs, tt.passedParams, true, tt.requestResult).Return(expectedRemoteResult, nil)
237240
expectedOut := `{
238241
"body": "Hello world!"
239242
}

commands/serverless.go

+32-12
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,23 @@ var (
3535
// errUndeployTooFewArgs is the error returned when neither --all nor args are specified on undeploy
3636
errUndeployTooFewArgs = errors.New("either command line arguments or `--all` must be specified")
3737

38+
// errUndeployTrigPkg is the error returned when both --packages and --triggers are specified on undeploy
39+
errUndeployTrigPkg = errors.New("the `--packages` and `--triggers` flags are mutually exclusive")
40+
3841
// languageKeywords maps the backend's runtime category names to keywords accepted as languages
3942
// Note: this table has all languages for which we possess samples. Only those with currently
4043
// active runtimes will display.
4144
languageKeywords map[string][]string = map[string][]string{
42-
"nodejs": {"javascript", "js"},
43-
"deno": {"deno"},
44-
"go": {"go", "golang"},
45-
"java": {"java"},
46-
"php": {"php"},
47-
"python": {"python", "py"},
48-
"ruby": {"ruby"},
49-
"rust": {"rust"},
50-
"swift": {"swift"},
51-
"dotnet": {"csharp", "cs"},
52-
"typescript": {"typescript", "ts"},
45+
"nodejs": {"javascript", "js", "typescript", "ts"},
46+
"deno": {"deno"},
47+
"go": {"go", "golang"},
48+
"java": {"java"},
49+
"php": {"php"},
50+
"python": {"python", "py"},
51+
"ruby": {"ruby"},
52+
"rust": {"rust"},
53+
"swift": {"swift"},
54+
"dotnet": {"csharp", "cs"},
5355
}
5456
)
5557

@@ -106,11 +108,14 @@ Functions should be listed in `+"`"+`pkgName/fnName`+"`"+` form, or `+"`"+`fnNam
106108
The `+"`"+`--packages`+"`"+` flag causes arguments without slash separators to be intepreted as packages, in which case
107109
the entire packages are removed.`, Writer)
108110
AddBoolFlag(undeploy, "packages", "p", false, "interpret simple name arguments as packages")
111+
AddBoolFlag(undeploy, "triggers", "", false, "interpret all arguments as triggers")
109112
AddBoolFlag(undeploy, "all", "", false, "remove all packages and functions")
113+
undeploy.Flags().MarkHidden("triggers") // support is experimental at this point
110114

111115
cmd.AddCommand(Activations())
112116
cmd.AddCommand(Functions())
113117
cmd.AddCommand(Namespaces())
118+
cmd.AddCommand(Triggers())
114119
ServerlessExtras(cmd)
115120
return cmd
116121
}
@@ -365,21 +370,36 @@ func showLanguageInfo(c *CmdConfig, APIHost string) error {
365370
func RunServerlessUndeploy(c *CmdConfig) error {
366371
haveArgs := len(c.Args) > 0
367372
pkgFlag, _ := c.Doit.GetBool(c.NS, "packages")
373+
trigFlag, _ := c.Doit.GetBool(c.NS, "triggers")
368374
all, _ := c.Doit.GetBool(c.NS, "all")
369375
if haveArgs && all {
370376
return errUndeployAllAndArgs
371377
}
372378
if !haveArgs && !all {
373379
return errUndeployTooFewArgs
374380
}
381+
if pkgFlag && trigFlag {
382+
return errUndeployTrigPkg
383+
}
384+
if all && trigFlag {
385+
return cleanTriggers(c)
386+
}
375387
if all {
376388
return cleanNamespace(c)
377389
}
378390
var lastError error
379391
errorCount := 0
392+
var ctx context.Context
393+
var sls do.ServerlessService
394+
if trigFlag {
395+
ctx = context.TODO()
396+
sls = c.Serverless()
397+
}
380398
for _, arg := range c.Args {
381399
var err error
382-
if strings.Contains(arg, "/") || !pkgFlag {
400+
if trigFlag {
401+
err = sls.DeleteTrigger(ctx, arg)
402+
} else if strings.Contains(arg, "/") || !pkgFlag {
383403
err = deleteFunction(c, arg)
384404
} else {
385405
err = deletePackage(c, arg)

0 commit comments

Comments
 (0)