Make `func invoke` print any response it receives. (#881)

* Make `func invoke` print any response it receives.

Signed-off-by: Lance Ball <lball@redhat.com>

* fixup: improve test

Signed-off-by: Lance Ball <lball@redhat.com>

* fixup: check Write() return value

Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Lance Ball 2022-03-06 17:25:35 -05:00 committed by GitHub
parent 5a122c31e6
commit 6d7ab83aed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 39 additions and 17 deletions

View File

@ -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()

View File

@ -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")

View File

@ -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
}

View File

@ -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
}