Skip to content

Commit

Permalink
feat: allow boltrouter config to be set from env (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
dskart committed May 31, 2023
1 parent c3fc76f commit 17aa433
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 34 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ export AWS_REGION=<YOUR_BOLT_CLUSTER_REGION>
export AWS_ZONE_ID=<AWS_ZONE_ID>
```

### Failover

Sidekick automatically failovers the request to s3 if the bolt request fails. For example This is usefull when the object does not exist in bolt yet.
You can disable failover by passing a flag or setting a ENV variable:

```bash
# Using flag
go run main serve --failover=false
# Using binary
./sidekick serve --failover=false
```

```bash
# Using env variable
export SIDEKICK_BOLTROUTER_FAILOVER=true
go run main serve
```

### Local

You can run sidekick directly from the command line:
Expand Down
6 changes: 3 additions & 3 deletions boltrouter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package boltrouter
type Config struct {
// If set, boltrouter will be running in local mode.
// For example, boultrouter will not query quicksilver to get endpoints.
Local bool
Local bool `yaml:"Local"`

// Enable pass through in Bolt.
Passthrough bool
Passthrough bool `yaml:"Passthrough"`

// Enable failover to a aws request if the Bolt request fails.
Failover bool
Failover bool `yaml:"Failover"`
}
117 changes: 117 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package cmd

import (
"context"
"encoding/base64"
"fmt"
"os"
"reflect"
"strconv"
"strings"

"github.com/project-n-oss/sidekick/boltrouter"
)

type Config struct {
BoltRouter boltrouter.Config `yaml:"BoltRouter"`
}

const configPrefix = "SIDEKICK"

// UnmarshalConfigFromEnv populates config with values from environment variables. The names of the
// environment variables read are formed by joining the config struct's field names with underscores
// and adding a "SIDEKICK" prefix. For example, if you want to set the Redis address, you would use
// SIDEKICK_BOLTROUTER_FAILOVER=true
func UnmarshalConfigFromEnv(ctx context.Context, config *Config) error {
_, err := unmarshalConfig(configPrefix, reflect.ValueOf(config), func(key string) (*string, error) {
v, ok := os.LookupEnv(key)
if !ok {
return nil, nil
}
return &v, nil
})
return err
}

func unmarshalConfig(prefix string, v reflect.Value, lookup func(string) (*string, error)) (didUnmarshal bool, err error) {
if v.Kind() == reflect.Ptr && !v.IsNil() {
if env, err := lookup(prefix); err != nil {
return false, err
} else if env != nil {
switch dest := v.Interface().(type) {
case *bool:
switch *env {
case "false":
*dest = false
case "true":
*dest = true
default:
return false, fmt.Errorf("boolean config must be \"true\" or \"false\"")
}
case *int:
n, err := strconv.Atoi(*env)
if err != nil {
return false, fmt.Errorf("invalid value for integer config")
}
*dest = n
case *string:
*dest = *env
case *[]byte:
buf, err := base64.StdEncoding.DecodeString(*env)
if err != nil {
return false, fmt.Errorf("byte slice configs must be base64 encoded")
}
*dest = buf
case *[]int:
parts := strings.Split(*env, ",")
intParts := make([]int, len(parts))
for i, part := range parts {
intParts[i], err = strconv.Atoi(strings.TrimSpace(part))
if err != nil {
return false, fmt.Errorf("invalid value for integer config")
}
}
*dest = intParts
case *[]string:
parts := strings.Split(*env, ",")
for i, part := range parts {
parts[i] = strings.TrimSpace(part)
}
*dest = parts
default:
return false, fmt.Errorf("unsupported environment config type %T", v.Elem().Interface())
}
return true, nil
}
}

if v.Kind() == reflect.Ptr {
if !v.IsNil() {
return unmarshalConfig(prefix, v.Elem(), lookup)
}

new := reflect.New(v.Type().Elem())
didUnmarshal, err := unmarshalConfig(prefix, new, lookup)
if err != nil {
return false, err
} else if didUnmarshal {
v.Set(new)
}
return didUnmarshal, nil
}

if v.Kind() == reflect.Struct {
t := v.Type()
for i := 0; i < v.NumField(); i++ {
didUnmarshalField, err := unmarshalConfig(prefix+"_"+strings.ToUpper(t.Field(i).Name), v.Field(i).Addr(), lookup)
if err != nil {
return false, err
}
if didUnmarshalField {
didUnmarshal = true
}
}
}

return didUnmarshal, nil
}
55 changes: 55 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"reflect"
"testing"

"github.com/project-n-oss/sidekick/boltrouter"
"github.com/stretchr/testify/assert"
)

func TestUnmarshalConfig(t *testing.T) {
for name, tc := range map[string]struct {
Environment map[string]string
In Config
Expected *Config
}{
"EnvOnly": {
Environment: map[string]string{
"TEST_BOLTROUTER_FAILOVER": "true",
},
Expected: &Config{
BoltRouter: boltrouter.Config{
Failover: true,
},
},
},
"Override": {
Environment: map[string]string{
"TEST_BOLTROUTER_FAILOVER": "false",
},
In: Config{
BoltRouter: boltrouter.Config{
Failover: true,
},
},
Expected: &Config{
BoltRouter: boltrouter.Config{
Failover: false,
},
},
},
} {
t.Run(name, func(t *testing.T) {
config := tc.In
_, err := unmarshalConfig("TEST", reflect.ValueOf(&config), func(key string) (*string, error) {
if v, ok := tc.Environment[key]; ok {
return &v, nil
}
return nil, nil
})
assert.NoError(t, err)
assert.Equal(t, tc.Expected, &config)
})
}
}
20 changes: 12 additions & 8 deletions cmd/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ import (

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/sys/unix"
"golang.org/x/term"
)

func NewLogger(verbose bool) *zap.Logger {
logEncoder := getConsoleEncoder()
var logEncoder zapcore.Encoder
if !term.IsTerminal(unix.Stdout) {
encoderConfig := getEncoderConfig()
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
logEncoder = zapcore.NewJSONEncoder(encoderConfig)
} else {
logEncoder = zapcore.NewConsoleEncoder(getEncoderConfig())
}

zapAtom := zap.NewAtomicLevel()
zapAtom.SetLevel(zapcore.InfoLevel)
Expand All @@ -29,20 +38,15 @@ func NewLogger(verbose bool) *zap.Logger {
zapAtom.SetLevel(zapcore.DebugLevel)
}

OnShutdown(func() {
_ = logger.Sync()
})

return ret
}

func getConsoleEncoder() zapcore.Encoder {
func getEncoderConfig() zapcore.EncoderConfig {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = customMilliTimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
encoderConfig.CallerKey = "caller"
return zapcore.NewConsoleEncoder(encoderConfig)
return encoderConfig
}

func customMilliTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
Expand Down
34 changes: 30 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
_ "embed"
"fmt"
"os"
Expand All @@ -12,6 +13,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)

//go:embed ascii.txt
Expand All @@ -30,7 +32,8 @@ func init() {
rootCmd.PersistentFlags().StringP("config", "c", "", "read configuration from this file")
}

var logger *zap.Logger
var rootLogger *zap.Logger
var rootConfig Config

var rootCmd = &cobra.Command{
Use: "sidekick",
Expand All @@ -39,7 +42,10 @@ var rootCmd = &cobra.Command{
SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
verbose, _ := cmd.Flags().GetBool("verbose")
logger = NewLogger(verbose)
rootLogger = NewLogger(verbose)
OnShutdown(func() {
_ = rootLogger.Sync()
})

if _, err := os.Stat(".env"); err == nil {
err := godotenv.Load()
Expand All @@ -48,13 +54,33 @@ var rootCmd = &cobra.Command{
}
}

if config, _ := cmd.Flags().GetString("config"); config != "" {
f, err := os.Open(config)
if err != nil {
return err
}
defer f.Close()
if err := yaml.NewDecoder(f).Decode(&rootConfig); err != nil {
return fmt.Errorf("failed to decode config: %w", err)
}
} else if f, err := os.Open("config.yaml"); err == nil {
defer f.Close()
if err := yaml.NewDecoder(f).Decode(&rootConfig); err != nil {
return fmt.Errorf("failed to decode config: %w", err)
}
}

if err := UnmarshalConfigFromEnv(context.Background(), &rootConfig); err != nil {
return err
}

// wait forever for sig signal
go func() {
WaitForTermSignal()
}()

fmt.Println(asciiArt)
logger.Sugar().Infof("Version: %s", getVersion())
rootLogger.Sugar().Infof("Version: %s", getVersion())

return nil
},
Expand All @@ -70,7 +96,7 @@ func WaitForTermSignal() {
signal.Notify(sigs, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT)

sig := <-sigs
logger.Info("received signal, shutting down", zap.String("signal", sig.String()))
rootLogger.Info("received signal, shutting down", zap.String("signal", sig.String()))

// Do a graceful shutdown
Shutdown()
Expand Down
Loading

0 comments on commit 17aa433

Please sign in to comment.