Skip to content

Commit

Permalink
feat: core toml functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
palkan committed Oct 8, 2024
1 parent 4072dd8 commit fad83c3
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## master

- Add `anycable.toml` support. ([@palkan][])

- Print metrics with keys sorted alphabetically. ([@palkan][])

- Upgrade to Go 1.23. ([@palkan][])
Expand Down
6 changes: 5 additions & 1 deletion cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ func (r *Runner) Run() error {

r.log.Info(fmt.Sprintf("Starting %s %s%s (pid: %d, open file limit: %s, gomaxprocs: %d)", r.name, version.Version(), mrubySupport, os.Getpid(), utils.OpenFileLimit(), numProcs))

if r.config.IsPublic() {
if r.config.ConfigFilePath != "" {
r.log.Info(fmt.Sprintf("Using configuration from file: %s", r.config.ConfigFilePath))
}

if r.config.PublicMode {
r.log.Warn("Server is running in the public mode")
}

Expand Down
70 changes: 68 additions & 2 deletions cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
"os"
"regexp"
"strings"

Expand Down Expand Up @@ -49,14 +50,24 @@ func WithCLICustomOptions(factory customOptionsFactory) cliOption {
}
}

const DefaultConfigPath = "/etc/anycable/anycable.toml"
const CurrentConfigPath = "./anycable.toml"

// NewConfigFromCLI reads config from os.Args. It returns config, error (if any) and a bool value
// indicating that the execution was interrupted (e.g., usage message or version was shown), no further action required.
func NewConfigFromCLI(args []string, opts ...cliOption) (*config.Config, error, bool) {
c := config.NewConfig()

if _, err := os.Stat(CurrentConfigPath); err == nil {
args = append([]string{args[0], "--config-path", CurrentConfigPath}, args[1:]...)
} else if _, err := os.Stat(DefaultConfigPath); err == nil {
args = append([]string{args[0], "--config-path", DefaultConfigPath}, args[1:]...)
}

var path, headers, cookieFilter, mtags string
var broadcastAdapters string
var cliInterrupted = true
var shouldPrintConfig = false
var metricsFilter string
var enatsRoutes, enatsGateways string
var presets string
Expand All @@ -71,7 +82,42 @@ func NewConfigFromCLI(args []string, opts ...cliOption) (*config.Config, error,
_, _ = fmt.Fprintf(cCtx.App.Writer, "%v\n", cCtx.App.Version)
}

flags := []cli.Flag{}
flags := []cli.Flag{
&cli.BoolFlag{
Name: "ignore-config-path",
Usage: "Ignore configuration files",
Action: func(ctx *cli.Context, val bool) error {
if val {
c.ConfigFilePath = "none"
}
return nil
},
},
&cli.StringFlag{
Name: "config-path",
Usage: "Path to the TOML configuration file",
Action: func(ctx *cli.Context, path string) error {
if c.ConfigFilePath == "none" {
c.ConfigFilePath = ""
return nil
}

c.ConfigFilePath = path

// check if file exists and try to load config from it
if err := c.LoadFromFile(); err != nil {
return err
}

return nil
},
},
&cli.BoolFlag{
Name: "print-config",
Usage: "Print configuration and exit",
Destination: &shouldPrintConfig,
},
}
flags = append(flags, serverCLIFlags(&c, &path)...)
flags = append(flags, sslCLIFlags(&c)...)
flags = append(flags, broadcastCLIFlags(&c, &broadcastAdapters)...)
Expand Down Expand Up @@ -295,6 +341,10 @@ Use broadcast_key instead.`)
}

// Nullify none secrets
if c.Secret == "none" {
c.Secret = ""
}

if c.Streams.Secret == "none" {
c.Streams.Secret = ""
}
Expand Down Expand Up @@ -329,6 +379,11 @@ Use broadcast_key instead.`)
c.HTTPBroadcast.SecretBase = ""
}

if shouldPrintConfig {
fmt.Print(c.Display())
return &c, nil, true
}

return &c, nil, false
}

Expand Down Expand Up @@ -453,6 +508,18 @@ func serverCLIFlags(c *config.Config, path *string) []cli.Flag {
Destination: &c.App.ShutdownDisconnectPoolSize,
Hidden: true,
},

&cli.StringFlag{
Name: "node_id",
Usage: "Unique node identifier",
Value: c.ID,
Hidden: true,
Action: func(ctx *cli.Context, val string) error {
c.ID = val
c.UserProvidedID = true
return nil
},
},
})
}

