github.com/anycable/anycable-go@v1.5.1/cli/options.go (about) 1 package cli 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 8 "github.com/anycable/anycable-go/config" 9 "github.com/anycable/anycable-go/node" 10 "github.com/anycable/anycable-go/version" 11 "github.com/nats-io/nats.go" 12 "github.com/urfave/cli/v2" 13 ) 14 15 type cliOption func(*cli.App) error 16 17 type customOptionsFactory = func() ([]cli.Flag, error) 18 19 func WithCLIName(name string) cliOption { 20 return func(app *cli.App) error { 21 app.Name = name 22 return nil 23 } 24 } 25 26 func WithCLIVersion(str string) cliOption { 27 return func(app *cli.App) error { 28 app.Version = str 29 return nil 30 } 31 } 32 33 func WithCLIUsageHeader(desc string) cliOption { 34 return func(app *cli.App) error { 35 app.Usage = desc 36 return nil 37 } 38 } 39 40 func WithCLICustomOptions(factory customOptionsFactory) cliOption { 41 return func(app *cli.App) error { 42 custom, err := factory() 43 if err != nil { 44 return err 45 } 46 47 app.Flags = append(app.Flags, custom...) 48 return nil 49 } 50 } 51 52 // NewConfigFromCLI reads config from os.Args. It returns config, error (if any) and a bool value 53 // indicating that the usage message or version was shown, no further action required. 54 func NewConfigFromCLI(args []string, opts ...cliOption) (*config.Config, error, bool) { 55 c := config.NewConfig() 56 57 var path, headers, cookieFilter, mtags string 58 var helpOrVersionWereShown = true 59 var metricsFilter string 60 var enatsRoutes, enatsGateways string 61 var presets string 62 var turboRailsKey, cableReadyKey string 63 var turboRailsClearText, cableReadyClearText bool 64 var jwtIdKey, jwtIdParam string 65 var jwtIdEnforce bool 66 var noRPC bool 67 var isPublic bool 68 69 // Print raw version without prefix 70 cli.VersionPrinter = func(cCtx *cli.Context) { 71 _, _ = fmt.Fprintf(cCtx.App.Writer, "%v\n", cCtx.App.Version) 72 } 73 74 flags := []cli.Flag{} 75 flags = append(flags, serverCLIFlags(&c, &path, &isPublic)...) 76 flags = append(flags, sslCLIFlags(&c)...) 77 flags = append(flags, broadcastCLIFlags(&c)...) 78 flags = append(flags, brokerCLIFlags(&c)...) 79 flags = append(flags, redisCLIFlags(&c)...) 80 flags = append(flags, httpBroadcastCLIFlags(&c)...) 81 flags = append(flags, natsCLIFlags(&c)...) 82 flags = append(flags, rpcCLIFlags(&c, &headers, &cookieFilter, &noRPC)...) 83 flags = append(flags, disconnectorCLIFlags(&c)...) 84 flags = append(flags, logCLIFlags(&c)...) 85 flags = append(flags, metricsCLIFlags(&c, &metricsFilter, &mtags)...) 86 flags = append(flags, wsCLIFlags(&c)...) 87 flags = append(flags, pingCLIFlags(&c)...) 88 flags = append(flags, jwtCLIFlags(&c, &jwtIdKey, &jwtIdParam, &jwtIdEnforce)...) 89 flags = append(flags, signedStreamsCLIFlags(&c, &turboRailsKey, &cableReadyKey, &turboRailsClearText, &cableReadyClearText)...) 90 flags = append(flags, statsdCLIFlags(&c)...) 91 flags = append(flags, embeddedNatsCLIFlags(&c, &enatsRoutes, &enatsGateways)...) 92 flags = append(flags, sseCLIFlags(&c)...) 93 flags = append(flags, miscCLIFlags(&c, &presets)...) 94 95 app := &cli.App{ 96 Name: "anycable-go", 97 Version: version.Version(), 98 Usage: "AnyCable-Go, The WebSocket server for https://anycable.io", 99 HideHelpCommand: true, 100 Flags: flags, 101 Action: func(nc *cli.Context) error { 102 helpOrVersionWereShown = false 103 return nil 104 }, 105 } 106 107 for _, o := range opts { 108 err := o(app) 109 if err != nil { 110 return &config.Config{}, err, false 111 } 112 } 113 114 err := app.Run(args) 115 if err != nil { 116 return &config.Config{}, err, false 117 } 118 119 // helpOrVersionWereShown = false indicates that the default action has been run. 120 // true means that help/version message was displayed. 121 // 122 // Unfortunately, cli module does not support another way of detecting if or which 123 // command was run. 124 if helpOrVersionWereShown { 125 return &config.Config{}, nil, true 126 } 127 128 if path != "" { 129 c.Path = strings.Split(path, ",") 130 } 131 132 c.Headers = strings.Split(strings.ToLower(headers), ",") 133 134 if len(cookieFilter) > 0 { 135 c.Cookies = strings.Split(cookieFilter, ",") 136 } 137 138 if c.Debug { 139 c.LogLevel = "debug" 140 } 141 142 if c.Metrics.Port == 0 { 143 c.Metrics.Port = c.Port 144 } 145 146 if mtags != "" { 147 c.Metrics.Tags = parseTags(mtags) 148 } 149 150 if c.Metrics.LogInterval > 0 { 151 fmt.Println(`DEPRECATION WARNING: metrics_log_interval option is deprecated 152 and will be deleted in the next major release of anycable-go. 153 Use metrics_rotate_interval instead.`) 154 155 if c.Metrics.RotateInterval == 0 { 156 c.Metrics.RotateInterval = c.Metrics.LogInterval 157 } 158 } 159 160 if metricsFilter != "" { 161 c.Metrics.LogFilter = strings.Split(metricsFilter, ",") 162 } 163 164 if enatsRoutes != "" { 165 c.EmbeddedNats.Routes = strings.Split(enatsRoutes, ",") 166 } 167 168 if enatsGateways != "" { 169 c.EmbeddedNats.Gateways = strings.Split(enatsGateways, ";") 170 } 171 172 if presets != "" { 173 c.UserPresets = strings.Split(presets, ",") 174 } 175 176 // Automatically set the URL of the embedded NATS as the pub/sub server URL 177 if c.EmbedNats && c.NATS.Servers == nats.DefaultURL { 178 c.NATS.Servers = c.EmbeddedNats.ServiceAddr 179 } 180 181 if c.DisconnectorDisabled { 182 fmt.Println(`DEPRECATION WARNING: disable_disconnect option is deprecated 183 and will be removed in the next major release of anycable-go. 184 Use disconnect_mode=never instead.`) 185 186 c.App.DisconnectMode = node.DISCONNECT_MODE_NEVER 187 } 188 189 if c.DisconnectQueue.ShutdownTimeout > 0 { 190 fmt.Println(`DEPRECATION WARNING: disconnect_timeout option is deprecated 191 and will be removed in the next major release of anycable-go. 192 Use shutdown_timeout instead.`) 193 } 194 195 c.SSE.AllowedOrigins = c.WS.AllowedOrigins 196 c.HTTPBroadcast.CORSHosts = c.WS.AllowedOrigins 197 198 if turboRailsKey != "" { 199 fmt.Println(`DEPRECATION WARNING: turbo_rails_key option is deprecated 200 and will be removed in the next major release of anycable-go. 201 Use turbo_streams_secret instead.`) 202 203 c.Streams.TurboSecret = turboRailsKey 204 205 c.Streams.Turbo = true 206 } 207 208 if turboRailsClearText { 209 fmt.Println(`DEPRECATION WARNING: turbo_rails_cleartext option is deprecated 210 and will be removed in the next major release of anycable-go. 211 It has no effect anymore, use public streams instead.`) 212 } 213 214 if cableReadyKey != "" { 215 fmt.Println(`DEPRECATION WARNING: cable_ready_key option is deprecated 216 and will be removed in the next major release of anycable-go. 217 Use cable_ready_secret instead.`) 218 219 c.Streams.CableReadySecret = cableReadyKey 220 221 c.Streams.CableReady = true 222 } 223 224 if cableReadyClearText { 225 fmt.Println(`DEPRECATION WARNING: cable_ready_cleartext option is deprecated 226 and will be removed in the next major release of anycable-go. 227 It has no effect anymore, use public streams instead.`) 228 } 229 230 if jwtIdKey != "" { 231 fmt.Println(`DEPRECATION WARNING: jwt_id_key option is deprecated 232 and will be removed in the next major release of anycable-go. 233 Use jwt_secret instead.`) 234 235 if c.JWT.Secret == "" { 236 c.JWT.Secret = jwtIdKey 237 } 238 } 239 240 if jwtIdParam != "" { 241 fmt.Println(`DEPRECATION WARNING: jwt_id_param option is deprecated 242 and will be removed in the next major release of anycable-go. 243 Use jwt_param instead.`) 244 245 if c.JWT.Param == "" { 246 c.JWT.Param = jwtIdParam 247 } 248 } 249 250 if jwtIdEnforce { 251 fmt.Println(`DEPRECATION WARNING: jwt_id_enforce option is deprecated 252 and will be removed in the next major release of anycable-go. 253 Use enfore_jwt instead.`) 254 255 c.JWT.Force = true 256 } 257 258 // Configure RPC 259 if noRPC { 260 c.RPC.Implementation = "none" 261 } 262 263 // Legacy HTTP authentication stuff 264 if c.HTTPBroadcast.Secret != "" { 265 fmt.Println(`DEPRECATION WARNING: http_broadcast_secret option is deprecated 266 and will be removed in the next major release of anycable-go. 267 Use broadcast_key instead.`) 268 } 269 270 if c.HTTPBroadcast.Secret == "" { 271 c.HTTPBroadcast.Secret = c.BroadcastKey 272 } 273 274 // Fallback secrets 275 if c.Secret != "" { 276 if c.Streams.Secret == "" { 277 c.Streams.Secret = c.Secret 278 } 279 280 if c.JWT.Secret == "" { 281 c.JWT.Secret = c.Secret 282 } 283 284 if c.HTTPBroadcast.Secret == "" { 285 c.HTTPBroadcast.SecretBase = c.Secret 286 } 287 288 if c.RPC.Secret == "" { 289 c.RPC.SecretBase = c.Secret 290 } 291 } 292 293 // Nullify none secrets 294 if c.Streams.Secret == "none" { 295 c.Streams.Secret = "" 296 } 297 298 if c.JWT.Secret == "none" { 299 c.JWT.Secret = "" 300 } 301 302 if c.RPC.Secret == "none" { 303 c.RPC.Secret = "" 304 } 305 306 if c.HTTPBroadcast.Secret == "none" { 307 c.HTTPBroadcast.Secret = "" 308 } 309 310 // Configure default HTTP port 311 if c.HTTPBroadcast.Port == 0 { 312 if c.HTTPBroadcast.IsSecured() { 313 c.HTTPBroadcast.Port = c.Port 314 } else { 315 c.HTTPBroadcast.Port = 8090 316 } 317 } 318 319 // Configure public mode and other insecure features 320 if isPublic { 321 c.SkipAuth = true 322 c.Streams.Public = true 323 // Ensure broadcasting is also public 324 c.HTTPBroadcast.Secret = "" 325 c.HTTPBroadcast.SecretBase = "" 326 } 327 328 return &c, nil, false 329 } 330 331 // NewConfig returns a new AnyCable configuration combining default values and values from the environment. 332 func NewConfig() *config.Config { 333 c, err, _ := NewConfigFromCLI([]string{}) 334 335 if err != nil { 336 panic(err) 337 } 338 339 return c 340 } 341 342 // Flags ordering issue: https://github.com/urfave/cli/pull/1430 343 344 const ( 345 serverCategoryDescription = "ANYCABLE-GO SERVER:" 346 sslCategoryDescription = "SSL:" 347 broadcastCategoryDescription = "BROADCASTING:" 348 redisCategoryDescription = "REDIS:" 349 httpBroadcastCategoryDescription = "HTTP BROADCAST:" 350 natsCategoryDescription = "NATS:" 351 rpcCategoryDescription = "RPC:" 352 disconnectorCategoryDescription = "DISCONNECTOR:" 353 logCategoryDescription = "LOG:" 354 metricsCategoryDescription = "METRICS:" 355 wsCategoryDescription = "WEBSOCKETS:" 356 pingCategoryDescription = "PING:" 357 jwtCategoryDescription = "JWT:" 358 signedStreamsCategoryDescription = "SIGNED STREAMS:" 359 statsdCategoryDescription = "STATSD:" 360 embeddedNatsCategoryDescription = "EMBEDDED NATS:" 361 miscCategoryDescription = "MISC:" 362 brokerCategoryDescription = "BROKER:" 363 sseCategoryDescription = "SERVER-SENT EVENTS:" 364 365 envPrefix = "ANYCABLE_" 366 ) 367 368 var ( 369 splitFlagName = regexp.MustCompile("[_-]") 370 ) 371 372 // serverCLIFlags returns base server flags 373 func serverCLIFlags(c *config.Config, path *string, isPublic *bool) []cli.Flag { 374 return withDefaults(serverCategoryDescription, []cli.Flag{ 375 &cli.StringFlag{ 376 Name: "host", 377 Value: c.Host, 378 Usage: "Server host", 379 Destination: &c.Host, 380 }, 381 382 &cli.IntFlag{ 383 Name: "port", 384 Value: c.Port, 385 Usage: "Server port", 386 EnvVars: []string{envPrefix + "PORT", "PORT"}, 387 Destination: &c.Port, 388 }, 389 390 &cli.StringFlag{ 391 Name: "secret", 392 Usage: "A common secret key used by all features by default", 393 Value: c.Secret, 394 Destination: &c.Secret, 395 }, 396 397 &cli.StringFlag{ 398 Name: "broadcast_key", 399 Usage: "An authentication key for broadcast requests", 400 Value: c.BroadcastKey, 401 Destination: &c.BroadcastKey, 402 }, 403 404 &cli.BoolFlag{ 405 Name: "public", 406 Usage: "[DANGER ZONE] Run server in the public mode allowing all connections and stream subscriptions", 407 Destination: isPublic, 408 }, 409 410 &cli.BoolFlag{ 411 Name: "noauth", 412 Usage: "[DANGER ZONE] Disable client authentication over RPC", 413 Value: c.SkipAuth, 414 Destination: &c.SkipAuth, 415 }, 416 417 &cli.IntFlag{ 418 Name: "max-conn", 419 Usage: "Limit simultaneous server connections (0 – without limit)", 420 Destination: &c.MaxConn, 421 }, 422 423 &cli.StringFlag{ 424 Name: "path", 425 Value: strings.Join(c.Path, ","), 426 Usage: "WebSocket endpoint path (you can specify multiple paths using comma as separator)", 427 Destination: path, 428 }, 429 430 &cli.StringFlag{ 431 Name: "health-path", 432 Value: c.HealthPath, 433 Usage: "HTTP health endpoint path", 434 Destination: &c.HealthPath, 435 }, 436 437 &cli.IntFlag{ 438 Name: "shutdown_timeout", 439 Usage: "Graceful shutdown timeout (in seconds)", 440 Value: c.App.ShutdownTimeout, 441 Destination: &c.App.ShutdownTimeout, 442 }, 443 444 &cli.IntFlag{ 445 Name: "shutdown_pool_size", 446 Usage: "The number of goroutines to use for disconnect calls on shutdown", 447 Value: c.App.ShutdownDisconnectPoolSize, 448 Destination: &c.App.ShutdownDisconnectPoolSize, 449 Hidden: true, 450 }, 451 }) 452 } 453 454 // sslCLIFlags returns SSL flags 455 func sslCLIFlags(c *config.Config) []cli.Flag { 456 return withDefaults(sslCategoryDescription, []cli.Flag{ 457 &cli.PathFlag{ 458 Name: "ssl_cert", 459 Usage: "SSL certificate path", 460 Destination: &c.SSL.CertPath, 461 }, 462 463 &cli.PathFlag{ 464 Name: "ssl_key", 465 Usage: "SSL private key path", 466 Destination: &c.SSL.KeyPath, 467 }, 468 }) 469 } 470 471 // broadcastCLIFlags returns broadcast_adapter flag 472 func broadcastCLIFlags(c *config.Config) []cli.Flag { 473 return withDefaults(broadcastCategoryDescription, []cli.Flag{ 474 &cli.StringFlag{ 475 Name: "broadcast_adapter", 476 Usage: "Broadcasting adapter to use (http, redisx, redis or nats). You can specify multiple at once via a comma-separated list", 477 Value: c.BroadcastAdapter, 478 Destination: &c.BroadcastAdapter, 479 }, 480 &cli.StringFlag{ 481 Name: "broker", 482 Usage: "Broker engine to use (memory)", 483 Value: c.BrokerAdapter, 484 Destination: &c.BrokerAdapter, 485 }, 486 &cli.StringFlag{ 487 Name: "pubsub", 488 Usage: "Pub/Sub adapter to use (redis or nats)", 489 Value: c.PubSubAdapter, 490 Destination: &c.PubSubAdapter, 491 }, 492 &cli.IntFlag{ 493 Name: "hub_gopool_size", 494 Usage: "The size of the goroutines pool to broadcast messages", 495 Value: c.App.HubGopoolSize, 496 Destination: &c.App.HubGopoolSize, 497 Hidden: true, 498 }, 499 }) 500 } 501 502 // brokerCLIFlags returns broker related flags 503 func brokerCLIFlags(c *config.Config) []cli.Flag { 504 return withDefaults(brokerCategoryDescription, []cli.Flag{ 505 &cli.IntFlag{ 506 Name: "history_limit", 507 Usage: "Max number of messages to keep in the stream's history", 508 Value: c.Broker.HistoryLimit, 509 Destination: &c.Broker.HistoryLimit, 510 }, 511 &cli.Int64Flag{ 512 Name: "history_ttl", 513 Usage: "TTL for messages in streams history (seconds)", 514 Value: c.Broker.HistoryTTL, 515 Destination: &c.Broker.HistoryTTL, 516 }, 517 &cli.Int64Flag{ 518 Name: "sessions_ttl", 519 Usage: "TTL for expired/disconnected sessions (seconds)", 520 Value: c.Broker.SessionsTTL, 521 Destination: &c.Broker.SessionsTTL, 522 }, 523 }) 524 } 525 526 func redisCLIFlags(c *config.Config) []cli.Flag { 527 return withDefaults(redisCategoryDescription, []cli.Flag{ 528 &cli.StringFlag{ 529 Name: "redis_url", 530 Usage: "Redis url", 531 Value: c.Redis.URL, 532 EnvVars: []string{envPrefix + "REDIS_URL", "REDIS_URL"}, 533 Destination: &c.Redis.URL, 534 }, 535 536 &cli.StringFlag{ 537 Name: "redis_channel", 538 Usage: "Redis channel for broadcasts", 539 Value: c.Redis.Channel, 540 Destination: &c.Redis.Channel, 541 }, 542 543 &cli.StringFlag{ 544 Name: "redis_sentinels", 545 Usage: "Comma separated list of sentinel hosts, format: 'hostname:port,..'", 546 Destination: &c.Redis.Sentinels, 547 }, 548 549 &cli.IntFlag{ 550 Name: "redis_sentinel_discovery_interval", 551 Usage: "Interval to rediscover sentinels in seconds", 552 Value: c.Redis.SentinelDiscoveryInterval, 553 Destination: &c.Redis.SentinelDiscoveryInterval, 554 Hidden: true, 555 }, 556 557 &cli.IntFlag{ 558 Name: "redis_keepalive_interval", 559 Usage: "Interval to periodically ping Redis to make sure it's alive", 560 Value: c.Redis.KeepalivePingInterval, 561 Destination: &c.Redis.KeepalivePingInterval, 562 Hidden: true, 563 }, 564 565 &cli.BoolFlag{ 566 Name: "redis_tls_verify", 567 Usage: "Verify Redis server TLS certificate (only if URL protocol is rediss://)", 568 Value: c.Redis.TLSVerify, 569 Destination: &c.Redis.TLSVerify, 570 Hidden: true, 571 }, 572 573 &cli.BoolFlag{ 574 Name: "redis_disable_cache", 575 Usage: "Disable client-side caching", 576 Value: c.Redis.DisableCache, 577 Destination: &c.Redis.DisableCache, 578 Hidden: true, 579 }, 580 }) 581 } 582 583 // httpBroadcastCLIFlags returns HTTP CLI flags 584 func httpBroadcastCLIFlags(c *config.Config) []cli.Flag { 585 return withDefaults(httpBroadcastCategoryDescription, []cli.Flag{ 586 &cli.IntFlag{ 587 Name: "http_broadcast_port", 588 Usage: "HTTP pub/sub server port", 589 Value: c.HTTPBroadcast.Port, 590 Destination: &c.HTTPBroadcast.Port, 591 }, 592 593 &cli.StringFlag{ 594 Name: "http_broadcast_path", 595 Usage: "HTTP pub/sub endpoint path", 596 Value: c.HTTPBroadcast.Path, 597 Destination: &c.HTTPBroadcast.Path, 598 }, 599 600 &cli.StringFlag{ 601 Name: "http_broadcast_secret", 602 Usage: "[Deprecated] HTTP pub/sub authorization secret", 603 Destination: &c.HTTPBroadcast.Secret, 604 Hidden: true, 605 }, 606 607 &cli.BoolFlag{ 608 Name: "http_broadcast_cors", 609 Destination: &c.HTTPBroadcast.AddCORSHeaders, 610 Hidden: true, 611 }, 612 }) 613 } 614 615 // natsCLIFlags returns NATS cli flags 616 func natsCLIFlags(c *config.Config) []cli.Flag { 617 return withDefaults(natsCategoryDescription, []cli.Flag{ 618 &cli.StringFlag{ 619 Name: "nats_servers", 620 Usage: "Comma separated list of NATS cluster servers", 621 Value: c.NATS.Servers, 622 Destination: &c.NATS.Servers, 623 }, 624 625 &cli.StringFlag{ 626 Name: "nats_channel", 627 Usage: "NATS channel for broadcasts", 628 Value: c.NATS.Channel, 629 Destination: &c.NATS.Channel, 630 }, 631 632 &cli.BoolFlag{ 633 Name: "nats_dont_randomize_servers", 634 Usage: "Pass this option to disable NATS servers randomization during (re-)connect", 635 Destination: &c.NATS.DontRandomizeServers, 636 Hidden: true, 637 }, 638 }) 639 } 640 641 // embeddedNatsCLIFlags returns NATS cli flags 642 func embeddedNatsCLIFlags(c *config.Config, routes *string, gateways *string) []cli.Flag { 643 return withDefaults(embeddedNatsCategoryDescription, []cli.Flag{ 644 &cli.BoolFlag{ 645 Name: "embed_nats", 646 Usage: "Enable embedded NATS server and use it for pub/sub", 647 Value: c.EmbedNats, 648 Destination: &c.EmbedNats, 649 }, 650 651 &cli.StringFlag{ 652 Name: "enats_addr", 653 Usage: "NATS server bind address", 654 Value: c.EmbeddedNats.ServiceAddr, 655 Destination: &c.EmbeddedNats.ServiceAddr, 656 Hidden: true, 657 }, 658 659 &cli.StringFlag{ 660 Name: "enats_cluster", 661 Usage: "NATS cluster service bind address", 662 Value: c.EmbeddedNats.ClusterAddr, 663 Destination: &c.EmbeddedNats.ClusterAddr, 664 Hidden: true, 665 }, 666 667 &cli.StringFlag{ 668 Name: "enats_cluster_name", 669 Usage: "NATS cluster name", 670 Value: c.EmbeddedNats.ClusterName, 671 Destination: &c.EmbeddedNats.ClusterName, 672 }, 673 674 &cli.StringFlag{ 675 Name: "enats_cluster_routes", 676 Usage: "Comma-separated list of known cluster addresses", 677 Destination: routes, 678 }, 679 680 &cli.StringFlag{ 681 Name: "enats_gateway", 682 Usage: "NATS gateway bind address", 683 Value: c.EmbeddedNats.GatewayAddr, 684 Destination: &c.EmbeddedNats.GatewayAddr, 685 Hidden: true, 686 }, 687 688 &cli.StringFlag{ 689 Name: "enats_gateways", 690 Usage: "Semicolon-separated list of known gateway configurations: name_a:gateway_1,gateway_2;name_b:gateway_4", 691 Destination: gateways, 692 }, 693 694 &cli.StringFlag{ 695 Name: "enats_gateway_advertise", 696 Usage: "NATS gateway advertise address", 697 Value: c.EmbeddedNats.GatewayAdvertise, 698 Destination: &c.EmbeddedNats.GatewayAdvertise, 699 }, 700 701 &cli.StringFlag{ 702 Name: "enats_store_dir", 703 Usage: "Embedded NATS store directory (for JetStream)", 704 Value: c.EmbeddedNats.StoreDir, 705 Destination: &c.EmbeddedNats.StoreDir, 706 Hidden: true, 707 }, 708 709 &cli.StringFlag{ 710 Name: "enats_server_name", 711 Usage: "Embedded NATS unique server name (required for JetStream), auto-generated by default", 712 Value: c.EmbeddedNats.Name, 713 Destination: &c.EmbeddedNats.Name, 714 }, 715 716 &cli.BoolFlag{ 717 Name: "enats_debug", 718 Usage: "Enable NATS server logs", 719 Destination: &c.EmbeddedNats.Debug, 720 Hidden: true, 721 }, 722 723 &cli.BoolFlag{ 724 Name: "enats_trace", 725 Usage: "Enable NATS server protocol trace logs", 726 Destination: &c.EmbeddedNats.Trace, 727 Hidden: true, 728 }, 729 }) 730 } 731 732 // rpcCLIFlags returns CLI flags for RPC 733 func rpcCLIFlags(c *config.Config, headers, cookieFilter *string, isNone *bool) []cli.Flag { 734 return withDefaults(rpcCategoryDescription, []cli.Flag{ 735 &cli.StringFlag{ 736 Name: "rpc_host", 737 Usage: "RPC service address (full URL in case of HTTP RPC)", 738 Value: c.RPC.Host, 739 Destination: &c.RPC.Host, 740 }, 741 742 &cli.BoolFlag{ 743 Name: "norpc", 744 Usage: "Disable RPC component and run server in the standalone mode", 745 Destination: isNone, 746 }, 747 748 &cli.IntFlag{ 749 Name: "rpc_concurrency", 750 Usage: "Max number of concurrent RPC request; should be slightly less than the RPC server concurrency", 751 Value: c.RPC.Concurrency, 752 Destination: &c.RPC.Concurrency, 753 }, 754 755 &cli.BoolFlag{ 756 Name: "rpc_enable_tls", 757 Usage: "Enable client-side TLS with the RPC server", 758 Destination: &c.RPC.EnableTLS, 759 Hidden: true, 760 }, 761 762 &cli.BoolFlag{ 763 Name: "rpc_tls_verify", 764 Usage: "Whether to verify the RPC server certificate", 765 Destination: &c.RPC.TLSVerify, 766 Value: true, 767 Hidden: true, 768 }, 769 770 &cli.StringFlag{ 771 Name: "rpc_tls_root_ca", 772 Usage: "CA root certificate file path or contents in PEM format (if not set, system CAs will be used)", 773 Destination: &c.RPC.TLSRootCA, 774 Hidden: true, 775 }, 776 777 &cli.IntFlag{ 778 Name: "rpc_max_call_recv_size", 779 Usage: "Override default MaxCallRecvMsgSize for RPC client (bytes)", 780 Value: c.RPC.MaxRecvSize, 781 Destination: &c.RPC.MaxRecvSize, 782 Hidden: true, 783 }, 784 785 &cli.IntFlag{ 786 Name: "rpc_max_call_send_size", 787 Usage: "Override default MaxCallSendMsgSize for RPC client (bytes)", 788 Value: c.RPC.MaxSendSize, 789 Destination: &c.RPC.MaxSendSize, 790 Hidden: true, 791 }, 792 793 &cli.StringFlag{ 794 Name: "headers", 795 Usage: "List of headers to proxy to RPC", 796 Value: strings.Join(c.Headers, ","), 797 Destination: headers, 798 }, 799 800 &cli.StringFlag{ 801 Name: "proxy-cookies", 802 Usage: "Cookie keys to send to RPC, default is all", 803 Destination: cookieFilter, 804 }, 805 806 &cli.StringFlag{ 807 Name: "rpc_impl", 808 Usage: "RPC implementation (grpc, http)", 809 Value: c.RPC.Implementation, 810 Destination: &c.RPC.Implementation, 811 Hidden: true, 812 }, 813 814 &cli.StringFlag{ 815 Name: "http_rpc_secret", 816 Usage: "Authentication secret for RPC over HTTP", 817 Value: c.RPC.Secret, 818 Destination: &c.RPC.Secret, 819 }, 820 821 &cli.IntFlag{ 822 Name: "http_rpc_timeout", 823 Usage: "HTTP RPC timeout (in ms)", 824 Value: c.RPC.RequestTimeout, 825 Destination: &c.RPC.RequestTimeout, 826 Hidden: true, 827 }, 828 }) 829 } 830 831 // rpcCLIFlags returns CLI flags for disconnect options 832 func disconnectorCLIFlags(c *config.Config) []cli.Flag { 833 return withDefaults(disconnectorCategoryDescription, []cli.Flag{ 834 &cli.StringFlag{ 835 Name: "disconnect_mode", 836 Usage: "Define when to call Disconnect callback (always, never, auto)", 837 Destination: &c.App.DisconnectMode, 838 Value: c.App.DisconnectMode, 839 }, 840 841 &cli.IntFlag{ 842 Name: "disconnect_rate", 843 Usage: "Max number of Disconnect calls per second", 844 Value: c.DisconnectQueue.Rate, 845 Destination: &c.DisconnectQueue.Rate, 846 Hidden: true, 847 }, 848 849 &cli.IntFlag{ 850 Name: "disconnect_backlog_size", 851 Usage: "The size of the channel's buffer for disconnect requests", 852 Value: c.DisconnectQueue.Backlog, 853 Destination: &c.DisconnectQueue.Backlog, 854 Hidden: true, 855 }, 856 857 &cli.IntFlag{ 858 Name: "disconnect_timeout", 859 Usage: "[DEPRECATED] Graceful shutdown timeout (in seconds)", 860 Value: c.DisconnectQueue.ShutdownTimeout, 861 Destination: &c.DisconnectQueue.ShutdownTimeout, 862 Hidden: true, 863 }, 864 865 &cli.BoolFlag{ 866 Name: "disable_disconnect", 867 Usage: "Disable calling Disconnect callback", 868 Destination: &c.DisconnectorDisabled, 869 Hidden: true, 870 }, 871 }) 872 } 873 874 // rpcCLIFlags returns CLI flags for logging 875 func logCLIFlags(c *config.Config) []cli.Flag { 876 return withDefaults(logCategoryDescription, []cli.Flag{ 877 &cli.StringFlag{ 878 Name: "log_level", 879 Usage: "Set logging level (debug/info/warn/error)", 880 Value: c.LogLevel, 881 Destination: &c.LogLevel, 882 }, 883 884 &cli.StringFlag{ 885 Name: "log_format", 886 Usage: "Set logging format (text/json)", 887 Value: c.LogFormat, 888 Destination: &c.LogFormat, 889 }, 890 891 &cli.BoolFlag{ 892 Name: "debug", 893 Usage: "Enable debug mode (more verbose logging)", 894 Destination: &c.Debug, 895 }, 896 }) 897 } 898 899 // metricsCLIFlags returns CLI flags for metrics 900 func metricsCLIFlags(c *config.Config, filter *string, mtags *string) []cli.Flag { 901 return withDefaults(metricsCategoryDescription, []cli.Flag{ 902 // Metrics 903 &cli.BoolFlag{ 904 Name: "metrics_log", 905 Usage: "Enable metrics logging (with info level)", 906 Destination: &c.Metrics.Log, 907 }, 908 909 &cli.IntFlag{ 910 Name: "metrics_rotate_interval", 911 Usage: "Specify how often flush metrics to writers (logs, statsd) (in seconds)", 912 Value: c.Metrics.RotateInterval, 913 Destination: &c.Metrics.RotateInterval, 914 }, 915 916 &cli.IntFlag{ 917 Name: "metrics_log_interval", 918 Usage: "DEPRECATED. Specify how often flush metrics logs (in seconds)", 919 Value: c.Metrics.LogInterval, 920 Destination: &c.Metrics.LogInterval, 921 Hidden: true, 922 }, 923 924 &cli.StringFlag{ 925 Name: "metrics_log_filter", 926 Usage: "Specify list of metrics to print to log (to reduce the output)", 927 Destination: filter, 928 }, 929 930 &cli.StringFlag{ 931 Name: "metrics_log_formatter", 932 Usage: "Specify the path to custom Ruby formatter script (only supported on MacOS and Linux)", 933 Destination: &c.Metrics.LogFormatter, 934 Hidden: true, 935 }, 936 937 &cli.StringFlag{ 938 Name: "metrics_http", 939 Usage: "Enable HTTP metrics endpoint at the specified path", 940 Destination: &c.Metrics.HTTP, 941 }, 942 943 &cli.StringFlag{ 944 Name: "metrics_host", 945 Usage: "Server host for metrics endpoint", 946 Destination: &c.Metrics.Host, 947 }, 948 949 &cli.IntFlag{ 950 Name: "metrics_port", 951 Usage: "Server port for metrics endpoint, the same as for main server by default", 952 Destination: &c.Metrics.Port, 953 }, 954 955 &cli.StringFlag{ 956 Name: "metrics_tags", 957 Usage: "Comma-separated list of default (global) tags to add to every metric", 958 Destination: mtags, 959 }, 960 961 &cli.IntFlag{ 962 Name: "stats_refresh_interval", 963 Usage: "How often to refresh the server stats (in seconds)", 964 Value: c.App.StatsRefreshInterval, 965 Destination: &c.App.StatsRefreshInterval, 966 }, 967 }) 968 } 969 970 // wsCLIFlags returns CLI flags for WebSocket 971 func wsCLIFlags(c *config.Config) []cli.Flag { 972 return withDefaults(wsCategoryDescription, []cli.Flag{ 973 &cli.IntFlag{ 974 Name: "read_buffer_size", 975 Usage: "WebSocket connection read buffer size", 976 Value: c.WS.ReadBufferSize, 977 Destination: &c.WS.ReadBufferSize, 978 Hidden: true, 979 }, 980 981 &cli.IntFlag{ 982 Name: "write_buffer_size", 983 Usage: "WebSocket connection write buffer size", 984 Value: c.WS.WriteBufferSize, 985 Destination: &c.WS.WriteBufferSize, 986 Hidden: true, 987 }, 988 989 &cli.Int64Flag{ 990 Name: "max_message_size", 991 Usage: "Maximum size of a message in bytes", 992 Value: c.WS.MaxMessageSize, 993 Destination: &c.WS.MaxMessageSize, 994 Hidden: true, 995 }, 996 997 &cli.BoolFlag{ 998 Name: "enable_ws_compression", 999 Usage: "Enable experimental WebSocket per message compression", 1000 Destination: &c.WS.EnableCompression, 1001 Hidden: true, 1002 }, 1003 1004 &cli.StringFlag{ 1005 Name: "allowed_origins", 1006 Usage: `Accept requests only from specified origins, e.g., "www.example.com,*example.io". No check is performed if empty`, 1007 Destination: &c.WS.AllowedOrigins, 1008 }, 1009 }) 1010 } 1011 1012 // pingCLIFlags returns CLI flag for ping settings 1013 func pingCLIFlags(c *config.Config) []cli.Flag { 1014 return withDefaults(pingCategoryDescription, []cli.Flag{ 1015 &cli.IntFlag{ 1016 Name: "ping_interval", 1017 Usage: "Action Cable ping interval (in seconds)", 1018 Value: c.App.PingInterval, 1019 Destination: &c.App.PingInterval, 1020 }, 1021 1022 &cli.StringFlag{ 1023 Name: "ping_timestamp_precision", 1024 Usage: "Precision for timestamps in ping messages (s, ms, ns)", 1025 Value: c.App.PingTimestampPrecision, 1026 Destination: &c.App.PingTimestampPrecision, 1027 Hidden: true, 1028 }, 1029 1030 &cli.IntFlag{ 1031 Name: "pong_timeout", 1032 Usage: `How long to wait for a pong response before disconnecting the client (in seconds). Zero means no pongs required`, 1033 Value: c.App.PongTimeout, 1034 Destination: &c.App.PongTimeout, 1035 }, 1036 }) 1037 } 1038 1039 // jwtCLIFlags returns CLI flags for JWT 1040 func jwtCLIFlags(c *config.Config, jwtIdKey *string, jwtIdParam *string, jwtIdEnforce *bool) []cli.Flag { 1041 return withDefaults(jwtCategoryDescription, []cli.Flag{ 1042 &cli.StringFlag{ 1043 Name: "jwt_id_key", 1044 Destination: jwtIdKey, 1045 Usage: "[Depracated]", 1046 Hidden: true, 1047 }, 1048 1049 &cli.StringFlag{ 1050 Name: "jwt_secret", 1051 Usage: "The encryption key used to verify JWT tokens", 1052 Destination: &c.JWT.Secret, 1053 }, 1054 1055 &cli.StringFlag{ 1056 Name: "jwt_id_param", 1057 Destination: jwtIdParam, 1058 Usage: "[Deprecated]", 1059 Hidden: true, 1060 }, 1061 1062 &cli.StringFlag{ 1063 Name: "jwt_param", 1064 Usage: "The name of a query string param or an HTTP header carrying a token", 1065 Value: c.JWT.Param, 1066 Destination: &c.JWT.Param, 1067 }, 1068 1069 &cli.BoolFlag{ 1070 Name: "jwt_id_enforce", 1071 Usage: "[Deprecated]", 1072 Destination: jwtIdEnforce, 1073 Hidden: true, 1074 }, 1075 1076 &cli.BoolFlag{ 1077 Name: "enforce_jwt", 1078 Usage: "Whether to enforce token presence for all connections", 1079 Destination: &c.JWT.Force, 1080 }, 1081 }) 1082 } 1083 1084 // signedStreamsCLIFlags returns misc CLI flags 1085 func signedStreamsCLIFlags(c *config.Config, turboRailsKey *string, cableReadyKey *string, turboRailsClearText *bool, cableReadyCleartext *bool) []cli.Flag { 1086 return withDefaults(signedStreamsCategoryDescription, []cli.Flag{ 1087 &cli.StringFlag{ 1088 Name: "streams_secret", 1089 Usage: "Secret you use to sign stream names", 1090 Destination: &c.Streams.Secret, 1091 }, 1092 1093 &cli.BoolFlag{ 1094 Name: "public_streams", 1095 Usage: "Enable public (unsigned) streams", 1096 Destination: &c.Streams.Public, 1097 }, 1098 1099 &cli.BoolFlag{ 1100 Name: "streams_whisper", 1101 Usage: "Enable whispering for signed pub/sub streams", 1102 Destination: &c.Streams.Whisper, 1103 }, 1104 1105 &cli.BoolFlag{ 1106 Name: "turbo_streams", 1107 Usage: "Enable Turbo Streams support", 1108 Destination: &c.Streams.Turbo, 1109 }, 1110 1111 &cli.BoolFlag{ 1112 Name: "cable_ready", 1113 Usage: "Enable Cable Ready support", 1114 Destination: &c.Streams.CableReady, 1115 }, 1116 1117 &cli.StringFlag{ 1118 Name: "turbo_rails_key", 1119 Usage: "[Deprecated]", 1120 Destination: turboRailsKey, 1121 Hidden: true, 1122 }, 1123 1124 &cli.StringFlag{ 1125 Name: "turbo_streams_secret", 1126 Usage: "A custom secret to verify Turbo Streams", 1127 Destination: &c.Streams.TurboSecret, 1128 }, 1129 1130 &cli.BoolFlag{ 1131 Name: "turbo_rails_cleartext", 1132 Usage: "[DEPRECATED] Enable Turbo Streams fastlane without stream names signing", 1133 Destination: turboRailsClearText, 1134 Hidden: true, 1135 }, 1136 1137 &cli.StringFlag{ 1138 Name: "cable_ready_key", 1139 Usage: "[Deprecated]", 1140 Destination: cableReadyKey, 1141 Hidden: true, 1142 }, 1143 1144 &cli.StringFlag{ 1145 Name: "cable_ready_secret", 1146 Usage: "A custom secret to verify CableReady streams", 1147 Destination: &c.Streams.CableReadySecret, 1148 }, 1149 1150 &cli.BoolFlag{ 1151 Name: "cable_ready_cleartext", 1152 Usage: "[DEPRECATED] Enable Cable Ready fastlane without stream names signing", 1153 Destination: cableReadyCleartext, 1154 Hidden: true, 1155 }, 1156 }) 1157 } 1158 1159 // StatsD related flags 1160 func statsdCLIFlags(c *config.Config) []cli.Flag { 1161 return withDefaults(statsdCategoryDescription, []cli.Flag{ 1162 &cli.StringFlag{ 1163 Name: "statsd_host", 1164 Usage: "Server host for metrics sent to statsd server in the format <host>:<port>", 1165 Destination: &c.Metrics.Statsd.Host, 1166 }, 1167 &cli.StringFlag{ 1168 Name: "statsd_prefix", 1169 Usage: "Statsd metrics prefix", 1170 Value: c.Metrics.Statsd.Prefix, 1171 Destination: &c.Metrics.Statsd.Prefix, 1172 }, 1173 &cli.IntFlag{ 1174 Name: "statsd_max_packet_size", 1175 Usage: "Statsd client maximum UDP packet size", 1176 Value: c.Metrics.Statsd.MaxPacketSize, 1177 Destination: &c.Metrics.Statsd.MaxPacketSize, 1178 Hidden: true, 1179 }, 1180 &cli.StringFlag{ 1181 Name: "statsd_tags_format", 1182 Usage: `One of "datadog", "influxdb", or "graphite"`, 1183 Value: c.Metrics.Statsd.TagFormat, 1184 Destination: &c.Metrics.Statsd.TagFormat, 1185 }, 1186 }) 1187 } 1188 1189 // sseCLIFlags returns CLI flags for SSE 1190 func sseCLIFlags(c *config.Config) []cli.Flag { 1191 return withDefaults(sseCategoryDescription, []cli.Flag{ 1192 &cli.BoolFlag{ 1193 Name: "sse", 1194 Usage: "Enable SSE endpoint", 1195 Value: c.SSE.Enabled, 1196 Destination: &c.SSE.Enabled, 1197 }, 1198 &cli.StringFlag{ 1199 Name: "sse_path", 1200 Usage: "SSE endpoint path", 1201 Value: c.SSE.Path, 1202 Destination: &c.SSE.Path, 1203 }, 1204 }) 1205 } 1206 1207 // miscCLIFlags returns uncategorized flags 1208 func miscCLIFlags(c *config.Config, presets *string) []cli.Flag { 1209 return withDefaults(miscCategoryDescription, []cli.Flag{ 1210 &cli.StringFlag{ 1211 Name: "presets", 1212 Usage: "Configuration presets, comma-separated (none, fly, heroku, broker). Inferred automatically", 1213 Destination: presets, 1214 }, 1215 }) 1216 } 1217 1218 // withDefaults sets category and env var name a flags passed as the arument 1219 func withDefaults(category string, flags []cli.Flag) []cli.Flag { 1220 for _, f := range flags { 1221 switch v := f.(type) { 1222 case *cli.IntFlag: 1223 v.Category = category 1224 if len(v.EnvVars) == 0 { 1225 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1226 } 1227 case *cli.Int64Flag: 1228 v.Category = category 1229 if len(v.EnvVars) == 0 { 1230 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1231 } 1232 case *cli.Float64Flag: 1233 v.Category = category 1234 if len(v.EnvVars) == 0 { 1235 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1236 } 1237 case *cli.DurationFlag: 1238 v.Category = category 1239 if len(v.EnvVars) == 0 { 1240 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1241 } 1242 case *cli.BoolFlag: 1243 v.Category = category 1244 if len(v.EnvVars) == 0 { 1245 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1246 } 1247 case *cli.StringFlag: 1248 v.Category = category 1249 if len(v.EnvVars) == 0 { 1250 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1251 } 1252 case *cli.PathFlag: 1253 v.Category = category 1254 if len(v.EnvVars) == 0 { 1255 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1256 } 1257 case *cli.TimestampFlag: 1258 v.Category = category 1259 if len(v.EnvVars) == 0 { 1260 v.EnvVars = []string{nameToEnvVarName(v.Name)} 1261 } 1262 } 1263 } 1264 return flags 1265 } 1266 1267 // nameToEnvVarName converts flag name to env variable 1268 func nameToEnvVarName(name string) string { 1269 split := splitFlagName.Split(name, -1) 1270 set := []string{} 1271 1272 for i := range split { 1273 set = append(set, strings.ToUpper(split[i])) 1274 } 1275 1276 return envPrefix + strings.Join(set, "_") 1277 } 1278 1279 func parseTags(str string) map[string]string { 1280 tags := strings.Split(str, ",") 1281 1282 res := make(map[string]string, len(tags)) 1283 1284 for _, v := range tags { 1285 parts := strings.Split(v, ":") 1286 res[parts[0]] = parts[1] 1287 } 1288 1289 return res 1290 }