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. // See NewInvokeMessage for its defaults.
// Functions are invoked in a manner consistent with the settings defined in // Functions are invoked in a manner consistent with the settings defined in
// their metadata. For example HTTP vs CloudEvent // 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() { go func() {
<-ctx.Done() <-ctx.Done()
c.progressListener.Stopping() c.progressListener.Stopping()

View File

@ -1099,6 +1099,10 @@ func TestClient_Invoke_HTTP(t *testing.T) {
if req.Form.Get("ID") != message.ID { if req.Form.Get("ID") != message.ID {
t.Fatalf("expected message ID '%v', got '%v'", message.ID, req.Form.Get("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. // Expose the masquarading Function on an OS-chosen port.
@ -1139,10 +1143,16 @@ func TestClient_Invoke_HTTP(t *testing.T) {
defer job.Stop() defer job.Stop()
// Invoke the Function, which will use the mock Runner // 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) 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. // Fail if the Function was never invoked.
if !invoked { if !invoked {
t.Fatal("Function was not 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 invoked bool // flag the Function was invoked
ctx = context.Background() ctx = context.Background()
message = fn.NewInvokeMessage() // message to send to the Function 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 // A CloudEvent Receiver which masquarades as a running Function and
// verifies the invoker sent the message as a populated CloudEvent. // 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 invoked = true
if event.ID() != message.ID { if event.ID() != message.ID {
t.Fatalf("expected event ID '%v', got '%v'", message.ID, event.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 // A cloudevent receive handler which will expect the HTTP protocol
@ -1224,10 +1237,14 @@ func TestClient_Invoke_CloudEvent(t *testing.T) {
defer job.Stop() defer job.Stop()
// Invoke the Function, which will use the mock Runner // 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) 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. // Fail if the Function was never invoked.
if !invoked { if !invoked {
t.Fatal("Function was not invoked") t.Fatal("Function was not invoked")

View File

@ -160,12 +160,12 @@ func runInvoke(cmd *cobra.Command, args []string, newClient ClientFactory) (err
} }
// Invoke // 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 { if err != nil {
return err return err
} }
fmt.Fprintf(cmd.OutOrStderr(), "Invoked %v\n", cfg.Target) fmt.Fprintln(cmd.OutOrStderr(), s)
return return
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
@ -45,13 +46,13 @@ func NewInvokeMessage() InvokeMessage {
// invoke the Function instance in the target environment with the // invoke the Function instance in the target environment with the
// invocation message. // 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 // Get the first available route from 'local', 'remote', a named environment
// or treat target // or treat target
route, err := invocationRoute(ctx, c, f, target) // choose instance to invoke route, err := invocationRoute(ctx, c, f, target) // choose instance to invoke
if err != nil { if err != nil {
return err return "", err
} }
// Format" either 'http' or 'cloudevent' // Format" either 'http' or 'cloudevent'
@ -75,7 +76,7 @@ func invoke(ctx context.Context, c *Client, f Function, target string, m InvokeM
case "cloudevent": case "cloudevent":
return sendEvent(ctx, route, m, c.transport) return sendEvent(ctx, route, m, c.transport)
default: 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. // 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 := cloudevents.NewEvent()
event.SetID(m.ID) event.SetID(m.ID)
event.SetSource(m.Source) event.SetSource(m.Source)
@ -146,15 +147,18 @@ func sendEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundT
return return
} }
result := c.Send(cloudevents.ContextWithTarget(ctx, route), event) evt, result := c.Request(cloudevents.ContextWithTarget(ctx, route), event)
if cloudevents.IsUndelivered(result) { if cloudevents.IsUndelivered(result) {
err = fmt.Errorf("unable to invoke: %v", 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 return
} }
// sendPost to the route populated with data in the invoke message. // 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{ client := http.Client{
Transport: t, Transport: t,
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
@ -167,11 +171,12 @@ func sendPost(ctx context.Context, route string, m InvokeMessage, t http.RoundTr
"Data": {m.Data}, "Data": {m.Data},
}) })
if err != nil { if err != nil {
return err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode > 299 {
return fmt.Errorf("failure invoking '%v' (HTTP %v)", route, resp.StatusCode) return "", fmt.Errorf("failure invoking '%v' (HTTP %v)", route, resp.StatusCode)
} }
return nil b, err := io.ReadAll(resp.Body)
return string(b), err
} }