From 7f58ec68a0f314ef918bde25c06e12be772a231d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:53:07 +0200 Subject: [PATCH 1/6] feat: add standard CLI options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- cli.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 cli.go diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..bf66ae6 --- /dev/null +++ b/cli.go @@ -0,0 +1,50 @@ +/* +Copyright 2026 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import "github.com/crossplane/function-sdk-go/logging" + +// CLI provides standard flags and environment variables for Composition +// Functions. Embed it in your own struct to add custom flags. +// +// type CLI struct { +// function.CLI `kong:"embed"` +// MyFlag string `default:"foo" env:"MY_FLAG" help:"My custom flag."` +// } +type CLI struct { + Address string `default:":9443" env:"ADDRESS" help:"Address at which to listen for gRPC connections."` + Debug bool `env:"DEBUG" help:"Emit debug logs in addition to info logs." short:"d"` + Insecure bool `env:"INSECURE" help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` + MaxRecvMessageSize int `default:"4" env:"MAX_RECV_MESSAGE_SIZE" help:"Maximum size of received messages in MB."` + Network string `default:"tcp" env:"NETWORK" help:"Network on which to listen for gRPC connections."` + TLSCertsDir string `env:"TLS_SERVER_CERTS_DIR" help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)."` +} + +// StandardOptions returns the ServeOptions derived from standard CLI flags. +func (c *CLI) StandardOptions() []ServeOption { + return []ServeOption{ + Listen(c.Network, c.Address), + MTLSCertificates(c.TLSCertsDir), + Insecure(c.Insecure), + MaxRecvMessageSize(c.MaxRecvMessageSize * 1024 * 1024), + } +} + +// Logger returns a new logger configured from CLI flags. +func (c *CLI) Logger() (logging.Logger, error) { + return NewLogger(c.Debug) +} From 4fc2cab39490f7b4118ba83b4a9d75805048c2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:37:01 +0200 Subject: [PATCH 2/6] chore: refactor and document usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- cli.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/cli.go b/cli.go index bf66ae6..a2f88fc 100644 --- a/cli.go +++ b/cli.go @@ -16,15 +16,51 @@ limitations under the License. package function -import "github.com/crossplane/function-sdk-go/logging" +import ( + "github.com/alecthomas/kong" + + "github.com/crossplane/function-sdk-go/logging" +) // CLI provides standard flags and environment variables for Composition -// Functions. Embed it in your own struct to add custom flags. +// Functions. It is designed to be used with [github.com/alecthomas/kong]. +// +// Without custom flags, use CLI directly with [Parse]: +// +// type CLI struct { +// function.CLI `kong:"embed"` +// } +// +// func (c *CLI) Run() error { +// log, err := c.Logger() +// if err != nil { +// return err +// } +// return function.Serve(&Function{log: log}, c.StandardOptions()...) +// } +// +// func main() { +// function.Parse(&CLI{}, "My function.") +// } +// +// To add custom flags, add fields to your struct: // // type CLI struct { // function.CLI `kong:"embed"` // MyFlag string `default:"foo" env:"MY_FLAG" help:"My custom flag."` // } +// +// func (c *CLI) Run() error { +// log, err := c.Logger() +// if err != nil { +// return err +// } +// return function.Serve(&Function{log: log, myFlag: c.MyFlag}, c.StandardOptions()...) +// } +// +// func main() { +// function.Parse(&CLI{}, "My function.") +// } type CLI struct { Address string `default:":9443" env:"ADDRESS" help:"Address at which to listen for gRPC connections."` Debug bool `env:"DEBUG" help:"Emit debug logs in addition to info logs." short:"d"` @@ -48,3 +84,14 @@ func (c *CLI) StandardOptions() []ServeOption { func (c *CLI) Logger() (logging.Logger, error) { return NewLogger(c.Debug) } + +// Parse parses CLI flags using kong and runs the command. The cli argument must +// have a Run() error method. An optional description is used as CLI help text. +func Parse(cli any, description ...string) { + options := []kong.Option{} + if len(description) > 0 { + options = append(options, kong.Description(description[0])) + } + ctx := kong.Parse(cli, options...) + ctx.FatalIfErrorf(ctx.Run()) +} From 032f984b067e48023bde18b8f1b07cee154ad003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:46:08 +0200 Subject: [PATCH 3/6] chore: merge both examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- cli.go | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/cli.go b/cli.go index a2f88fc..0cf5010 100644 --- a/cli.go +++ b/cli.go @@ -29,6 +29,7 @@ import ( // // type CLI struct { // function.CLI `kong:"embed"` +// // MyFlag string `default:"foo" env:"MY_FLAG" help:"My custom flag."` // } // // func (c *CLI) Run() error { @@ -37,25 +38,8 @@ import ( // return err // } // return function.Serve(&Function{log: log}, c.StandardOptions()...) -// } -// -// func main() { -// function.Parse(&CLI{}, "My function.") -// } -// -// To add custom flags, add fields to your struct: -// -// type CLI struct { -// function.CLI `kong:"embed"` -// MyFlag string `default:"foo" env:"MY_FLAG" help:"My custom flag."` -// } -// -// func (c *CLI) Run() error { -// log, err := c.Logger() -// if err != nil { -// return err -// } -// return function.Serve(&Function{log: log, myFlag: c.MyFlag}, c.StandardOptions()...) +// // or with custom flags: +// // return function.Serve(&Function{log: log, myFlag: c.MyFlag}, c.StandardOptions()...) // } // // func main() { From 1b1a251ce5df919ccca9b0f659fe4503a6a3ae26 Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Fri, 18 Sep 2026 16:46:55 -0500 Subject: [PATCH 4/6] go mod tidy Signed-off-by: Bob Haddleton --- go.mod | 1 + go.sum | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/go.mod b/go.mod index cc427a8..6cef791 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/crossplane/function-sdk-go go 1.25.11 require ( + github.com/alecthomas/kong v1.16.1 github.com/bufbuild/buf v1.72.0 github.com/crossplane/crossplane-runtime/v2 v2.4.0 github.com/crossplane/crossplane/apis/v2 v2.4.0 diff --git a/go.sum b/go.sum index db81536..398decd 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,12 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.16.1 h1:ixhCt93XkJ98kGposQ54+bl0IK6XwqB40AsMynU7Z8E= +github.com/alecthomas/kong v1.16.1/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -173,6 +179,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/E github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= From 27645934153a14f6e3c8ff8ea7d8e9a463e21308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:20:33 +0200 Subject: [PATCH 5/6] fix: add alias to tls-certs-dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- cli.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cli.go b/cli.go index 0cf5010..ea6111e 100644 --- a/cli.go +++ b/cli.go @@ -46,12 +46,12 @@ import ( // function.Parse(&CLI{}, "My function.") // } type CLI struct { - Address string `default:":9443" env:"ADDRESS" help:"Address at which to listen for gRPC connections."` - Debug bool `env:"DEBUG" help:"Emit debug logs in addition to info logs." short:"d"` - Insecure bool `env:"INSECURE" help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` - MaxRecvMessageSize int `default:"4" env:"MAX_RECV_MESSAGE_SIZE" help:"Maximum size of received messages in MB."` - Network string `default:"tcp" env:"NETWORK" help:"Network on which to listen for gRPC connections."` - TLSCertsDir string `env:"TLS_SERVER_CERTS_DIR" help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)."` + Address string `default:":9443" env:"ADDRESS" help:"Address at which to listen for gRPC connections."` + Debug bool `env:"DEBUG" help:"Emit debug logs in addition to info logs." short:"d"` + Insecure bool `env:"INSECURE" help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` + MaxRecvMessageSize int `default:"4" env:"MAX_RECV_MESSAGE_SIZE" help:"Maximum size of received messages in MB."` + Network string `default:"tcp" env:"NETWORK" help:"Network on which to listen for gRPC connections."` + TLSCertsDir string `aliases:"tls-server-certs-dir" env:"TLS_SERVER_CERTS_DIR" help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)." name:"tls-certs-dir"` } // StandardOptions returns the ServeOptions derived from standard CLI flags. From 228e30ef51658e6eed9aa76837571976a484b89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:58:59 +0200 Subject: [PATCH 6/6] fix: expose max-send-message-size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- cli.go | 18 ++++++++++++------ sdk.go | 13 +++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cli.go b/cli.go index ea6111e..9076a86 100644 --- a/cli.go +++ b/cli.go @@ -46,21 +46,27 @@ import ( // function.Parse(&CLI{}, "My function.") // } type CLI struct { - Address string `default:":9443" env:"ADDRESS" help:"Address at which to listen for gRPC connections."` - Debug bool `env:"DEBUG" help:"Emit debug logs in addition to info logs." short:"d"` - Insecure bool `env:"INSECURE" help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` - MaxRecvMessageSize int `default:"4" env:"MAX_RECV_MESSAGE_SIZE" help:"Maximum size of received messages in MB."` - Network string `default:"tcp" env:"NETWORK" help:"Network on which to listen for gRPC connections."` - TLSCertsDir string `aliases:"tls-server-certs-dir" env:"TLS_SERVER_CERTS_DIR" help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)." name:"tls-certs-dir"` + Address string `default:":9443" env:"ADDRESS" help:"Address at which to listen for gRPC connections."` + Debug bool `env:"DEBUG" help:"Emit debug logs in addition to info logs." short:"d"` + Insecure bool `env:"INSECURE" help:"Run without mTLS credentials. If you supply this flag --tls-server-certs-dir will be ignored."` + MaxRecvMessageSize int `aliases:"max-grpc-message-size" default:"4" env:"MAX_RECV_MESSAGE_SIZE,MAX_GRPC_MESSAGE_SIZE" help:"Maximum size of received messages in MB."` + MaxSendMessageSize int `env:"MAX_SEND_MESSAGE_SIZE" help:"Maximum size of sent messages in MB. Defaults to max-recv-message-size when unset."` + Network string `default:"tcp" env:"NETWORK" help:"Network on which to listen for gRPC connections."` + TLSCertsDir string `aliases:"tls-server-certs-dir" env:"TLS_SERVER_CERTS_DIR" help:"Directory containing server certs (tls.key, tls.crt) and the CA used to verify client certificates (ca.crt)." name:"tls-certs-dir"` } // StandardOptions returns the ServeOptions derived from standard CLI flags. func (c *CLI) StandardOptions() []ServeOption { + sendSize := c.MaxSendMessageSize + if sendSize == 0 { + sendSize = c.MaxRecvMessageSize + } return []ServeOption{ Listen(c.Network, c.Address), MTLSCertificates(c.TLSCertsDir), Insecure(c.Insecure), MaxRecvMessageSize(c.MaxRecvMessageSize * 1024 * 1024), + MaxSendMessageSize(sendSize * 1024 * 1024), } } diff --git a/sdk.go b/sdk.go index 816bc95..1168ff3 100644 --- a/sdk.go +++ b/sdk.go @@ -48,6 +48,7 @@ const ( DefaultNetwork = "tcp" DefaultAddress = ":9443" DefaultMaxRecvMsgSize = 1024 * 1024 * 4 + DefaultMaxSendMsgSize = 1024 * 1024 * 4 DefaultMetricsAddress = ":8080" ) @@ -56,6 +57,7 @@ type ServeOptions struct { Network string Address string MaxRecvMsgSize int + MaxSendMsgSize int Credentials credentials.TransportCredentials HealthServer healthgrpc.HealthServer @@ -142,6 +144,15 @@ func MaxRecvMessageSize(sz int) ServeOption { } } +// MaxSendMessageSize returns a ServeOption to set the max message size in bytes the server can send. +// If this is not set, gRPC uses the default limit. +func MaxSendMessageSize(sz int) ServeOption { + return func(o *ServeOptions) error { + o.MaxSendMsgSize = sz + return nil + } +} + // WithHealthServer lets the server start with a health server that can be called // to verify that the server is ready to accept connections. // @@ -191,6 +202,7 @@ func Serve(fn v1.FunctionRunnerServiceServer, o ...ServeOption) error { Network: DefaultNetwork, Address: DefaultAddress, MaxRecvMsgSize: DefaultMaxRecvMsgSize, + MaxSendMsgSize: DefaultMaxSendMsgSize, MetricsAddress: DefaultMetricsAddress, MetricsRegistry: prometheus.DefaultRegisterer.(*prometheus.Registry), // Use default registry } @@ -214,6 +226,7 @@ func Serve(fn v1.FunctionRunnerServiceServer, o ...ServeOption) error { // Create server options serverOpts := []grpc.ServerOption{ grpc.MaxRecvMsgSize(so.MaxRecvMsgSize), + grpc.MaxSendMsgSize(so.MaxSendMsgSize), grpc.Creds(so.Credentials), }