diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..9076a86 --- /dev/null +++ b/cli.go @@ -0,0 +1,87 @@ +/* +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/alecthomas/kong" + + "github.com/crossplane/function-sdk-go/logging" +) + +// CLI provides standard flags and environment variables for Composition +// 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"` +// // 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}, c.StandardOptions()...) +// // or with custom flags: +// // 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"` + 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), + } +} + +// Logger returns a new logger configured from CLI flags. +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()) +} diff --git a/go.mod b/go.mod index 487cad3..7c00ab7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/crossplane/function-sdk-go go 1.26.7 require ( + github.com/alecthomas/kong v1.16.1 github.com/bufbuild/buf v1.73.0 github.com/crossplane/crossplane-runtime/v2 v2.4.0 github.com/crossplane/crossplane/apis/v2 v2.4.1 diff --git a/go.sum b/go.sum index 920ab5d..ff2ed14 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,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= @@ -175,6 +181,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= 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), }