From 728b6c2ec7082fedd1033a934ea43930d3e9a1a1 Mon Sep 17 00:00:00 2001 From: jayarora Date: Thu, 14 May 2026 15:25:22 +0100 Subject: [PATCH] Add support for invoke parity: detached invoke / intent / dry-run --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++++ RELEASES.md | 7 +++ client/invoke.go | 8 +++ client/invoke_test.go | 44 ++++++++++++++++ commands/invoke.go | 68 ++++++++++++++++++++++++- 5 files changed, 239 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aab91a03..0259ed4a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,119 @@ curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh * For a list of commands see [Fn CLI Command Guide and Reference](https://github.com/fnproject/docs/blob/master/cli/README.md). * For general information see Fn [docs](https://github.com/fnproject/docs) and [tutorials](https://fnproject.io/tutorials/). +## OCI Functions configuration +Fn CLI now supports additional OCI Functions settings for: + +- **Provisioned concurrency** via `--provisioned-concurrency` +- **Detached / long-running functions** via: + - `--detached-timeout` + - `--on-success` + - `--on-failure` + - `--clear-on-success` + - `--clear-on-failure` + +These OCI Functions settings can be used through multiple Fn CLI workflows: + +- `fn init` to scaffold and persist them in `func.yaml` +- `fn create function` / `fn update function` to apply them directly from the CLI +- `fn deploy` to apply values persisted in `func.yaml` + +### Examples +Initialize a function with provisioned concurrency: + +```sh +fn init --runtime go --provisioned-concurrency constant:40 hello +``` + +Create a function with provisioned concurrency directly: + +```sh +fn create function \ + --provisioned-concurrency constant:40 +``` + +Update a function with provisioned concurrency directly: + +```sh +fn update function \ + --provisioned-concurrency constant:40 +``` + +Initialize a function for detached mode with OCI destinations: + +```sh +fn init --runtime go \ + --detached-timeout 20m \ + --on-success stream: \ + --on-failure notifications: \ + hello +``` + +Create a function with detached-mode settings directly: + +```sh +fn create function \ + --detached-timeout 20m \ + --on-success stream: \ + --on-failure notifications: +``` + +Update a function with detached-mode settings directly: + +```sh +fn update function \ + --detached-timeout 20m \ + --on-success stream: \ + --on-failure notifications: +``` + +Example `func.yaml` output: + +```yaml +deploy: + oci: + provisionedConcurrency: + strategy: CONSTANT + count: 40 + detachedMode: + timeout: 20m + onSuccess: + type: stream + ocid: + onFailure: + type: notifications + ocid: +``` + +When these OCI-specific flags are used with a non-Oracle provider or local Fn server workflows, Fn CLI accepts them and emits user-friendly warnings where the settings are not applicable. + +### Detached invoke examples +Invoke a function in detached mode: + +```sh +fn invoke detached --display-call-id +``` + +Equivalent OCI-style invoke flag: + +```sh +fn invoke --fn-invoke-type detached --display-call-id +``` + +Invoke with an intent header: + +```sh +fn invoke detached --fn-intent cloudevent --display-call-id +``` + +Invoke as a dry run: + +```sh +fn invoke detached --is-dry-run --output json +``` + +For OCI Functions, detached invocation typically returns immediately and, when requested, prints a call ID that can be used for correlation with downstream success/failure destinations. + ## CLI Development * Refer to the [Fn CLI Wiki](https://github.com/fnproject/cli/wiki) for development details. diff --git a/RELEASES.md b/RELEASES.md index fecfed06..e8787dfc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,13 @@ * Enabling support for node24 * Enabling support for java21 +* Add OCI Functions provisioned concurrency support to Fn CLI, including CLI flag parsing, `func.yaml` persistence, deploy/update handling, inspect/list output, and OCI shim mappings. +* Add OCI Functions detached / long-running configuration support, including detached timeout and success/failure destinations persisted via `func.yaml` and applied through deploy/update flows. +* Add detached invoke support for OCI Functions in Fn CLI, including: + * `fn invoke detached ` + * `--fn-invoke-type detached` + * `--fn-intent` + * `--is-dry-run` ## v 0.6.47 diff --git a/client/invoke.go b/client/invoke.go index b132e704..ff7dae63 100644 --- a/client/invoke.go +++ b/client/invoke.go @@ -53,6 +53,8 @@ type InvokeRequest struct { Env []string ContentType string FnInvokeType string + FnIntent string + IsDryRun bool // TODO headers should be their real type? } @@ -86,6 +88,12 @@ func Invoke(provider provider.Provider, ireq InvokeRequest) (*http.Response, err if ireq.FnInvokeType != "" { req.Header.Set("fn-invoke-type", ireq.FnInvokeType) } + if ireq.FnIntent != "" { + req.Header.Set("fn-intent", ireq.FnIntent) + } + if ireq.IsDryRun { + req.Header.Set("is-dry-run", "true") + } if len(env) > 0 { EnvAsHeader(req, env) diff --git a/client/invoke_test.go b/client/invoke_test.go index dcc17f8b..ea9a9b7c 100644 --- a/client/invoke_test.go +++ b/client/invoke_test.go @@ -45,4 +45,48 @@ func TestInvokeSetsFnInvokeTypeHeader(t *testing.T) { } defer resp.Body.Close() _, _ = io.ReadAll(resp.Body) +} + +func TestInvokeSetsFnIntentHeader(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("fn-intent"); got != "cloudevent" { + t.Fatalf("expected fn-intent cloudevent, got %q", got) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + defer server.Close() + + resp, err := Invoke(&invokeTestProvider{}, InvokeRequest{ + URL: server.URL, + FnIntent: "cloudevent", + Content: strings.NewReader("{}"), + }) + if err != nil { + t.Fatalf("Invoke() error = %v", err) + } + defer resp.Body.Close() + _, _ = io.ReadAll(resp.Body) +} + +func TestInvokeSetsIsDryRunHeader(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("is-dry-run"); got != "true" { + t.Fatalf("expected is-dry-run true, got %q", got) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + defer server.Close() + + resp, err := Invoke(&invokeTestProvider{}, InvokeRequest{ + URL: server.URL, + IsDryRun: true, + Content: strings.NewReader("{}"), + }) + if err != nil { + t.Fatalf("Invoke() error = %v", err) + } + defer resp.Body.Close() + _, _ = io.ReadAll(resp.Body) } \ No newline at end of file diff --git a/commands/invoke.go b/commands/invoke.go index 4fb3ad06..f3b9f9a8 100644 --- a/commands/invoke.go +++ b/commands/invoke.go @@ -65,12 +65,47 @@ var InvokeFnFlags = []cli.Flag{ Name: "output", Usage: "Output format (json)", }, + cli.StringFlag{ + Name: "fn-intent", + Usage: "Optional intent header for function invocation, e.g. httprequest or cloudevent", + }, + cli.BoolFlag{ + Name: "is-dry-run", + Usage: "Send the invocation as a dry run without executing the function when supported by the server", + }, cli.StringFlag{ Name: "fn-invoke-type", Usage: "Invoke type for Oracle Functions: sync or detached", }, } +var InvokeDetachedFnFlags = []cli.Flag{ + cli.StringFlag{ + Name: "endpoint", + Usage: "Specify the function invoke endpoint for this function, the app-name and func-name parameters will be ignored", + }, + cli.StringFlag{ + Name: "content-type", + Usage: "The payload Content-Type for the function invocation.", + }, + cli.BoolFlag{ + Name: "display-call-id", + Usage: "whether display call ID or not", + }, + cli.StringFlag{ + Name: "output", + Usage: "Output format (json)", + }, + cli.StringFlag{ + Name: "fn-intent", + Usage: "Optional intent header for function invocation, e.g. httprequest or cloudevent", + }, + cli.BoolFlag{ + Name: "is-dry-run", + Usage: "Send the invocation as a dry run without executing the function when supported by the server", + }, +} + // InvokeCommand returns call cli.command func InvokeCommand() cli.Command { cl := invokeCmd{} @@ -89,6 +124,23 @@ func InvokeCommand() cli.Command { }, ArgsUsage: "[app-name] [function-name]", Flags: InvokeFnFlags, + Subcommands: []cli.Command{ + { + Name: "detached", + Usage: "\tInvoke a remote function in detached mode", + ArgsUsage: "[app-name] [function-name]", + Flags: InvokeDetachedFnFlags, + Action: cl.InvokeDetached, + BashComplete: func(c *cli.Context) { + switch len(c.Args()) { + case 0: + app.BashCompleteApps(c) + case 1: + fn.BashCompleteFns(c) + } + }, + }, + }, Category: "DEVELOPMENT COMMANDS", Description: `This command invokes a function. Users may send input to their function by passing input to this command via STDIN.`, Action: cl.Invoke, @@ -104,6 +156,14 @@ func InvokeCommand() cli.Command { } func (cl *invokeCmd) Invoke(c *cli.Context) error { + return cl.invoke(c, "") +} + +func (cl *invokeCmd) InvokeDetached(c *cli.Context) error { + return cl.invoke(c, "detached") +} + +func (cl *invokeCmd) invoke(c *cli.Context, forcedInvokeType string) error { var contentType string invokeURL := c.String("endpoint") @@ -133,7 +193,10 @@ func (cl *invokeCmd) Invoke(c *cli.Context) error { } content := stdin() wd := common.GetWd() - invokeType := strings.ToLower(strings.TrimSpace(c.String("fn-invoke-type"))) + invokeType := strings.ToLower(strings.TrimSpace(forcedInvokeType)) + if invokeType == "" { + invokeType = strings.ToLower(strings.TrimSpace(c.String("fn-invoke-type"))) + } if invokeType != "" && invokeType != "sync" && invokeType != "detached" { return fmt.Errorf("invalid value for --fn-invoke-type: %q", invokeType) } @@ -141,6 +204,7 @@ func (cl *invokeCmd) Invoke(c *cli.Context) error { fmt.Fprintln(os.Stderr, "Warning: --fn-invoke-type=detached is only supported with an oracle provider and will be ignored.") invokeType = "" } + fnIntent := strings.TrimSpace(c.String("fn-intent")) if c.String("content-type") != "" { contentType = c.String("content-type") @@ -157,6 +221,8 @@ func (cl *invokeCmd) Invoke(c *cli.Context) error { Content: content, Env: c.StringSlice("e"), ContentType: contentType, + FnIntent: fnIntent, + IsDryRun: c.Bool("is-dry-run"), FnInvokeType: invokeType, }, )