From 5a8531d5f4eab350a4d897ac4a9b1b7b785d82e9 Mon Sep 17 00:00:00 2001 From: Matt Dering Date: Mon, 12 Jan 2026 11:30:31 -0500 Subject: [PATCH 1/2] Add support for CloudEvents extensions in invoke command Introduces a new --extension/-e flag to the invoke command, allowing users to specify CloudEvents extensions as key=value pairs. The extensions are parsed and included in the InvokeMessage, and are set on the outgoing CloudEvent. Documentation is updated to reflect the new flag. --- cmd/invoke.go | 20 +++++++++++++++++++- docs/reference/func_invoke.md | 1 + pkg/functions/invoke.go | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cmd/invoke.go b/cmd/invoke.go index 2a60bc2f39..bfe122cece 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -106,7 +106,7 @@ EXAMPLES "onvoke", "unvoke", "knvoke", "imvoke", "ihvoke", "ibvoke"}, PreRunE: bindEnv("path", "format", "target", "id", "source", "type", "data", "content-type", "request-type", "file", "insecure", - "confirm", "verbose"), + "confirm", "verbose", "extension"), RunE: func(cmd *cobra.Command, args []string) error { return runInvoke(cmd, args, newClient) }, @@ -129,6 +129,7 @@ EXAMPLES cmd.Flags().StringP("data", "", fn.DefaultInvokeData, "Data to send in the request. ($FUNC_DATA)") 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)") cmd.Flags().BoolP("insecure", "i", false, "Allow insecure server connections when using SSL. ($FUNC_INSECURE)") + cmd.Flags().StringSliceP("extension", "e", nil, "Extensions as key=value pairs. Can be repeated. cloudevents only ($FUNC_EXTENSION)") addConfirmFlag(cmd, cfg.Confirm) addPathFlag(cmd) addVerboseFlag(cmd, cfg.Verbose) @@ -169,6 +170,19 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err client, done := newClient(ClientConfig{Verbose: cfg.Verbose, InsecureSkipVerify: cfg.Insecure}) defer done() + // Build extensions map + if cfg.Extensions == nil { + cfg.Extensions = []string{} + } + extensionsMap := make(map[string]string) + for _, ext := range cfg.Extensions { + parts := strings.SplitN(ext, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid extension format: %q, expected key=value", ext) + } + extensionsMap[parts[0]] = parts[1] + } + // Message to send the running function built from parameters gathered // from the user (or defaults) m := fn.InvokeMessage{ @@ -179,6 +193,7 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err RequestType: strings.ToUpper(cfg.RequestType), Data: cfg.Data, Format: cfg.Format, + Extensions: extensionsMap, } // If --file was specified, use its content for message data @@ -239,6 +254,7 @@ type invokeConfig struct { Confirm bool Verbose bool Insecure bool + Extensions []string } func newInvokeConfig() (cfg invokeConfig, err error) { @@ -256,6 +272,7 @@ func newInvokeConfig() (cfg invokeConfig, err error) { Confirm: viper.GetBool("confirm"), Verbose: viper.GetBool("verbose"), Insecure: viper.GetBool("insecure"), + Extensions: viper.GetStringSlice("extension"), } // If file was passed, read it in as data @@ -293,6 +310,7 @@ func newInvokeConfig() (cfg invokeConfig, err error) { fmt.Printf("Content Type: %v\n", cfg.ContentType) fmt.Printf("File: %v\n", cfg.File) fmt.Printf("Insecure: %v\n", cfg.Insecure) + fmt.Printf("Extensions: %v\n", cfg.Extensions) return } diff --git a/docs/reference/func_invoke.md b/docs/reference/func_invoke.md index 0ecaeb3529..21cf277cdb 100644 --- a/docs/reference/func_invoke.md +++ b/docs/reference/func_invoke.md @@ -99,6 +99,7 @@ func invoke -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) --content-type string Content Type of the data. ($FUNC_CONTENT_TYPE) (default "application/json") --data string Data to send in the request. ($FUNC_DATA) (default "{\"message\":\"Hello World\"}") + -e, --extension strings Extensions as key=value pairs. Can be repeated. cloudevents only ($FUNC_EXTENSION) --file string Path to a file to use as data. Overrides --data flag and should be sent with a correct --content-type. ($FUNC_FILE) -f, --format string Format of message to send, 'http' or 'cloudevent(s)'. Default is to choose automatically. ($FUNC_FORMAT) -h, --help help for invoke diff --git a/pkg/functions/invoke.go b/pkg/functions/invoke.go index e0331f7f98..45d08e16ba 100644 --- a/pkg/functions/invoke.go +++ b/pkg/functions/invoke.go @@ -35,6 +35,7 @@ type InvokeMessage struct { Data []byte RequestType string // HTTP Request GET/POST (defaults to POST) Format string // optional override for function-defined message format + Extensions map[string]string } // NewInvokeMessage creates a new InvokeMessage with fields populated @@ -168,6 +169,9 @@ func sendEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundT event.SetID(m.ID) event.SetSource(m.Source) event.SetType(m.Type) + for extension := range m.Extensions { + event.SetExtension(extension, m.Extensions[extension]) + } err = event.SetData(m.ContentType, (m.Data)) if err != nil { return "", fmt.Errorf("cannot set data: %w", err) From f50cb881aa5a8eb01c11b26a5b041674c686eeb1 Mon Sep 17 00:00:00 2001 From: Matt Dering Date: Thu, 7 May 2026 13:50:38 -0400 Subject: [PATCH 2/2] address feedback --- cmd/invoke.go | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/cmd/invoke.go b/cmd/invoke.go index bfe122cece..9ff96d37fa 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -166,23 +166,24 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err return fmt.Errorf("no function found in current directory.\nYou need to be inside a function directory to invoke it.\n\nTry this:\n func create --language go myfunction Create a new function\n cd myfunction Go into the function directory\n func invoke Now you can invoke it\n\nOr if you have an existing function:\n cd path/to/your/function Go to your function directory\n func invoke Invoke the function") } + // If extensions were provided, ensure the format is cloudevent, otherwise return an error. + if len(cfg.Extensions) > 0 { + effectiveFormat := cfg.Format + if effectiveFormat == "" { + effectiveFormat = f.Invoke + } + if effectiveFormat != "cloudevent" { + return fmt.Errorf("--extension (-e) is only valid with cloudevents") + } + if effectiveFormat != "" && effectiveFormat != "cloudevent" { + return fmt.Errorf("--extension flag is only valid with cloudevent format") + } + } + // Client instance from env vars, flags, args and user prompts (if --confirm) client, done := newClient(ClientConfig{Verbose: cfg.Verbose, InsecureSkipVerify: cfg.Insecure}) defer done() - // Build extensions map - if cfg.Extensions == nil { - cfg.Extensions = []string{} - } - extensionsMap := make(map[string]string) - for _, ext := range cfg.Extensions { - parts := strings.SplitN(ext, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid extension format: %q, expected key=value", ext) - } - extensionsMap[parts[0]] = parts[1] - } - // Message to send the running function built from parameters gathered // from the user (or defaults) m := fn.InvokeMessage{ @@ -193,7 +194,7 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err RequestType: strings.ToUpper(cfg.RequestType), Data: cfg.Data, Format: cfg.Format, - Extensions: extensionsMap, + Extensions: cfg.extensionsMap(), } // If --file was specified, use its content for message data @@ -314,6 +315,17 @@ func newInvokeConfig() (cfg invokeConfig, err error) { return } +func (c invokeConfig) extensionsMap() map[string]string { + extensionsMap := make(map[string]string) + for _, ext := range c.Extensions { + parts := strings.SplitN(ext, "=", 2) + if len(parts) == 2 { + extensionsMap[parts[0]] = parts[1] + } + } + return extensionsMap +} + func (c invokeConfig) prompt() (invokeConfig, error) { var qs []*survey.Question