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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#

# build stage
FROM golang:1.23-alpine AS build-env
FROM golang:1.24-alpine AS build-env
RUN apk add --no-cache gcc musl-dev
ARG D=/go/src/github.com/fnproject/cli
ARG GO111MODULE=on
Expand Down
4 changes: 4 additions & 0 deletions client/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type InvokeRequest struct {
Content io.Reader
Env []string
ContentType string
FnInvokeType string
// TODO headers should be their real type?
}

Expand Down Expand Up @@ -82,6 +83,9 @@ func Invoke(provider provider.Provider, ireq InvokeRequest) (*http.Response, err
} else {
req.Header.Set("Content-Type", "text/plain")
}
if ireq.FnInvokeType != "" {
req.Header.Set("fn-invoke-type", ireq.FnInvokeType)
}

if len(env) > 0 {
EnvAsHeader(req, env)
Expand Down
48 changes: 48 additions & 0 deletions client/invoke_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package client

import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/fnproject/fn_go/client/version"
"github.com/fnproject/fn_go/clientv2"
"github.com/fnproject/fn_go/provider"
)

type invokeTestProvider struct{}

func (p *invokeTestProvider) APIClientv2() *clientv2.Fn { return nil }
func (p *invokeTestProvider) APIURL() *url.URL { return nil }
func (p *invokeTestProvider) UnavailableResources() []provider.FnResourceType {
return nil
}
func (p *invokeTestProvider) VersionClient() *version.Client { return nil }
func (p *invokeTestProvider) WrapCallTransport(rt http.RoundTripper) http.RoundTripper {
return rt
}

func TestInvokeSetsFnInvokeTypeHeader(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("fn-invoke-type"); got != "detached" {
t.Fatalf("expected fn-invoke-type detached, got %q", got)
}
w.WriteHeader(http.StatusAccepted)
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()

resp, err := Invoke(&invokeTestProvider{}, InvokeRequest{
URL: server.URL,
FnInvokeType: "detached",
Content: strings.NewReader("{}"),
})
if err != nil {
t.Fatalf("Invoke() error = %v", err)
}
defer resp.Body.Close()
_, _ = io.ReadAll(resp.Body)
}
13 changes: 13 additions & 0 deletions commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,13 @@ func (p *deploycmd) deployFuncV20180708(c *cli.Context, app *models.App, funcfil

func (p *deploycmd) updateFunction(c *cli.Context, appID string, ff *common.FuncFileV20180708) error {
fmt.Printf("Updating function %s using image %s...\n", ff.Name, ff.ImageNameV20180708())
var detachedSeconds int
if ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.DetachedMode != nil && ff.Deploy.OCI.DetachedMode.Timeout != "" {
_, seconds, err := common.ParseDetachedTimeoutSpec(ff.Deploy.OCI.DetachedMode.Timeout)
if err != nil {
return err
}
detachedSeconds = seconds
if ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.ProvisionedConcurrency != nil {
if err := common.ValidateProvisionedConcurrencyConfig(ff.Deploy.OCI.ProvisionedConcurrency); err != nil {
return err
Expand All @@ -433,6 +440,12 @@ func (p *deploycmd) updateFunction(c *cli.Context, appID string, ff *common.Func
if err := function.WithFuncFileV20180708(ff, fn); err != nil {
return fmt.Errorf("Error getting function with funcfile: %s", err)
}
if detachedSeconds > 0 && common.IsOracleProvider(p.provider) {
function.SetDetachedTimeoutAnnotation(fn, detachedSeconds)
}
if common.IsOracleProvider(p.provider) && ff.Deploy != nil && ff.Deploy.OCI != nil && ff.Deploy.OCI.DetachedMode != nil {
function.SetDestinationAnnotations(fn, ff.Deploy.OCI.DetachedMode.OnSuccess, ff.Deploy.OCI.DetachedMode.OnFailure)
}
created := false

fnRes, err := function.GetFnByName(p.clientV2, appID, ff.Name)
Expand Down
25 changes: 25 additions & 0 deletions commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ func initFlags(a *initFnCmd) []cli.Flag {
Usage: "Function annotation (can be specified multiple times)",
},
cli.StringFlag{
Name: "detached-timeout",
Usage: "Set OCI detached mode timeout using a duration like 20m or 1h",
},
cli.StringFlag{
Name: "on-success",
Usage: "Set OCI detached success destination using <stream|queue|notifications>:<ocid>",
},
cli.StringFlag{
Name: "on-failure",
Usage: "Set OCI detached failure destination using <stream|queue|notifications>:<ocid>",
Name: "provisioned-concurrency",
Usage: "Set OCI provisioned concurrency using 'none' or 'constant:<count>'",
},
Expand Down Expand Up @@ -180,6 +190,21 @@ func (a *initFnCmd) init(c *cli.Context) error {

function.WithFlags(c, &fn)
a.bindFn(&fn)
if timeoutSpec, _, err := common.ParseDetachedTimeoutSpec(c.String("detached-timeout")); err != nil {
return err
} else {
common.SetDetachedTimeout(a.ff, timeoutSpec)
}
if dest, err := common.ParseOCIDestinationSpec("--on-success", c.String("on-success")); err != nil {
return err
} else {
common.SetOnSuccessDestination(a.ff, dest)
}
if dest, err := common.ParseOCIDestinationSpec("--on-failure", c.String("on-failure")); err != nil {
return err
} else {
common.SetOnFailureDestination(a.ff, dest)
}
pcConfig, err := common.ParseProvisionedConcurrencySpec(c.String("provisioned-concurrency"))
if err != nil {
return err
Expand Down
13 changes: 13 additions & 0 deletions commands/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ var InvokeFnFlags = []cli.Flag{
Name: "output",
Usage: "Output format (json)",
},
cli.StringFlag{
Name: "fn-invoke-type",
Usage: "Invoke type for Oracle Functions: sync or detached",
},
}

// InvokeCommand returns call cli.command
Expand Down Expand Up @@ -129,6 +133,14 @@ func (cl *invokeCmd) Invoke(c *cli.Context) error {
}
content := stdin()
wd := common.GetWd()
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 = ""
}

if c.String("content-type") != "" {
contentType = c.String("content-type")
Expand All @@ -145,6 +157,7 @@ func (cl *invokeCmd) Invoke(c *cli.Context) error {
Content: content,
Env: c.StringSlice("e"),
ContentType: contentType,
FnInvokeType: invokeType,
},
)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func PrintContextualInfo() {
func PrintDockerfileContent(dockerfile string, stdout io.Writer) {
file, err := os.Open(dockerfile)
if err != nil {
fmt.Fprintf(stdout, err.Error())
fmt.Fprint(stdout, err.Error())
fmt.Fprintf(stdout, "\n")
return
}
Expand Down
10 changes: 5 additions & 5 deletions common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ func Test_writeTmpDockerfileV20180708(t *testing.T) {
},
{"java-debug-image",
args{&langs.JavaLangHelper{Version: "17"}, dir, &javaFuncFile, true, javaFdkEntryPoint},
javaDebugDockerfile,
fmt.Sprintf(javaDebugDockerfile, langs.MavenOptsForTest()),
},
{"java-normal-image",
args{&langs.JavaLangHelper{Version: "17"}, dir, &javaFuncFile, false, javaFdkEntryPoint},
javaDockerfile,
fmt.Sprintf(javaDockerfile, langs.MavenOptsForTest()),
},
}
for _, tt := range tests {
Expand Down Expand Up @@ -318,9 +318,9 @@ WORKDIR /function
COPY --from=build-stage /go/src/func/func /function/
ENTRYPOINT ["./func"]
`
javaDebugDockerfile = `FROM fnproject/fn-java-fdk-build:jdk17-1.1.7 as build-stage
javaDebugDockerfile = `FROM fnproject/fn-java-fdk-build:jdk17-1.1.7 as build-stage
WORKDIR /function
ENV MAVEN_OPTS -Dhttp.proxyHost= -Dhttp.proxyPort= -Dhttps.proxyHost= -Dhttps.proxyPort= -Dhttp.nonProxyHosts= -Dmaven.repo.local=/usr/share/maven/ref/repository
ENV MAVEN_OPTS %s
ADD pom.xml /function/pom.xml
RUN ["mvn", "package", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=true", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target", "--fail-never"]
ADD src /function/src
Expand All @@ -333,7 +333,7 @@ CMD ["com.example.fn.HelloFunction::handleRequest"]
`
javaDockerfile = `FROM fnproject/fn-java-fdk-build:jdk17-1.1.7 as build-stage
WORKDIR /function
ENV MAVEN_OPTS -Dhttp.proxyHost= -Dhttp.proxyPort= -Dhttps.proxyHost= -Dhttps.proxyPort= -Dhttp.nonProxyHosts= -Dmaven.repo.local=/usr/share/maven/ref/repository
ENV MAVEN_OPTS %s
ADD pom.xml /function/pom.xml
RUN ["mvn", "package", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=true", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target", "--fail-never"]
ADD src /function/src
Expand Down
66 changes: 66 additions & 0 deletions common/destinations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package common

import (
"fmt"
"strings"
)

const (
DestinationTypeStream = "stream"
DestinationTypeQueue = "queue"
DestinationTypeNotifications = "notifications"
)

// ParseOCIDestinationSpec parses a shorthand destination spec like stream:<ocid>.
func ParseOCIDestinationSpec(flagName, spec string) (*OCIDestination, error) {
spec = strings.TrimSpace(spec)
if spec == "" {
return nil, nil
}
parts := strings.SplitN(spec, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid value for %s: %q (expected <stream|queue|notifications>:<ocid>)", flagName, spec)
}
typePart := strings.ToLower(strings.TrimSpace(parts[0]))
ocid := strings.TrimSpace(parts[1])
if ocid == "" {
return nil, fmt.Errorf("invalid value for %s: %q (destination OCID is required)", flagName, spec)
}
switch typePart {
case DestinationTypeStream, DestinationTypeQueue:
return &OCIDestination{Type: typePart, OCID: ocid}, nil
case "notification", DestinationTypeNotifications:
return &OCIDestination{Type: DestinationTypeNotifications, OCID: ocid}, nil
default:
return nil, fmt.Errorf("invalid value for %s: %q (unsupported destination type %q)", flagName, spec, typePart)
}
}

func ensureDetachedModeConfig(ff *FuncFileV20180708) *OCIDetachedModeConfig {
if ff.Deploy == nil {
ff.Deploy = &FuncDeployConfig{}
}
if ff.Deploy.OCI == nil {
ff.Deploy.OCI = &OCIFunctionDeployConfig{}
}
if ff.Deploy.OCI.DetachedMode == nil {
ff.Deploy.OCI.DetachedMode = &OCIDetachedModeConfig{}
}
return ff.Deploy.OCI.DetachedMode
}

// SetOnSuccessDestination stores an OCI on-success destination in func.yaml config.
func SetOnSuccessDestination(ff *FuncFileV20180708, dest *OCIDestination) {
if ff == nil || dest == nil {
return
}
ensureDetachedModeConfig(ff).OnSuccess = dest
}

// SetOnFailureDestination stores an OCI on-failure destination in func.yaml config.
func SetOnFailureDestination(ff *FuncFileV20180708, dest *OCIDestination) {
if ff == nil || dest == nil {
return
}
ensureDetachedModeConfig(ff).OnFailure = dest
}
47 changes: 47 additions & 0 deletions common/destinations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package common

import "testing"

func TestParseOCIDestinationSpec(t *testing.T) {
tests := []struct {
name string
spec string
wantNil bool
wantErr bool
wantType string
wantOCID string
}{
{name: "empty", spec: "", wantNil: true},
{name: "stream", spec: "stream:ocid1.stream.oc1..abc", wantType: DestinationTypeStream, wantOCID: "ocid1.stream.oc1..abc"},
{name: "queue", spec: "queue:ocid1.queue.oc1..abc", wantType: DestinationTypeQueue, wantOCID: "ocid1.queue.oc1..abc"},
{name: "notifications alias", spec: "notification:ocid1.onstopic.oc1..abc", wantType: DestinationTypeNotifications, wantOCID: "ocid1.onstopic.oc1..abc"},
{name: "notifications plural", spec: "notifications:ocid1.onstopic.oc1..abc", wantType: DestinationTypeNotifications, wantOCID: "ocid1.onstopic.oc1..abc"},
{name: "invalid missing ocid", spec: "stream:", wantErr: true},
{name: "invalid missing colon", spec: "stream", wantErr: true},
{name: "invalid type", spec: "topic:abc", wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dest, err := ParseOCIDestinationSpec("--on-success", tt.spec)
if (err != nil) != tt.wantErr {
t.Fatalf("ParseOCIDestinationSpec() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantNil {
if dest != nil {
t.Fatalf("expected nil destination, got %#v", dest)
}
return
}
if tt.wantErr {
return
}
if dest == nil {
t.Fatal("expected non-nil destination")
}
if dest.Type != tt.wantType || dest.OCID != tt.wantOCID {
t.Fatalf("expected (%q, %q), got (%q, %q)", tt.wantType, tt.wantOCID, dest.Type, dest.OCID)
}
})
}
}
44 changes: 44 additions & 0 deletions common/detached_timeout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package common

import (
"fmt"
"strings"
"time"
)

// ParseDetachedTimeoutSpec validates a human-friendly detached timeout such as 20m or 1h
// and returns the original trimmed value plus the timeout in seconds.
func ParseDetachedTimeoutSpec(spec string) (string, int, error) {
spec = strings.TrimSpace(spec)
if spec == "" {
return "", 0, nil
}
d, err := time.ParseDuration(spec)
if err != nil {
return "", 0, fmt.Errorf("invalid value for --detached-timeout: %q", spec)
}
if d <= 0 {
return "", 0, fmt.Errorf("invalid value for --detached-timeout: %q", spec)
}
if d%time.Second != 0 {
return "", 0, fmt.Errorf("invalid value for --detached-timeout: %q (must resolve to whole seconds)", spec)
}
return spec, int(d / time.Second), nil
}

// SetDetachedTimeout stores detached timeout config in the function deploy section.
func SetDetachedTimeout(ff *FuncFileV20180708, timeout string) {
if ff == nil || strings.TrimSpace(timeout) == "" {
return
}
if ff.Deploy == nil {
ff.Deploy = &FuncDeployConfig{}
}
if ff.Deploy.OCI == nil {
ff.Deploy.OCI = &OCIFunctionDeployConfig{}
}
if ff.Deploy.OCI.DetachedMode == nil {
ff.Deploy.OCI.DetachedMode = &OCIDetachedModeConfig{}
}
ff.Deploy.OCI.DetachedMode.Timeout = strings.TrimSpace(timeout)
}
Loading