Skip to content

Commit 4c0fa01

Browse files
authored
feat: func invoke get requests (knative#2942)
1 parent f096936 commit 4c0fa01

File tree

3 files changed

+102
-18
lines changed

3 files changed

+102
-18
lines changed

cmd/invoke.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,14 @@ EXAMPLES
9999
o Allow insecure server connections when using SSL
100100
$ {{rootCmdUse}} invoke --insecure
101101
102+
o In case you need to specifically send GET request
103+
$ {{rootCmdUse}} invoke --request-type=GET
102104
`,
103-
SuggestFor: []string{"emit", "emti", "send", "emit", "exec", "nivoke", "onvoke", "unvoke", "knvoke", "imvoke", "ihvoke", "ibvoke"},
104-
PreRunE: bindEnv("path", "format", "target", "id", "source", "type", "data", "content-type", "file", "insecure", "confirm", "verbose"),
105+
SuggestFor: []string{"emit", "emti", "send", "emit", "exec", "nivoke",
106+
"onvoke", "unvoke", "knvoke", "imvoke", "ihvoke", "ibvoke"},
107+
PreRunE: bindEnv("path", "format", "target", "id", "source", "type",
108+
"data", "content-type", "request-type", "file", "insecure",
109+
"confirm", "verbose"),
105110
RunE: func(cmd *cobra.Command, args []string) error {
106111
return runInvoke(cmd, args, newClient)
107112
},
@@ -120,6 +125,7 @@ EXAMPLES
120125
cmd.Flags().StringP("source", "", fn.DefaultInvokeSource, "Source value for the request data. ($FUNC_SOURCE)")
121126
cmd.Flags().StringP("type", "", fn.DefaultInvokeType, "Type value for the request data. ($FUNC_TYPE)")
122127
cmd.Flags().StringP("content-type", "", fn.DefaultInvokeContentType, "Content Type of the data. ($FUNC_CONTENT_TYPE)")
128+
cmd.Flags().StringP("request-type", "", fn.DefaultInvokeRequestType, "Type of request to use. Can be POST or GET. ($FUNC_REQUEST_TYPE)")
123129
cmd.Flags().StringP("data", "", fn.DefaultInvokeData, "Data to send in the request. ($FUNC_DATA)")
124130
cmd.Flags().StringP("file", "", "", "Path to a file to use as data. Overrides --data flag and should be sent with a correct --content-type. ($FUNC_FILE)")
125131
cmd.Flags().BoolP("insecure", "i", false, "Allow insecure server connections when using SSL. ($FUNC_INSECURE)")
@@ -162,6 +168,7 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
162168
Source: cfg.Source,
163169
Type: cfg.Type,
164170
ContentType: cfg.ContentType,
171+
RequestType: strings.ToUpper(cfg.RequestType),
165172
Data: cfg.Data,
166173
Format: cfg.Format,
167174
}
@@ -219,6 +226,7 @@ type invokeConfig struct {
219226
Type string
220227
Data []byte
221228
ContentType string
229+
RequestType string
222230
File string
223231
Confirm bool
224232
Verbose bool
@@ -235,6 +243,7 @@ func newInvokeConfig() (cfg invokeConfig, err error) {
235243
Type: viper.GetString("type"),
236244
Data: []byte(viper.GetString("data")),
237245
ContentType: viper.GetString("content-type"),
246+
RequestType: viper.GetString("request-type"),
238247
File: viper.GetString("file"),
239248
Confirm: viper.GetBool("confirm"),
240249
Verbose: viper.GetBool("verbose"),

docs/reference/func_invoke.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ EXAMPLES
8585
o Allow insecure server connections when using SSL
8686
$ func invoke --insecure
8787

88+
o In case you need to specifically send GET request
89+
$ func invoke --request-type=GET
8890

8991

9092
```
@@ -103,6 +105,7 @@ func invoke
103105
--id string ID for the request data. ($FUNC_ID)
104106
-i, --insecure Allow insecure server connections when using SSL. ($FUNC_INSECURE)
105107
-p, --path string Path to the function. Default is current directory ($FUNC_PATH)
108+
--request-type string Type of request to use. Can be POST or GET. ($FUNC_REQUEST_TYPE) (default "POST")
106109
--source string Source value for the request data. ($FUNC_SOURCE) (default "/boson/fn")
107110
-t, --target string Function instance to invoke. Can be 'local', 'remote' or a URL. Defaults to auto-discovery if not provided. ($FUNC_TARGET)
108111
--type string Type value for the request data. ($FUNC_TYPE) (default "boson.fn")

pkg/functions/invoke.go

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import (
1111
"time"
1212

1313
cloudevents "github.com/cloudevents/sdk-go/v2"
14+
cehttp "github.com/cloudevents/sdk-go/v2/protocol/http"
15+
1416
"github.com/google/uuid"
1517
)
1618

1719
const (
1820
DefaultInvokeSource = "/boson/fn"
1921
DefaultInvokeType = "boson.fn"
2022
DefaultInvokeContentType = "application/json"
23+
DefaultInvokeRequestType = "POST"
2124
DefaultInvokeData = `{"message":"Hello World"}`
2225
DefaultInvokeFormat = "http"
2326
)
@@ -30,6 +33,7 @@ type InvokeMessage struct {
3033
Type string
3134
ContentType string
3235
Data []byte
36+
RequestType string // HTTP Request GET/POST (defaults to POST)
3337
Format string // optional override for function-defined message format
3438
}
3539

@@ -40,6 +44,7 @@ func NewInvokeMessage() InvokeMessage {
4044
Source: DefaultInvokeSource,
4145
Type: DefaultInvokeType,
4246
ContentType: DefaultInvokeContentType,
47+
RequestType: DefaultInvokeRequestType,
4348
Data: []byte(DefaultInvokeData),
4449
// Format override not set by default: value from function being preferred.
4550
}
@@ -63,6 +68,11 @@ func invoke(ctx context.Context, c *Client, f Function, target string, m InvokeM
6368
// set. Once decided, codify in a test.
6469
format := DefaultInvokeFormat
6570

71+
// RequestType is expected GET or POST
72+
if m.RequestType == "" {
73+
m.RequestType = DefaultInvokeRequestType
74+
}
75+
6676
if verbose {
6777
fmt.Printf("Invoking '%v' function at %v\n", f.Invoke, route)
6878
}
@@ -79,19 +89,27 @@ func invoke(ctx context.Context, c *Client, f Function, target string, m InvokeM
7989
}
8090
}
8191

92+
if m.RequestType != "POST" && m.RequestType != "GET" {
93+
err = fmt.Errorf("http request type '%v' not supported, expected GET or POST", m.RequestType)
94+
return
95+
}
96+
8297
switch format {
8398
case "http":
84-
return sendPost(ctx, route, m, c.transport, verbose)
99+
return sendHttp(ctx, route, m, c.transport, verbose)
85100
case "cloudevent":
86-
// CouldEvents return a string which always includes a fairly verbose
87-
// summation of fields, so metadata is not applicable
88-
meta := make(map[string][]string)
89-
body, err = sendEvent(ctx, route, m, c.transport, verbose)
90-
return meta, body, err
101+
switch m.RequestType {
102+
case "POST":
103+
body, err = sendEvent(ctx, route, m, c.transport, verbose)
104+
case "GET":
105+
// Construct a special CloudEvents GET request.
106+
// This will be used most likely only for very special cases
107+
body, err = sendGetEvent(ctx, route, m, c.transport, verbose)
108+
}
91109
default:
92110
err = fmt.Errorf("format '%v' not supported", format)
93-
return
94111
}
112+
return
95113
}
96114

