From 6d7ab83aed671aa24f5c35d1bf5912a08a6ac45c Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Sun, 6 Mar 2022 17:25:35 -0500 Subject: [PATCH] Make `func invoke` print any response it receives. (#881) * Make `func invoke` print any response it receives. Signed-off-by: Lance Ball * fixup: improve test Signed-off-by: Lance Ball * fixup: check Write() return value Signed-off-by: Lance Ball --- client.go | 2 +- client_test.go | 25 +++++++++++++++++++++---- cmd/invoke.go | 4 ++-- invoke.go | 25 +++++++++++++++---------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/client.go b/client.go index 9d4f0a83..dde8e477 100644 --- a/client.go +++ b/client.go @@ -846,7 +846,7 @@ func (c *Client) Remove(ctx context.Context, cfg Function, deleteAll bool) error // See NewInvokeMessage for its defaults. // Functions are invoked in a manner consistent with the settings defined in // their metadata. For example HTTP vs CloudEvent -func (c *Client) Invoke(ctx context.Context, root string, target string, m InvokeMessage) (err error) { +func (c *Client) Invoke(ctx context.Context, root string, target string, m InvokeMessage) (s string, err error) { go func() { <-ctx.Done() c.progressListener.Stopping() diff --git a/client_test.go b/client_test.go index 94026d34..010f42de 100644 --- a/client_test.go +++ b/client_test.go @@ -1099,6 +1099,10 @@ func TestClient_Invoke_HTTP(t *testing.T) { if req.Form.Get("ID") != message.ID { t.Fatalf("expected message ID '%v', got '%v'", message.ID, req.Form.Get("ID")) } + _, err := res.Write([]byte("hello world")) + if err != nil { + t.Fatal(err) + } }) // Expose the masquarading Function on an OS-chosen port. @@ -1139,10 +1143,16 @@ func TestClient_Invoke_HTTP(t *testing.T) { defer job.Stop() // Invoke the Function, which will use the mock Runner - if err := client.Invoke(context.Background(), f.Root, "", message); err != nil { + r, err := client.Invoke(context.Background(), f.Root, "", message) + if err != nil { t.Fatal(err) } + // Check the response value + if r != "hello world" { + t.Fatal("Unexpected response from function " + r) + } + // Fail if the Function was never invoked. if !invoked { t.Fatal("Function was not invoked") @@ -1166,15 +1176,18 @@ func TestClient_Invoke_CloudEvent(t *testing.T) { invoked bool // flag the Function was invoked ctx = context.Background() message = fn.NewInvokeMessage() // message to send to the Function + evt *cloudevents.Event // A pointer to the received event ) // A CloudEvent Receiver which masquarades as a running Function and // verifies the invoker sent the message as a populated CloudEvent. - receiver := func(ctx context.Context, event cloudevents.Event) { + receiver := func(ctx context.Context, event cloudevents.Event) *cloudevents.Event { invoked = true if event.ID() != message.ID { t.Fatalf("expected event ID '%v', got '%v'", message.ID, event.ID()) } + evt = &event + return evt } // A cloudevent receive handler which will expect the HTTP protocol @@ -1224,10 +1237,14 @@ func TestClient_Invoke_CloudEvent(t *testing.T) { defer job.Stop() // Invoke the Function, which will use the mock Runner - if err := client.Invoke(context.Background(), f.Root, "", message); err != nil { + r, err := client.Invoke(context.Background(), f.Root, "", message) + if err != nil { t.Fatal(err) } - + // Test the contents of the returned string. + if r != evt.String() { + t.Fatal("Invoke failed to return a response") + } // Fail if the Function was never invoked. if !invoked { t.Fatal("Function was not invoked") diff --git a/cmd/invoke.go b/cmd/invoke.go index 7fecc9d8..a1128985 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -160,12 +160,12 @@ func runInvoke(cmd *cobra.Command, args []string, newClient ClientFactory) (err } // Invoke - err = client.Invoke(cmd.Context(), cfg.Path, cfg.Target, m) + s, err := client.Invoke(cmd.Context(), cfg.Path, cfg.Target, m) if err != nil { return err } - fmt.Fprintf(cmd.OutOrStderr(), "Invoked %v\n", cfg.Target) + fmt.Fprintln(cmd.OutOrStderr(), s) return } diff --git a/invoke.go b/invoke.go index 62f3bbc8..8e863b86 100644 --- a/invoke.go +++ b/invoke.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "net/http" "net/url" "time" @@ -45,13 +46,13 @@ func NewInvokeMessage() InvokeMessage { // invoke the Function instance in the target environment with the // invocation message. -func invoke(ctx context.Context, c *Client, f Function, target string, m InvokeMessage) error { +func invoke(ctx context.Context, c *Client, f Function, target string, m InvokeMessage) (string, error) { // Get the first available route from 'local', 'remote', a named environment // or treat target route, err := invocationRoute(ctx, c, f, target) // choose instance to invoke if err != nil { - return err + return "", err } // Format" either 'http' or 'cloudevent' @@ -75,7 +76,7 @@ func invoke(ctx context.Context, c *Client, f Function, target string, m InvokeM case "cloudevent": return sendEvent(ctx, route, m, c.transport) default: - return fmt.Errorf("format '%v' not supported.", format) + return "", fmt.Errorf("format '%v' not supported.", format) } } @@ -130,7 +131,7 @@ func invocationRoute(ctx context.Context, c *Client, f Function, target string) } // sendEvent to the route populated with data in the invoke message. -func sendEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper) (err error) { +func sendEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper) (resp string, err error) { event := cloudevents.NewEvent() event.SetID(m.ID) event.SetSource(m.Source) @@ -146,15 +147,18 @@ func sendEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundT return } - result := c.Send(cloudevents.ContextWithTarget(ctx, route), event) + evt, result := c.Request(cloudevents.ContextWithTarget(ctx, route), event) if cloudevents.IsUndelivered(result) { err = fmt.Errorf("unable to invoke: %v", result) + } else if evt != nil { // Check for nil in case no event is returned + resp = evt.String() } + return } // sendPost to the route populated with data in the invoke message. -func sendPost(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper) error { +func sendPost(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper) (string, error) { client := http.Client{ Transport: t, Timeout: 10 * time.Second, @@ -167,11 +171,12 @@ func sendPost(ctx context.Context, route string, m InvokeMessage, t http.RoundTr "Data": {m.Data}, }) if err != nil { - return err + return "", err } defer resp.Body.Close() - if resp.StatusCode != 200 { - return fmt.Errorf("failure invoking '%v' (HTTP %v)", route, resp.StatusCode) + if resp.StatusCode > 299 { + return "", fmt.Errorf("failure invoking '%v' (HTTP %v)", route, resp.StatusCode) } - return nil + b, err := io.ReadAll(resp.Body) + return string(b), err }