mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-21 21:33:06 +01:00
fix(config): fix loading config precedence
This commit is contained in:
31
cmd/root.go
31
cmd/root.go
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,8 +18,17 @@ const (
|
|||||||
defaultConfigFilename = "config"
|
defaultConfigFilename = "config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var conf = config.NewConfig()
|
||||||
rootCmd = &cobra.Command{
|
|
||||||
|
func Execute() {
|
||||||
|
cmd := NewRootCommand()
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRootCommand() *cobra.Command {
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
Use: "sablier",
|
Use: "sablier",
|
||||||
Short: "A webserver to start container on demand",
|
Short: "A webserver to start container on demand",
|
||||||
Long: `Sablier is an API that start containers on demand.
|
Long: `Sablier is an API that start containers on demand.
|
||||||
@@ -28,18 +38,8 @@ It provides an integrations with multiple reverse proxies and different loading
|
|||||||
return initializeConfig(cmd)
|
return initializeConfig(cmd)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// Execute executes the root command.
|
startCmd := newStartCommand()
|
||||||
func Execute() error {
|
|
||||||
return rootCmd.Execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
var conf = config.NewConfig()
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
|
|
||||||
rootCmd.AddCommand(startCmd)
|
|
||||||
// Provider flags
|
// Provider flags
|
||||||
startCmd.Flags().StringVar(&conf.Provider.Name, "provider.name", "docker", fmt.Sprintf("Provider to use to manage containers %v", config.GetProviders()))
|
startCmd.Flags().StringVar(&conf.Provider.Name, "provider.name", "docker", fmt.Sprintf("Provider to use to manage containers %v", config.GetProviders()))
|
||||||
viper.BindPFlag("provider.name", startCmd.Flags().Lookup("provider.name"))
|
viper.BindPFlag("provider.name", startCmd.Flags().Lookup("provider.name"))
|
||||||
@@ -54,7 +54,7 @@ func init() {
|
|||||||
// Sessions flags
|
// Sessions flags
|
||||||
startCmd.Flags().DurationVar(&conf.Sessions.DefaultDuration, "sessions.default-duration", time.Duration(5)*time.Minute, "The default session duration")
|
startCmd.Flags().DurationVar(&conf.Sessions.DefaultDuration, "sessions.default-duration", time.Duration(5)*time.Minute, "The default session duration")
|
||||||
viper.BindPFlag("sessions.default-duration", startCmd.Flags().Lookup("sessions.default-duration"))
|
viper.BindPFlag("sessions.default-duration", startCmd.Flags().Lookup("sessions.default-duration"))
|
||||||
startCmd.Flags().DurationVar(&conf.Sessions.DefaultDuration, "sessions.expiration-interval", time.Duration(20)*time.Second, "The expiration checking interval. Higher duration gives less stress on CPU. If you only use sessions of 1h, setting this to 5m is a good trade-off.")
|
startCmd.Flags().DurationVar(&conf.Sessions.ExpirationInterval, "sessions.expiration-interval", time.Duration(20)*time.Second, "The expiration checking interval. Higher duration gives less stress on CPU. If you only use sessions of 1h, setting this to 5m is a good trade-off.")
|
||||||
viper.BindPFlag("sessions.expiration-interval", startCmd.Flags().Lookup("sessions.expiration-interval"))
|
viper.BindPFlag("sessions.expiration-interval", startCmd.Flags().Lookup("sessions.expiration-interval"))
|
||||||
|
|
||||||
// logging level
|
// logging level
|
||||||
@@ -71,7 +71,10 @@ func init() {
|
|||||||
startCmd.Flags().DurationVar(&conf.Strategy.Blocking.DefaultTimeout, "strategy.blocking.default-timeout", 1*time.Minute, "Default timeout used for blocking strategy")
|
startCmd.Flags().DurationVar(&conf.Strategy.Blocking.DefaultTimeout, "strategy.blocking.default-timeout", 1*time.Minute, "Default timeout used for blocking strategy")
|
||||||
viper.BindPFlag("strategy.blocking.default-timeout", startCmd.Flags().Lookup("strategy.blocking.default-timeout"))
|
viper.BindPFlag("strategy.blocking.default-timeout", startCmd.Flags().Lookup("strategy.blocking.default-timeout"))
|
||||||
|
|
||||||
|
rootCmd.AddCommand(startCmd)
|
||||||
rootCmd.AddCommand(versionCmd)
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
|
||||||
|
return rootCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeConfig(cmd *cobra.Command) error {
|
func initializeConfig(cmd *cobra.Command) error {
|
||||||
|
|||||||
183
cmd/root_test.go
Normal file
183
cmd/root_test.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/acouvreur/sablier/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrecedence(t *testing.T) {
|
||||||
|
// Run the tests in a temporary directory
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
testDir, err := os.Getwd()
|
||||||
|
require.NoError(t, err, "error getting the current working directory")
|
||||||
|
defer os.Chdir(testDir)
|
||||||
|
err = os.Chdir(tmpDir)
|
||||||
|
require.NoError(t, err, "error changing to the temporary test directory")
|
||||||
|
|
||||||
|
// CHANGE `startCmd` behavior to only print the config, this is for testing purposes only
|
||||||
|
newStartCommand = mockStartCommand
|
||||||
|
|
||||||
|
t.Run("config file", func(t *testing.T) {
|
||||||
|
configB, err := os.ReadFile(filepath.Join(testDir, "testdata", "config.yml"))
|
||||||
|
require.NoError(t, err, "error reading test config file")
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tmpDir, "config.yml"), configB, 0644)
|
||||||
|
require.NoError(t, err, "error writing test config file")
|
||||||
|
defer os.Remove(filepath.Join(tmpDir, "config.yml"))
|
||||||
|
|
||||||
|
wantConfig, err := ioutil.ReadFile(filepath.Join(testDir, "testdata", "config_yaml_wanted.json"))
|
||||||
|
require.NoError(t, err, "error reading test config file")
|
||||||
|
|
||||||
|
conf = config.NewConfig()
|
||||||
|
cmd := NewRootCommand()
|
||||||
|
output := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(output)
|
||||||
|
cmd.SetArgs([]string{"start"})
|
||||||
|
cmd.Execute()
|
||||||
|
|
||||||
|
gotOutput := output.String()
|
||||||
|
|
||||||
|
assert.Equal(t, string(wantConfig), gotOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env var", func(t *testing.T) {
|
||||||
|
// 1. Load Config file for precedence assertions
|
||||||
|
configB, err := os.ReadFile(filepath.Join(testDir, "testdata", "config.yml"))
|
||||||
|
require.NoError(t, err, "error reading test config file")
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tmpDir, "config.yml"), configB, 0644)
|
||||||
|
require.NoError(t, err, "error writing test config file")
|
||||||
|
defer os.Remove(filepath.Join(tmpDir, "config.yml"))
|
||||||
|
|
||||||
|
setEnvsFromFile(filepath.Join(testDir, "testdata", "config.env"))
|
||||||
|
defer unsetEnvsFromFile(filepath.Join(testDir, "testdata", "config.env"))
|
||||||
|
|
||||||
|
wantConfig, err := ioutil.ReadFile(filepath.Join(testDir, "testdata", "config_env_wanted.json"))
|
||||||
|
require.NoError(t, err, "error reading test config file")
|
||||||
|
|
||||||
|
conf = config.NewConfig()
|
||||||
|
cmd := NewRootCommand()
|
||||||
|
output := &bytes.Buffer{}
|
||||||
|
cmd.SetOut(output)
|
||||||
|
cmd.SetArgs([]string{"start"})
|
||||||
|
cmd.Execute()
|
||||||
|
|
||||||
|
gotOutput := output.String()
|
||||||
|
|
||||||
|
assert.Equal(t, string(wantConfig), gotOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("flag", func(t *testing.T) {
|
||||||
|
// 1. Load Config file for precedence assertions
|
||||||
|
configB, err := os.ReadFile(filepath.Join(testDir, "testdata", "config.yml"))
|
||||||
|
require.NoError(t, err, "error reading test config file")
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tmpDir, "config.yml"), configB, 0644)
|
||||||
|
require.NoError(t, err, "error writing test config file")
|
||||||
|
defer os.Remove(filepath.Join(tmpDir, "config.yml"))
|
||||||
|
|
||||||
|
// 2. Load envs variable for precedence assertions
|
||||||
|
setEnvsFromFile(filepath.Join(testDir, "testdata", "config.env"))
|
||||||
|
defer unsetEnvsFromFile(filepath.Join(testDir, "testdata", "config.env"))
|
||||||
|
|
||||||
|
wantConfig, err := ioutil.ReadFile(filepath.Join(testDir, "testdata", "config_cli_wanted.json"))
|
||||||
|
require.NoError(t, err, "error reading test config file")
|
||||||
|
|
||||||
|
cmd := NewRootCommand()
|
||||||
|
output := &bytes.Buffer{}
|
||||||
|
conf = config.NewConfig()
|
||||||
|
cmd.SetOut(output)
|
||||||
|
cmd.SetArgs([]string{
|
||||||
|
"start",
|
||||||
|
"--provider.name", "cli",
|
||||||
|
"--server.port", "3333",
|
||||||
|
"--server.base-path", "/cli/",
|
||||||
|
"--storage.file", "/tmp/cli.json",
|
||||||
|
"--sessions.default-duration", "3h",
|
||||||
|
"--sessions.expiration-interval", "3h",
|
||||||
|
"--logging.level", "info",
|
||||||
|
"--strategy.dynamic.custom-themes-path", "/tmp/cli/themes",
|
||||||
|
"--strategy.dynamic.default-theme", "cli",
|
||||||
|
"--strategy.dynamic.default-refresh-frequency", "3h",
|
||||||
|
"--strategy.blocking.default-timeout", "3h",
|
||||||
|
})
|
||||||
|
cmd.Execute()
|
||||||
|
|
||||||
|
gotOutput := output.String()
|
||||||
|
|
||||||
|
assert.Equal(t, string(wantConfig), gotOutput)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnvsFromFile(path string) {
|
||||||
|
readFile, err := os.Open(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer readFile.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileScanner := bufio.NewScanner(readFile)
|
||||||
|
|
||||||
|
fileScanner.Split(bufio.ScanLines)
|
||||||
|
|
||||||
|
for fileScanner.Scan() {
|
||||||
|
splitted := strings.Split(fileScanner.Text(), "=")
|
||||||
|
os.Setenv(splitted[0], splitted[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsetEnvsFromFile(path string) {
|
||||||
|
readFile, err := os.Open(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer readFile.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileScanner := bufio.NewScanner(readFile)
|
||||||
|
|
||||||
|
fileScanner.Split(bufio.ScanLines)
|
||||||
|
|
||||||
|
for fileScanner.Scan() {
|
||||||
|
splitted := strings.Split(fileScanner.Text(), "=")
|
||||||
|
os.Unsetenv(splitted[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockStartCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Start the Sablier server",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
viper.Unmarshal(&conf)
|
||||||
|
|
||||||
|
out := cmd.OutOrStdout()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(out)
|
||||||
|
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
encoder.Encode(conf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -2,16 +2,16 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/acouvreur/sablier/app"
|
"github.com/acouvreur/sablier/app"
|
||||||
"github.com/acouvreur/sablier/config"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
var startCmd = &cobra.Command{
|
var newStartCommand = func() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
|
||||||
Use: "start",
|
Use: "start",
|
||||||
Short: "Start the Sablier server",
|
Short: "Start the Sablier server",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
conf := config.NewConfig()
|
|
||||||
viper.Unmarshal(&conf)
|
viper.Unmarshal(&conf)
|
||||||
|
|
||||||
err := app.Start(conf)
|
err := app.Start(conf)
|
||||||
@@ -19,4 +19,6 @@ var startCmd = &cobra.Command{
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
11
cmd/testdata/config.env
vendored
Normal file
11
cmd/testdata/config.env
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
PROVIDER_NAME=envvar
|
||||||
|
SERVER_PORT=2222
|
||||||
|
SERVER_BASE_PATH=/envvar/
|
||||||
|
STORAGE_FILE=/tmp/envvar.json
|
||||||
|
SESSIONS_DEFAULT_DURATION=2h
|
||||||
|
SESSIONS_EXPIRATION_INTERVAL=2h
|
||||||
|
LOGGING_LEVEL=debug
|
||||||
|
STRATEGY_DYNAMIC_CUSTOM_THEMES_PATH=/tmp/envvar/themes
|
||||||
|
STRATEGY_DYNAMIC_DEFAULT_THEME=envvar
|
||||||
|
STRATEGY_DYNAMIC_DEFAULT_REFRESH_FREQUENCY=2h
|
||||||
|
STRATEGY_BLOCKING_DEFAULT_TIMEOUT=2h
|
||||||
19
cmd/testdata/config.yml
vendored
Normal file
19
cmd/testdata/config.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
provider:
|
||||||
|
name: configfile
|
||||||
|
server:
|
||||||
|
port: 1111
|
||||||
|
base-path: /configfile/
|
||||||
|
storage:
|
||||||
|
file: /tmp/configfile.json
|
||||||
|
sessions:
|
||||||
|
default-duration: 1h
|
||||||
|
expiration-interval: 1h
|
||||||
|
logging:
|
||||||
|
level: trace
|
||||||
|
strategy:
|
||||||
|
dynamic:
|
||||||
|
custom-themes-path: /tmp/configfile/themes
|
||||||
|
default-theme: configfile
|
||||||
|
default-refresh-frequency: 1h
|
||||||
|
blocking:
|
||||||
|
default-timeout: 1h
|
||||||
29
cmd/testdata/config_cli_wanted.json
vendored
Normal file
29
cmd/testdata/config_cli_wanted.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"Server": {
|
||||||
|
"Port": 3333,
|
||||||
|
"BasePath": "/cli/"
|
||||||
|
},
|
||||||
|
"Storage": {
|
||||||
|
"File": "/tmp/cli.json"
|
||||||
|
},
|
||||||
|
"Provider": {
|
||||||
|
"Name": "cli"
|
||||||
|
},
|
||||||
|
"Sessions": {
|
||||||
|
"DefaultDuration": 10800000000000,
|
||||||
|
"ExpirationInterval": 10800000000000
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"Level": "info"
|
||||||
|
},
|
||||||
|
"Strategy": {
|
||||||
|
"Dynamic": {
|
||||||
|
"CustomThemesPath": "/tmp/cli/themes",
|
||||||
|
"DefaultTheme": "cli",
|
||||||
|
"DefaultRefreshFrequency": 10800000000000
|
||||||
|
},
|
||||||
|
"Blocking": {
|
||||||
|
"DefaultTimeout": 10800000000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
cmd/testdata/config_env_wanted.json
vendored
Normal file
29
cmd/testdata/config_env_wanted.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"Server": {
|
||||||
|
"Port": 2222,
|
||||||
|
"BasePath": "/envvar/"
|
||||||
|
},
|
||||||
|
"Storage": {
|
||||||
|
"File": "/tmp/envvar.json"
|
||||||
|
},
|
||||||
|
"Provider": {
|
||||||
|
"Name": "envvar"
|
||||||
|
},
|
||||||
|
"Sessions": {
|
||||||
|
"DefaultDuration": 7200000000000,
|
||||||
|
"ExpirationInterval": 7200000000000
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"Level": "debug"
|
||||||
|
},
|
||||||
|
"Strategy": {
|
||||||
|
"Dynamic": {
|
||||||
|
"CustomThemesPath": "/tmp/envvar/themes",
|
||||||
|
"DefaultTheme": "envvar",
|
||||||
|
"DefaultRefreshFrequency": 7200000000000
|
||||||
|
},
|
||||||
|
"Blocking": {
|
||||||
|
"DefaultTimeout": 7200000000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
cmd/testdata/config_yaml_wanted.json
vendored
Normal file
29
cmd/testdata/config_yaml_wanted.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"Server": {
|
||||||
|
"Port": 1111,
|
||||||
|
"BasePath": "/configfile/"
|
||||||
|
},
|
||||||
|
"Storage": {
|
||||||
|
"File": "/tmp/configfile.json"
|
||||||
|
},
|
||||||
|
"Provider": {
|
||||||
|
"Name": "configfile"
|
||||||
|
},
|
||||||
|
"Sessions": {
|
||||||
|
"DefaultDuration": 3600000000000,
|
||||||
|
"ExpirationInterval": 3600000000000
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"Level": "trace"
|
||||||
|
},
|
||||||
|
"Strategy": {
|
||||||
|
"Dynamic": {
|
||||||
|
"CustomThemesPath": "/tmp/configfile/themes",
|
||||||
|
"DefaultTheme": "configfile",
|
||||||
|
"DefaultRefreshFrequency": 3600000000000
|
||||||
|
},
|
||||||
|
"Blocking": {
|
||||||
|
"DefaultTimeout": 3600000000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package config
|
|||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Port int `mapstructure:"PORT" yaml:"port" default:"10000"`
|
Port int `mapstructure:"PORT" yaml:"port" default:"10000"`
|
||||||
BasePath string `mapstructure:"BASEPATH" yaml:"basePath" default:"/"`
|
BasePath string `mapstructure:"BASE_PATH" yaml:"basePath" default:"/"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServerConfig() Server {
|
func NewServerConfig() Server {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package config
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type DynamicStrategy struct {
|
type DynamicStrategy struct {
|
||||||
CustomThemesPath string `mapstructure:"CUSTOMTHEMESPATH" yaml:"customThemesPath"`
|
CustomThemesPath string `mapstructure:"CUSTOM_THEMES_PATH" yaml:"customThemesPath"`
|
||||||
DefaultTheme string `mapstructure:"DEFAULTTHEME" yaml:"defaultTheme" default:"hacker-terminal"`
|
DefaultTheme string `mapstructure:"DEFAULT_THEME" yaml:"defaultTheme" default:"hacker-terminal"`
|
||||||
DefaultRefreshFrequency time.Duration `mapstructure:"DEFAULTREFRESHFREQUENCY" yaml:"defaultRefreshFrequency" default:"5s"`
|
DefaultRefreshFrequency time.Duration `mapstructure:"DEFAULT_REFRESH_FREQUENCY" yaml:"defaultRefreshFrequency" default:"5s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockingStrategy struct {
|
type BlockingStrategy struct {
|
||||||
DefaultTimeout time.Duration `mapstructure:"DEFAULTTIMEOUT" yaml:"defaultTimeout" default:"1m"`
|
DefaultTimeout time.Duration `mapstructure:"DEFAULT_TIMEOUT" yaml:"defaultTimeout" default:"1m"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user