97115
// invocationRoute returns a route to the named target instance of a func:
@@ -176,30 +194,84 @@ func sendEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundT
176194
return
177195
}
178196

197+
// sendGetEvent sends a GET event as a cloudEvent. Normally, this is not the
198+
// way CEs are intended to be sent exactly.
199+
// In CloudEvents specification it reads:
200+
//
201+
// "Events can be transferred with all standard or application-defined HTTP request
202+
// methods that support payload body transfers"
203+
//
204+
// Since this is not the case for GET request, we need to specify custom protocol
205+
// and use a slightly different client resulting in a slightly different
206+
// function all together.
207+
func sendGetEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper, verbose bool) (resp string, err error) {
208+
if m.ID == "" {
209+
// we're using a different Client function, we need to create an ID.
210+
// ce.NewClientHTTP() sets ID if not present, ce.NewClient() doesn't
211+
m.ID = uuid.NewString()
212+
}
213+
214+
// construct the event
215+
event := cloudevents.NewEvent()
216+
event.SetID(m.ID)
217+
event.SetSource(m.Source)
218+
event.SetType(m.Type)
219+
event.SetDataContentType(m.ContentType)
220+
221+
if verbose {
222+
fmt.Println("Constructing a GET request CloudEvent:")
223+
fmt.Printf("Event: %+v\n", event)
224+
}
225+
// create http protocol with GET method
226+
protocol, err := cehttp.New(cehttp.WithRoundTripper(t), cehttp.WithMethod("GET"))
227+
if err != nil {
228+
return
229+
}
230+
231+
c, err := cloudevents.NewClient(protocol)
232+
if err != nil {
233+
return
234+
}
235+
236+
if verbose {
237+
fmt.Printf("Sending event\n%v", event)
238+
}
239+
240+
evt, result := c.Request(cloudevents.ContextWithTarget(ctx, route), event)
241+
if cloudevents.IsUndelivered(result) {
242+
err = fmt.Errorf("unable to invoke: %v", result)
243+
} else if evt != nil { // Check for nil in case no event is returned
244+
resp = evt.String()
245+
}
246+
return
247+
}
248+
179249
// sendPost to the route populated with data in the invoke message.
180-
func sendPost(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper, verbose bool) (map[string][]string, string, error) {
250+
func sendHttp(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper, verbose bool) (map[string][]string, string, error) {
181251
client := http.Client{
182252
Transport: t,
183253
Timeout: time.Minute,
184254
}
185-
values := url.Values{
186-
"ID": {m.ID},
187-
"Source": {m.Source},
188-
"Type": {m.Type},
189-
"ContentType": {m.ContentType},
190-
"Data": {string(m.Data)},
191-
}
255+
192256
if verbose {
257+
values := url.Values{
258+
"ID": {m.ID},
259+
"Source": {m.Source},
260+
"Type": {m.Type},
261+
"ContentType": {m.ContentType},
262+
"Data": {string(m.Data)},
263+
}
193264
fmt.Println("Sending values")
194265
for k, v := range values {
195266
fmt.Printf(" %v: %v\n", k, v[0]) // NOTE len==1 value slices assumed
196267
}
197268
}
198269

199-
req, err := http.NewRequestWithContext(ctx, "POST", route, bytes.NewReader(m.Data))
270+
req, err := http.NewRequestWithContext(ctx, m.RequestType, route, bytes.NewReader(m.Data))
200271
if err != nil {
201272
return nil, "", fmt.Errorf("failure to create request: %w", err)
202273
}
274+
203275
req.Header.Add("Content-Type", m.ContentType)
204276

205277
resp, err := client.Do(req)

0 commit comments

Comments
 (0)