From 91a5599b48acf2ebb8bd9bd8292eef188e6f2794 Mon Sep 17 00:00:00 2001 From: FZambia Date: Thu, 3 Oct 2024 08:38:27 +0300 Subject: [PATCH] defaultenv command --- internal/cli/defaultenv.go | 104 ++++++++++++++++++++++++++++++ internal/config/config.go | 10 +-- internal/configtypes/namespace.go | 2 +- internal/configtypes/types.go | 6 +- main.go | 1 + 5 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 internal/cli/defaultenv.go diff --git a/internal/cli/defaultenv.go b/internal/cli/defaultenv.go new file mode 100644 index 000000000..779420985 --- /dev/null +++ b/internal/cli/defaultenv.go @@ -0,0 +1,104 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "sort" + "strings" + + "github.com/centrifugal/centrifugo/v5/internal/config/envconfig" + + "github.com/centrifugal/centrifugo/v5/internal/config" + "github.com/spf13/cobra" +) + +func DefaultEnvCommand() *cobra.Command { + var baseConfigFile string + var defaultEnvCmd = &cobra.Command{ + Use: "defaultenv", + Short: "Generate full environment var list with defaults", + Long: `Generate full Centrifugo environment var list with defaults`, + Run: func(cmd *cobra.Command, args []string) { + DefaultEnv(baseConfigFile) + }, + } + defaultEnvCmd.Flags().StringVarP(&baseConfigFile, "base", "b", "", "path to the base config file to use") + return defaultEnvCmd +} + +func DefaultEnv(baseFile string) { + conf, meta, err := config.GetConfig(nil, baseFile) + if err != nil { + fmt.Printf("error: %v\n", err) + os.Exit(1) + } + if err = conf.Validate(); err != nil { + fmt.Printf("error: %v\n", err) + os.Exit(1) + } + printSortedEnvVars(meta.KnownEnvVars) +} + +func printSortedEnvVars(knownEnvVars map[string]envconfig.VarInfo) { + var envKeys []string + for env := range knownEnvVars { + envKeys = append(envKeys, env) + } + sort.Strings(envKeys) + for _, env := range envKeys { + if strings.HasSuffix(env, "-") { + // Hacky way to skip unnecessary struct field, can be based on struct tag. + continue + } + fmt.Printf("%s=%s\n", env, valueToStringReflect(knownEnvVars[env].Field)) + } +} + +// valueToStringReflect converts a reflect.Value to a string in a way suitable for environment variables. +func valueToStringReflect(v reflect.Value) string { + switch v.Kind() { + case reflect.Slice, reflect.Array: + // Check if the element type of the slice/array is a struct + if v.Type().Elem().Kind() == reflect.Struct { + // Marshal the entire array/slice of structs to JSON + jsonValue, err := json.Marshal(v.Interface()) + if err != nil { + panic(err) + } + // Escape double quotes to make the value suitable for environment variables + return fmt.Sprintf("\"%v\"", strings.ReplaceAll(string(jsonValue), `"`, `\"`)) + } + var elements []string + for i := 0; i < v.Len(); i++ { + elements = append(elements, valueToStringReflect(v.Index(i))) + } + if len(elements) == 0 { + return "\"\"" + } + return fmt.Sprintf("%v", strings.Join(elements, " ")) + case reflect.Struct: + // You can customize how structs should be serialized if needed + return fmt.Sprintf("%v", v.Interface()) // Fallback to default formatting (customize if necessary) + case reflect.Map: + jsonValue, err := json.Marshal(v.Interface()) + if err != nil { + panic(err) + } + // Escape double quotes to make the value suitable for environment variables + return fmt.Sprintf("\"%v\"", strings.ReplaceAll(string(jsonValue), `"`, `\"`)) + case reflect.Ptr: + if v.IsNil() { + return "" + } + return valueToStringReflect(v.Elem()) // Dereference the pointer and recursively process + case reflect.Invalid: + return "" // Handle zero/nil values + case reflect.String: + return fmt.Sprintf("\"%v\"", v.Interface()) + default: + // Fallback for other types (int, bool, etc.) + return fmt.Sprintf("%v", v.Interface()) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 82c622f63..d08f04acf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -132,6 +132,7 @@ type Meta struct { FileNotFound bool UnknownKeys []string UnknownEnvs []string + KnownEnvVars map[string]envconfig.VarInfo } func DefineFlags(rootCmd *cobra.Command) { @@ -210,7 +211,7 @@ func GetConfig(cmd *cobra.Command, configFile string) (Config, Meta, error) { return Config{}, Meta{}, fmt.Errorf("error unmarshaling config: %w", err) } - knownEnvVars := map[string]struct{}{} + knownEnvVars := map[string]envconfig.VarInfo{} varInfo, err := envconfig.Process("CENTRIFUGO", conf) if err != nil { return Config{}, Meta{}, fmt.Errorf("error processing env: %w", err) @@ -264,13 +265,14 @@ func GetConfig(cmd *cobra.Command, configFile string) (Config, Meta, error) { meta.UnknownKeys = findUnknownKeys(v.AllSettings(), conf, "") meta.UnknownEnvs = checkEnvironmentVars(knownEnvVars) + meta.KnownEnvVars = knownEnvVars return *conf, meta, nil } -func extendKnownEnvVars(knownEnvVars map[string]struct{}, varInfo []envconfig.VarInfo) { +func extendKnownEnvVars(knownEnvVars map[string]envconfig.VarInfo, varInfo []envconfig.VarInfo) { for _, info := range varInfo { - knownEnvVars[info.Key] = struct{}{} + knownEnvVars[info.Key] = info } } @@ -364,7 +366,7 @@ func appendKeyPath(parent, key string) string { return parent + "." + key } -func checkEnvironmentVars(knownEnvVars map[string]struct{}) []string { +func checkEnvironmentVars(knownEnvVars map[string]envconfig.VarInfo) []string { var unknownEnvs []string envPrefix := "CENTRIFUGO_" envVars := os.Environ() diff --git a/internal/configtypes/namespace.go b/internal/configtypes/namespace.go index 964fb9d29..461542893 100644 --- a/internal/configtypes/namespace.go +++ b/internal/configtypes/namespace.go @@ -164,5 +164,5 @@ type ChannelOptions struct { } type Compiled struct { - CompiledChannelRegex *regexp.Regexp `json:"-" yaml:"-" toml:"-"` + CompiledChannelRegex *regexp.Regexp `json:"-" yaml:"-" toml:"-" envconfig:"-"` } diff --git a/internal/configtypes/types.go b/internal/configtypes/types.go index 157712053..32fa14930 100644 --- a/internal/configtypes/types.go +++ b/internal/configtypes/types.go @@ -110,7 +110,7 @@ type PingPong struct { // NatsBroker configuration. type NatsBroker struct { // URL is a Nats server URL. - URL string `mapstructure:"url" json:"url" envconfig:"url" yaml:"url" toml:"url"` + URL string `mapstructure:"url" json:"url" envconfig:"url" yaml:"url" toml:"url" default:"nats://localhost:4222"` // Prefix allows customizing channel prefix in Nats to work with a single Nats from different // unrelated Centrifugo setups. Prefix string `mapstructure:"prefix" default:"centrifugo" json:"prefix" envconfig:"prefix" yaml:"prefix" toml:"prefix"` @@ -310,7 +310,7 @@ type RPC struct { type SubscribeToUserPersonalChannel struct { Enabled bool `mapstructure:"enabled" json:"enabled" envconfig:"enabled" yaml:"enabled" toml:"enabled"` PersonalChannelNamespace string `mapstructure:"personal_channel_namespace" json:"personal_channel_namespace" envconfig:"personal_channel_namespace" yaml:"personal_channel_namespace" toml:"personal_channel_namespace"` - SingleConnection bool `mapstructure:"single_connection" json:"single_connection" yaml:"single_connection" toml:"single_connection"` + SingleConnection bool `mapstructure:"single_connection" json:"single_connection" yaml:"single_connection" toml:"single_connection" envconfig:"single_connection"` } type Node struct { @@ -413,7 +413,7 @@ type Proxy struct { ProxyCommon `mapstructure:",squash" yaml:",inline"` - TestGrpcDialer func(context.Context, string) (net.Conn, error) `json:"-" yaml:"-" toml:"-"` + TestGrpcDialer func(context.Context, string) (net.Conn, error) `json:"-" yaml:"-" toml:"-" envconfig:"-"` } type Proxies []Proxy diff --git a/main.go b/main.go index 49ce86520..229087392 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,7 @@ func main() { rootCmd.AddCommand(cli.CheckTokenCommand()) rootCmd.AddCommand(cli.CheckSubTokenCommand()) rootCmd.AddCommand(cli.DefaultConfigCommand()) + rootCmd.AddCommand(cli.DefaultEnvCommand()) _ = rootCmd.Execute() }