mirror of https://github.com/knative/func.git
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:
parent
5a122c31e6
commit
6d7ab83aed
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
invoke.go
25
invoke.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue