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/7] 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/7] 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/7] 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/7] 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/7] 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/7] 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), } From 34cd7476d11fb1285884ebdac7568297bcba2de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:10:39 +0200 Subject: [PATCH 7/7] fix: PR review 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> --- README.md | 29 ++++++++++ cli.go | 37 ++++++++---- cli_test.go | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 cli_test.go diff --git a/README.md b/README.md index f425604..6b5c96e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,35 @@ To learn how to use this SDK: If you just want to jump in and get started, consider using the [function-template-go] template repository. +## Standard CLI options + +Embed `function.CLI` to get the standard flags and environment variables +(`--address`, `--debug`, `--insecure`, `--tls-certs-dir`, `--ttl`, and more). +Add your own flags alongside it: + +```go +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 + } + // Use c.TTL with response.To(req, c.TTL) in RunFunction. + return function.Serve(&Function{log: log, ttl: c.TTL, myFlag: c.MyFlag}, c.StandardOptions()...) +} + +func main() { + function.Parse(&CLI{}, "My function.") +} +``` + +See the [package docs] for the full list of options. + [go]: https://go.dev [functions]: https://docs.crossplane.io/latest/concepts/composition-functions [contributing guidelines]: https://github.com/crossplane/crossplane/tree/master/contributing diff --git a/cli.go b/cli.go index 9076a86..28d62d6 100644 --- a/cli.go +++ b/cli.go @@ -17,9 +17,12 @@ limitations under the License. package function import ( + "time" + "github.com/alecthomas/kong" "github.com/crossplane/function-sdk-go/logging" + "github.com/crossplane/function-sdk-go/response" ) // CLI provides standard flags and environment variables for Composition @@ -37,22 +40,24 @@ import ( // if err != nil { // return err // } -// return function.Serve(&Function{log: log}, c.StandardOptions()...) +// // Use c.TTL with response.To(req, c.TTL) in RunFunction. +// return function.Serve(&Function{log: log, ttl: c.TTL}, c.StandardOptions()...) // // or with custom flags: -// // return function.Serve(&Function{log: log, myFlag: c.MyFlag}, c.StandardOptions()...) +// // return function.Serve(&Function{log: log, ttl: c.TTL, 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"` - 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"` + 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."` + TTL time.Duration `default:"${default_ttl}" env:"TTL" help:"TTL for which a response can be cached. Pass it to response.To. Set to 0 to disable caching."` + 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. @@ -61,13 +66,16 @@ func (c *CLI) StandardOptions() []ServeOption { if sendSize == 0 { sendSize = c.MaxRecvMessageSize } - return []ServeOption{ + opts := []ServeOption{ Listen(c.Network, c.Address), - MTLSCertificates(c.TLSCertsDir), Insecure(c.Insecure), MaxRecvMessageSize(c.MaxRecvMessageSize * 1024 * 1024), MaxSendMessageSize(sendSize * 1024 * 1024), } + if !c.Insecure { + opts = append(opts, MTLSCertificates(c.TLSCertsDir)) + } + return opts } // Logger returns a new logger configured from CLI flags. @@ -75,10 +83,15 @@ func (c *CLI) Logger() (logging.Logger, error) { return NewLogger(c.Debug) } +// kongVars returns the variables interpolated into CLI struct tags. +func kongVars() kong.Vars { + return kong.Vars{"default_ttl": response.DefaultTTL.String()} +} + // 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{} + options := []kong.Option{kongVars()} if len(description) > 0 { options = append(options, kong.Description(description[0])) } diff --git a/cli_test.go b/cli_test.go new file mode 100644 index 0000000..478bccb --- /dev/null +++ b/cli_test.go @@ -0,0 +1,162 @@ +/* +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 ( + "testing" + "time" + + "github.com/alecthomas/kong" + "github.com/google/go-cmp/cmp" + + "github.com/crossplane/function-sdk-go/response" +) + +func TestCLIParse(t *testing.T) { + type want struct { + cli CLI + maxRecvMsgSize int + maxSendMsgSize int + } + + defaults := CLI{ + Address: ":9443", + MaxRecvMessageSize: 4, + Network: "tcp", + TTL: response.DefaultTTL, + } + + cases := map[string]struct { + reason string + args []string + env map[string]string + want want + }{ + "Defaults": { + reason: "With no flags or environment variables we should use the defaults.", + want: want{ + cli: defaults, + maxRecvMsgSize: 4 * 1024 * 1024, + maxSendMsgSize: 4 * 1024 * 1024, + }, + }, + "Flags": { + reason: "We should accept the standard flag names.", + args: []string{ + "--address=:8080", "--debug", "--insecure", "--network=unix", + "--max-recv-message-size=8", "--max-send-message-size=2", + "--ttl=30s", "--tls-certs-dir=/certs", + }, + want: want{ + cli: CLI{ + Address: ":8080", + Debug: true, + Insecure: true, + MaxRecvMessageSize: 8, + MaxSendMessageSize: 2, + Network: "unix", + TTL: 30 * time.Second, + TLSCertsDir: "/certs", + }, + maxRecvMsgSize: 8 * 1024 * 1024, + maxSendMsgSize: 2 * 1024 * 1024, + }, + }, + "Aliases": { + reason: "We should accept the flag names existing functions already use.", + args: []string{"--insecure", "--max-grpc-message-size=16", "--tls-server-certs-dir=/certs"}, + want: want{ + cli: func() CLI { + c := defaults + c.Insecure = true + c.MaxRecvMessageSize = 16 + c.TLSCertsDir = "/certs" + return c + }(), + maxRecvMsgSize: 16 * 1024 * 1024, + maxSendMsgSize: 16 * 1024 * 1024, + }, + }, + "EnvironmentVariables": { + reason: "We should accept the environment variables existing functions already use.", + env: map[string]string{ + "INSECURE": "true", + "MAX_GRPC_MESSAGE_SIZE": "16", + "TLS_SERVER_CERTS_DIR": "/certs", + "TTL": "0s", + }, + want: want{ + cli: func() CLI { + c := defaults + c.Insecure = true + c.MaxRecvMessageSize = 16 + c.TLSCertsDir = "/certs" + c.TTL = 0 + return c + }(), + maxRecvMsgSize: 16 * 1024 * 1024, + maxSendMsgSize: 16 * 1024 * 1024, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + for k, v := range tc.env { + t.Setenv(k, v) + } + + c := &CLI{} + p, err := kong.New(c, kongVars()) + if err != nil { + t.Fatalf("\n%s\nkong.New(): unexpected error: %v", tc.reason, err) + } + if _, err := p.Parse(tc.args); err != nil { + t.Fatalf("\n%s\nParse(%v): unexpected error: %v", tc.reason, tc.args, err) + } + if diff := cmp.Diff(tc.want.cli, *c); diff != "" { + t.Errorf("\n%s\nParse(%v): -want, +got:\n%s", tc.reason, tc.args, diff) + } + + so := &ServeOptions{} + for _, o := range c.StandardOptions() { + if err := o(so); err != nil { + t.Fatalf("\n%s\nStandardOptions(): unexpected error: %v", tc.reason, err) + } + } + if so.MaxRecvMsgSize != tc.want.maxRecvMsgSize { + t.Errorf("\n%s\nStandardOptions(): MaxRecvMsgSize: want %d, got %d", tc.reason, tc.want.maxRecvMsgSize, so.MaxRecvMsgSize) + } + if so.MaxSendMsgSize != tc.want.maxSendMsgSize { + t.Errorf("\n%s\nStandardOptions(): MaxSendMsgSize: want %d, got %d", tc.reason, tc.want.maxSendMsgSize, so.MaxSendMsgSize) + } + }) + } +} + +func TestStandardOptionsInsecureIgnoresTLSCertsDir(t *testing.T) { + c := &CLI{Insecure: true, TLSCertsDir: "/does/not/exist", MaxRecvMessageSize: 4} + so := &ServeOptions{} + for _, o := range c.StandardOptions() { + if err := o(so); err != nil { + t.Fatalf("StandardOptions() with --insecure: unexpected error: %v", err) + } + } + if so.Credentials == nil { + t.Fatal("StandardOptions() with --insecure: expected insecure credentials") + } +}