Expand All @@ -479,7 +546,6 @@ func broadcastCLIFlags(c *config.Config, adapters *string) []cli.Flag {
&cli.StringFlag{
Name: "broadcast_adapter",
Usage: "Broadcasting adapter to use (http, redisx, redis or nats). You can specify multiple at once via a comma-separated list",
Value: strings.Join(c.BroadcastAdapters, ","),
Destination: adapters,
},
&cli.StringFlag{
Expand Down
112 changes: 102 additions & 10 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package config

import (
"fmt"
"os"
"strings"

"github.com/BurntSushi/toml"
"github.com/anycable/anycable-go/broadcast"
"github.com/anycable/anycable-go/broker"
"github.com/anycable/anycable-go/enats"
Expand All @@ -15,20 +20,22 @@ import (
"github.com/anycable/anycable-go/sse"
"github.com/anycable/anycable-go/streams"
"github.com/anycable/anycable-go/ws"
"github.com/joomcode/errorx"

nanoid "github.com/matoous/go-nanoid"
)

// Config contains main application configuration
type Config struct {
ID string
Secret string
BroadcastKey string
SkipAuth bool
PublicMode bool
BroadcastAdapters []string
PubSubAdapter string
UserPresets []string
ID string `toml:"node_id"`
UserProvidedID bool
Secret string `toml:"secret"`
BroadcastKey string `toml:"broadcast_key"`
SkipAuth bool `toml:"noauth"`
PublicMode bool `toml:"public"`
BroadcastAdapters []string `toml:"broadcast_adapters"`
PubSubAdapter string `toml:"pubsub_adapter"`
UserPresets []string `toml:"presets"`
Log logger.Config
Server server.Config
App node.Config
Expand All @@ -45,6 +52,8 @@ type Config struct {
EmbeddedNats enats.Config
SSE sse.Config
Streams streams.Config

ConfigFilePath string
}

// NewConfig returns a new empty config
Expand Down Expand Up @@ -75,6 +84,89 @@ func NewConfig() Config {
return config
}

func (c Config) IsPublic() bool {
return c.SkipAuth && c.Streams.Public
func (c *Config) LoadFromFile() error {
bytes, err := os.ReadFile(c.ConfigFilePath)

if err != nil {
return errorx.Decorate(err, "failed to read config file")
}

prevID := c.ID
_, err = toml.Decode(string(bytes), &c)

if err != nil {
return errorx.Decorate(err, "failed to parse TOML configuration")
}

if c.ID != prevID {
c.UserProvidedID = true
}

return nil
}

func (c Config) Display() string {
var result strings.Builder

result.WriteString("# AnyCable server configuration.\n# Read more at https://docs.anycable.io/anycable-go/configuration\n\n")

result.WriteString("# General settings\n\n")

result.WriteString("# Public mode disables connection authentication, pub/sub streams and broadcasts verification\n")
if c.PublicMode {
result.WriteString(fmt.Sprintf("public = %t\n\n", c.PublicMode))
} else {
result.WriteString("# public = false\n\n")
}

result.WriteString("# Disable connection authentication only\n")
if c.SkipAuth {
result.WriteString(fmt.Sprintf("noauth = %t\n\n", c.SkipAuth))
} else {
result.WriteString("# noauth = false\n\n")
}

result.WriteString("# Application instance ID\n")
if c.UserProvidedID {
result.WriteString(fmt.Sprintf("node_id = \"%s\"\n\n", c.ID))
} else {
result.WriteString("# node_id = \"<auto-generated at each server start>\"\n\n")
}

result.WriteString("# The application secret key\n")

if c.Secret != "" {
result.WriteString(fmt.Sprintf("secret = \"%s\"\n\n", c.Secret))
} else {
result.WriteString("secret = \"none\"\n\n")
}

result.WriteString("# Broadcasting adapters for app-to-clients messages\n")
result.WriteString(fmt.Sprintf("broadcast_adapters = [\"%s\"]\n\n", strings.Join(c.BroadcastAdapters, "\", \"")))

result.WriteString("# Broadcasting authorization key\n")

if c.BroadcastKey != "" { // nolint: gocritic
result.WriteString(fmt.Sprintf("broadcast_key = \"%s\"\n\n", c.BroadcastKey))
} else if c.Secret != "" {
result.WriteString("# broadcast_key = \"<auto-generated from the application secret>\"\n\n")
} else {
result.WriteString("broadcast_key = \"none\"\n\n")
}

result.WriteString("# Pub/sub adapter for inter-node communication\n")
if c.PubSubAdapter == "" {
result.WriteString("# pubsub_adapter = \"redis\" # or \"nats\"\n\n")
} else {
result.WriteString(fmt.Sprintf("pubsub_adapter = \"%s\"\n\n", c.PubSubAdapter))
}

result.WriteString("# User-provided configuration presets\n")
if len(c.UserPresets) == 0 {
result.WriteString("# presets = [\"broker\"]\n\n")
} else {
result.WriteString(fmt.Sprintf("presets = [\"%s\"]\n\n", strings.Join(c.UserPresets, "\", \"")))
}

return result.String()
}
32 changes: 30 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AnyCable-Go configuration
# AnyCable server configuration

You can configure AnyCable-Go via CLI options, e.g.:
You can configure AnyCable server via CLI options, e.g.:

```sh
$ anycable-go --rpc_host=localhost:50051 --headers=cookie \
Expand All @@ -10,6 +10,8 @@ $ anycable-go --rpc_host=localhost:50051 --headers=cookie \

Or via the corresponding environment variables (i.e. `ANYCABLE_RPC_HOST`, `ANYCABLE_REDIS_URL`, etc.).

Finally, you can also store configuration in a `.toml` file (see [configuration files](#configuration-files)).

## Primary settings

Here is the list of the most commonly used configuration parameters.
Expand Down Expand Up @@ -258,5 +260,31 @@ You can find the actual value for GOMAXPROCS in the starting logs:
INFO 2022-06-30T03:31:21.848Z context=main Starting AnyCable 1.2.0-c4f1c6e (with mruby 1.2.0 (2015-11-17)) (pid: 39705, open file limit: 524288, gomaxprocs: 8)
```

## Configuration files

Since v1.5.4, you can also provide configuration via a TOML file. This is recommended for applications that require a lot of settings or have complex configurations.

AnyCable will look for a configuration file in the following locations:

- `./anycable.toml`
- `/ets/anycable/anycable.toml`

You can also specify the path to the configuration file using the `--config-path` option, e.g.:

```sh
$ anycable-go --config-path=/path/to/anycable.toml

2024-10-07 17:52:37.139 INF Starting AnyCable 1.5.4-87217bb (pid: 80235, open file limit: 122880, gomaxprocs: 8) nodeid=BzeSHV
2024-10-07 17:52:37.139 INF Using configuration from file: ./path/to/anycable.toml nodeid=BzeSHV
```

You can generate a sample configuration file (including the currently provided configuration) using the `--print-config` option:

```sh
$ anycable-go --print-config > anycable.toml
```

Finally, if there is a configuration file but you don't want to use it, you can disable it using the `--ignore-config-path` option.

[automaxprocs]: https://github.com/uber-go/automaxprocs
[NATS]: https://nats.io
42 changes: 42 additions & 0 deletions features/file_config.testfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generate a configuration file with provided values
run :anycable_gen_config,
["sh", "-c", './dist/anycable-go --noauth --port 2024 --broadcast_adapter=http,redisx --print-config > ./anycable.toml'],
env: {"ANYCABLE_SECRET" => "file-secret", "ANYCABLE_NODE_ID" => "node-1"}, clean_env: true

unless File.exist?("anycable.toml")
fail "Config file hasn't been generated"
end

at_exit { File.delete("anycable.toml") }

# Load from the default local path
run :anycable_load_config, "dist/anycable-go --print-config", clean_env: true

config = PerfectTOML.parse(stdout(:anycable_load_config))

$errors = []
def assert_equal(field, expected, actual)
$errors << "Expected #{field} to be #{expected}, got #{actual.nil? ? '<null>' : actual}" unless expected == actual
end

# top-level params
assert_equal("node ID", "node-1", config["node_id"])
assert_equal("noauth", true, config["noauth"])
assert_equal("secret", "file-secret", config["secret"])
assert_equal("broadcast adapters", %w[http redisx], config["broadcast_adapters"])

# nested params: TODO
# assert_equal("server.port", 2024, config.dig("server", "port"))

if $errors.any?
fail $errors.join("\n")
end

# Ignoring the config file
run :anycable_ignore_config_path, "dist/anycable-go --ignore-config-path --print-config", clean_env: true

config = PerfectTOML.parse(stdout(:anycable_ignore_config_path))

$errors.clear
assert_equal("node ID", nil, config["node_id"])
assert_equal("secret", nil, config["secret"])
Loading

0 comments on commit fad83c3

Please sign in to comment.