Skip to content
Open
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
113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <app-name> <function-name> <image> \
--provisioned-concurrency constant:40
```

Update a function with provisioned concurrency directly:

```sh
fn update function <app-name> <function-name> \
--provisioned-concurrency constant:40
```

Initialize a function for detached mode with OCI destinations:

```sh
fn init --runtime go \
--detached-timeout 20m \
--on-success stream:<stream-ocid> \
--on-failure notifications:<topic-ocid> \
hello
```

Create a function with detached-mode settings directly:

```sh
fn create function <app-name> <function-name> <image> \
--detached-timeout 20m \
--on-success stream:<stream-ocid> \
--on-failure notifications:<topic-ocid>
```

Update a function with detached-mode settings directly:

```sh
fn update function <app-name> <function-name> \
--detached-timeout 20m \
--on-success stream:<stream-ocid> \
--on-failure notifications:<topic-ocid>
```

Example `func.yaml` output:

```yaml
deploy:
oci:
provisionedConcurrency:
strategy: CONSTANT
count: 40
detachedMode:
timeout: 20m
onSuccess:
type: stream
ocid: <stream-ocid>
onFailure:
type: notifications
ocid: <topic-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 <app-name> <function-name> --display-call-id
```

Equivalent OCI-style invoke flag:

```sh
fn invoke <app-name> <function-name> --fn-invoke-type detached --display-call-id
```

Invoke with an intent header:

```sh
fn invoke detached <app-name> <function-name> --fn-intent cloudevent --display-call-id
```

Invoke as a dry run:

```sh
fn invoke detached <app-name> <function-name> --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.

Expand Down
7 changes: 7 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <app-name> <function-name>`
* `--fn-invoke-type detached`
* `--fn-intent`
* `--is-dry-run`

## v 0.6.47

Expand Down
8 changes: 8 additions & 0 deletions client/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type InvokeRequest struct {
Env []string
ContentType string
FnInvokeType string
FnIntent string
IsDryRun bool
// TODO headers should be their real type?
}

Expand Down Expand Up @@ -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)
Expand Down
44 changes: 44 additions & 0 deletions client/invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
68 changes: 67 additions & 1 deletion commands/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -133,14 +193,18 @@ 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)
}
if invokeType == "detached" && !common.IsOracleProvider(cl.provider) {
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")
Expand All @@ -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,
},
)
Expand Down