mirror of https://github.com/knative/func.git
210 lines
6.1 KiB
Go
210 lines
6.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/ioutil"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/ory/viper"
|
|
"github.com/spf13/cobra"
|
|
fn "knative.dev/kn-plugin-func"
|
|
"knative.dev/kn-plugin-func/cloudevents"
|
|
fnhttp "knative.dev/kn-plugin-func/http"
|
|
"knative.dev/kn-plugin-func/knative"
|
|
)
|
|
|
|
func init() {
|
|
root.AddCommand(NewEmitCmd(newEmitClient))
|
|
}
|
|
|
|
// create a fn.Client with an instance of a
|
|
func newEmitClient(cfg emitConfig) (*fn.Client, error) {
|
|
e := cloudevents.NewEmitter()
|
|
e.Id = cfg.Id
|
|
e.Source = cfg.Source
|
|
e.Type = cfg.Type
|
|
e.ContentType = cfg.ContentType
|
|
e.Data = cfg.Data
|
|
if e.Transport != nil {
|
|
e.Transport = cfg.Transport
|
|
}
|
|
if cfg.File != "" {
|
|
// See config.Validate for --Data and --file exclusivity enforcement
|
|
b, err := ioutil.ReadFile(cfg.File)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Data = string(b)
|
|
}
|
|
|
|
return fn.New(fn.WithEmitter(e)), nil
|
|
}
|
|
|
|
type emitClientFn func(emitConfig) (*fn.Client, error)
|
|
|
|
func NewEmitCmd(clientFn emitClientFn) *cobra.Command {
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "emit",
|
|
Short: "Emit a CloudEvent to a function endpoint",
|
|
Long: `Emit event
|
|
|
|
Emits a CloudEvent, sending it to the deployed function.
|
|
`,
|
|
Example: `
|
|
# Send a CloudEvent to the deployed function with no data and default values
|
|
# for source, type and ID
|
|
kn func emit
|
|
|
|
# Send a CloudEvent to the deployed function with the data found in ./test.json
|
|
kn func emit --file ./test.json
|
|
|
|
# Send a CloudEvent to the function running locally with a CloudEvent containing
|
|
# "Hello World!" as the data field, with a content type of "text/plain"
|
|
kn func emit --data "Hello World!" --content-type "text/plain" -s local
|
|
|
|
# Send a CloudEvent to the function running locally with an event type of "my.event"
|
|
kn func emit --type my.event --sink local
|
|
|
|
# Send a CloudEvent to the deployed function found at /path/to/fn with an id of "fn.test"
|
|
kn func emit --path /path/to/fn -i fn.test
|
|
|
|
# Send a CloudEvent to an arbitrary endpoint
|
|
kn func emit --sink "http://my.event.broker.com"
|
|
`,
|
|
SuggestFor: []string{"meit", "emti", "send"},
|
|
PreRunE: bindEnv("source", "type", "id", "data", "file", "path", "sink", "content-type"),
|
|
}
|
|
|
|
cmd.Flags().StringP("sink", "k", "", "Send the CloudEvent to the function running at [sink]. The special value \"local\" can be used to send the event to a function running on the local host. When provided, the --path flag is ignored (Env: $FUNC_SINK)")
|
|
cmd.Flags().StringP("source", "s", cloudevents.DefaultSource, "CloudEvent source (Env: $FUNC_SOURCE)")
|
|
cmd.Flags().StringP("type", "t", cloudevents.DefaultType, "CloudEvent type (Env: $FUNC_TYPE)")
|
|
cmd.Flags().StringP("id", "i", uuid.NewString(), "CloudEvent ID (Env: $FUNC_ID)")
|
|
cmd.Flags().StringP("data", "d", "", "Any arbitrary string to be sent as the CloudEvent data. Ignored if --file is provided (Env: $FUNC_DATA)")
|
|
cmd.Flags().StringP("file", "f", "", "Path to a local file containing CloudEvent data to be sent (Env: $FUNC_FILE)")
|
|
cmd.Flags().StringP("content-type", "c", "application/json", "The MIME Content-Type for the CloudEvent data (Env: $FUNC_CONTENT_TYPE)")
|
|
setPathFlag(cmd)
|
|
|
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
|
return runEmit(cmd, args, clientFn)
|
|
}
|
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
func runEmit(cmd *cobra.Command, _ []string, clientFn emitClientFn) (err error) {
|
|
config := newEmitConfig()
|
|
|
|
// Validate things like invalid config combinations.
|
|
if err := config.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Determine the final endpoint, taking into account the special value "local",
|
|
// and sampling the function's current route if not explicitly provided
|
|
endpoint, err := endpoint(cmd.Context(), config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Instantiate a client based on the final value of config
|
|
transport := fnhttp.NewRoundTripper()
|
|
defer transport.Close()
|
|
config.Transport = transport
|
|
client, err := clientFn(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Emit the event to the endpoint
|
|
return client.Emit(cmd.Context(), endpoint)
|
|
}
|
|
|
|
// endpoint returns the final effective endpoint.
|
|
// By default, the contextually active Function is queried for it's current
|
|
// address (route).
|
|
// If "local" is specified in cfg.Sink, localhost is used.
|
|
// Otherwise the value of Sink is used verbatim if defined.
|
|
func endpoint(ctx context.Context, cfg emitConfig) (url string, err error) {
|
|
var (
|
|
f fn.Function
|
|
d fn.Describer
|
|
i fn.Info
|
|
)
|
|
|
|
// If the special value "local" was requested,
|
|
// use localhost.
|
|
if cfg.Sink == "local" {
|
|
return "http://localhost:8080", nil
|
|
}
|
|
|
|
// If a sink was expressly provided, use that verbatim
|
|
if cfg.Sink != "" {
|
|
return cfg.Sink, nil
|
|
}
|
|
|
|
// If no sink was specified, use the route to the currently
|
|
// contectually active function
|
|
if f, err = fn.NewFunction(cfg.Path); err != nil {
|
|
return
|
|
}
|
|
|
|
// TODO: Decide what happens if the function hasn't been deployed but they
|
|
// don't run with --local=true. Perhaps an error in .Validate()?
|
|
if d, err = knative.NewDescriber(""); err != nil {
|
|
return
|
|
}
|
|
|
|
// Get the current state of the function.
|
|
if i, err = d.Describe(ctx, f.Name); err != nil {
|
|
return
|
|
}
|
|
|
|
// Probably wise to be defensive here:
|
|
if len(i.Routes) == 0 {
|
|
err = errors.New("function has no active routes")
|
|
return
|
|
}
|
|
|
|
// The first route should be the destination.
|
|
return i.Routes[0], nil
|
|
}
|
|
|
|
type emitConfig struct {
|
|
Path string
|
|
Source string
|
|
Type string
|
|
Id string
|
|
Data string
|
|
File string
|
|
ContentType string
|
|
Sink string
|
|
Verbose bool
|
|
Transport http.RoundTripper
|
|
}
|
|
|
|
func newEmitConfig() emitConfig {
|
|
return emitConfig{
|
|
Path: viper.GetString("path"),
|
|
Source: viper.GetString("source"),
|
|
Type: viper.GetString("type"),
|
|
Id: viper.GetString("id"),
|
|
Data: viper.GetString("data"),
|
|
File: viper.GetString("file"),
|
|
ContentType: viper.GetString("content-type"),
|
|
Sink: viper.GetString("sink"),
|
|
Verbose: viper.GetBool("verbose"),
|
|
}
|
|
}
|
|
|
|
func (c emitConfig) Validate() error {
|
|
if c.Data != "" && c.File != "" {
|
|
return errors.New("Only one of --data or --file may be specified")
|
|
}
|
|
// TODO: should we verify that sink is a url or "local"?
|
|
return nil
|
|
}
|