From 91c2164e493a90a3708323efc08cc9bc708c9e84 Mon Sep 17 00:00:00 2001 From: Alex Quick Date: Sun, 15 Oct 2017 20:10:46 -0400 Subject: [PATCH] [config] refactor package structure --- plugins/config/config.go | 89 ++++------ plugins/config/config_test.go | 106 ++++++++++- .../configenv/configenv.go => environment.go} | 103 ++++++----- plugins/config/environment_test.go | 102 +++++++++++ plugins/config/src/commands/commands.go | 23 +-- .../config/src/configenv/configenv_test.go | 66 ------- .../config/src/subcommands/bundle/bundle.go | 10 +- .../config/src/subcommands/export/export.go | 28 +-- plugins/config/src/subcommands/get/get.go | 22 +-- plugins/config/src/subcommands/keys/keys.go | 12 +- plugins/config/src/subcommands/set/set.go | 26 +-- plugins/config/src/subcommands/unset/unset.go | 4 +- plugins/config/subcommands.go | 167 ++++++++++++++++++ 13 files changed, 469 insertions(+), 289 deletions(-) rename plugins/config/{src/configenv/configenv.go => environment.go} (74%) create mode 100644 plugins/config/environment_test.go delete mode 100644 plugins/config/src/configenv/configenv_test.go create mode 100644 plugins/config/subcommands.go diff --git a/plugins/config/config.go b/plugins/config/config.go index 545350665..575b6c3ee 100644 --- a/plugins/config/config.go +++ b/plugins/config/config.go @@ -4,32 +4,33 @@ import ( "fmt" "github.com/dokku/dokku/plugins/common" - configenv "github.com/dokku/dokku/plugins/config/src/configenv" ) -//GetWithDefault gets a value from a config. If appName is empty the global config is used. -func GetWithDefault(appName string, key string, defaultValue string) (value string) { - env, err := loadConfig(appName) +//Get retreives a value from a config. If appName is empty the global config is used. +func Get(appName string, key string) (value string, ok bool) { + env, err := loadAppOrGlobalEnv(appName) if err != nil { - return defaultValue + return "", false } - return env.GetDefault(key, defaultValue) + return env.Get(key) } -//HasKey determines if the config given by appName has a value for the given key -func HasKey(appName string, key string) (ok bool) { - env, err := loadConfig(appName) - if err != nil { - return false +//GetWithDefault gets a value from a config. If appName is empty the global config is used. If the appName or key do not exist defaultValue is returned. +func GetWithDefault(appName string, key string, defaultValue string) (value string) { + value, ok := Get(appName, key) + if !ok { + return defaultValue } - _, ok = env.Get(key) - return ok + return value } //SetMany variables in the environment. If appName is empty the global config is used. If restart is true the app is restarted. -func SetMany(appName string, entries map[string]string, restart bool) { +func SetMany(appName string, entries map[string]string, restart bool) (err error) { global := appName == "" - env := GetConfig(appName, false) + env, err := loadAppOrGlobalEnv(appName) + if err != nil { + return + } keys := make([]string, 0, len(entries)) for k, v := range entries { env.Set(k, v) @@ -37,20 +38,24 @@ func SetMany(appName string, entries map[string]string, restart bool) { } if len(entries) != 0 { common.LogInfo1("Setting config vars") - fmt.Println(configenv.PrettyPrintEnvEntries(" ", entries)) + fmt.Println(prettyPrintEnvEntries(" ", entries)) env.Write() args := append([]string{appName, "set"}, keys...) common.PlugnTrigger("post-config-update", args...) } if !global && restart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) { - Restart(appName) + triggerRestart(appName) } + return } //UnsetMany a value in a config. If appName is empty the global config is used. If restart is true the app is restarted. -func UnsetMany(appName string, keys []string, restart bool) { +func UnsetMany(appName string, keys []string, restart bool) (err error) { global := appName == "" - env := GetConfig(appName, false) + env, err := loadAppOrGlobalEnv(appName) + if err != nil { + return + } var changed = false for _, k := range keys { common.LogInfo1(fmt.Sprintf("Unsetting %s", k)) @@ -63,53 +68,19 @@ func UnsetMany(appName string, keys []string, restart bool) { common.PlugnTrigger("post-config-update", args...) } if !global && restart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) { - Restart(appName) + triggerRestart(appName) } + return } -//GetCommonArgs extracts common positional args (appName and keys) -func GetCommonArgs(global bool, args []string) (appName string, keys []string) { - nextArg := 0 - if !global { - if len(args) > 0 { - appName = args[0] - } - if appName == "" { - common.LogFail("Please specify an app or --global") - } else { - nextArg++ - } - } - keys = args[nextArg:] - return appName, keys -} - -//GetConfig for the given app (global config if appName is empty). Merge with global config if merged is true. -func GetConfig(appName string, merged bool) (env *configenv.Env) { - env, err := loadConfig(appName) - if err != nil { - common.LogFail(err.Error()) - } - if appName != "" && merged { - global, err := configenv.LoadGlobal() - if err != nil { - common.LogFail(err.Error()) - } - global.Merge(env) - return global - } - return env -} - -//Restart trigger restart on app -func Restart(appName string) { +func triggerRestart(appName string) { common.LogInfo1(fmt.Sprintf("Restarting app %s", appName)) common.PlugnTrigger("app-restart", appName) } -func loadConfig(appName string) (env *configenv.Env, err error) { +func loadAppOrGlobalEnv(appName string) (env *Env, err error) { if appName == "" || appName == "--global" { - return configenv.LoadGlobal() + return LoadGlobalEnv() } - return configenv.LoadApp(appName) + return LoadAppEnv(appName) } diff --git a/plugins/config/config_test.go b/plugins/config/config_test.go index dbe660065..2537ed229 100644 --- a/plugins/config/config_test.go +++ b/plugins/config/config_test.go @@ -6,20 +6,29 @@ import ( "strings" "testing" + "github.com/dokku/dokku/plugins/common" + . "github.com/onsi/gomega" ) var ( - testAppName = "test-app-1" - testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "") + testAppName = "test-app-1" + dokkuRoot = common.MustGetEnv("DOKKU_ROOT") + testAppDir = strings.Join([]string{dokkuRoot, testAppName}, "/") + globalConfigFile = strings.Join([]string{dokkuRoot, "ENV"}, "/") ) func setupTestApp() (err error) { - Expect(os.MkdirAll(testAppDir, 0644)).To(Succeed()) + Expect(os.MkdirAll(testAppDir, 0766)).To(Succeed()) b := []byte("export testKey=TESTING\n") if err = ioutil.WriteFile(strings.Join([]string{testAppDir, "/ENV"}, ""), b, 0644); err != nil { return } + + b = []byte("export testKey=GLOBAL_TESTING\nexport globalKey=GLOBAL_VALUE") + if err = ioutil.WriteFile(globalConfigFile, b, 0644); err != nil { + return + } return } @@ -32,5 +41,96 @@ func TestConfigGetWithDefault(t *testing.T) { Expect(setupTestApp()).To(Succeed()) Expect(GetWithDefault(testAppName, "unknownKey", "UNKNOWN")).To(Equal("UNKNOWN")) Expect(GetWithDefault(testAppName, "testKey", "testKey")).To(Equal("TESTING")) + Expect(GetWithDefault(testAppName+"-nonexistent", "testKey", "default")).To(Equal("default")) teardownTestApp() } + +func TestConfigGet(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + defer teardownTestApp() + + expectValue(testAppName, "testKey", "TESTING") + expectValue("", "testKey", "GLOBAL_TESTING") + + expectNoValue(testAppName, "testKey2") + expectNoValue("", "testKey2") +} + +func TestConfigSetMany(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + defer teardownTestApp() + + expectValue(testAppName, "testKey", "TESTING") + + vals := map[string]string{"testKey": "updated", "testKey2": "new"} + Expect(SetMany(testAppName, vals, false)).To(Succeed()) + expectValue(testAppName, "testKey", "updated") + expectValue(testAppName, "testKey2", "new") + + vals = map[string]string{"testKey": "updated_global", "testKey2": "new_global"} + Expect(SetMany("", vals, false)).To(Succeed()) + expectValue("", "testKey", "updated_global") + expectValue("", "testKey2", "new_global") + expectValue("", "globalKey", "GLOBAL_VALUE") + expectValue(testAppName, "testKey", "updated") + expectValue(testAppName, "testKey2", "new") + + Expect(SetMany(testAppName+"does_not_exist", vals, false)).ToNot(Succeed()) +} + +func TestConfigUnsetMany(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + defer teardownTestApp() + + expectValue(testAppName, "testKey", "TESTING") + expectValue("", "testKey", "GLOBAL_TESTING") + + keys := []string{"testKey", "noKey"} + Expect(UnsetMany(testAppName, keys, false)).To(Succeed()) + expectNoValue(testAppName, "testKey") + expectValue("", "testKey", "GLOBAL_TESTING") + + Expect(UnsetMany(testAppName, keys, false)).To(Succeed()) + expectNoValue(testAppName, "testKey") + expectNoValue(testAppName, "globalKey") + + Expect(UnsetMany(testAppName+"does-not-exist", keys, false)).ToNot(Succeed()) +} + +func TestEnvironmentLoading(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + defer teardownTestApp() + + env, err := LoadMergedAppEnv(testAppName) + Expect(err).To(Succeed()) + v, _ := env.Get("testKey") + Expect(v).To(Equal("TESTING")) + v, _ = env.Get("globalKey") + Expect(v).To(Equal("GLOBAL_VALUE")) + Expect(env.Write()).ToNot(Succeed()) + + env, err = LoadAppEnv(testAppName) + env.Set("testKey", "TESTING-updated") + env.Set("testKey2", "TESTING-'\n'-updated") + env.Write() + + expectValue(testAppName, "testKey", "TESTING-updated") + expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + expectValue("", "testKey", "GLOBAL_TESTING") + Expect(err).To(Succeed()) +} + +func expectValue(appName string, key string, expected string) { + v, ok := Get(appName, key) + Expect(ok).To(Equal(true)) + Expect(v).To(Equal(expected)) +} + +func expectNoValue(appName string, key string) { + _, ok := Get(appName, key) + Expect(ok).To(Equal(false)) +} diff --git a/plugins/config/src/configenv/configenv.go b/plugins/config/environment.go similarity index 74% rename from plugins/config/src/configenv/configenv.go rename to plugins/config/environment.go index 5ddd93732..43417cd87 100644 --- a/plugins/config/src/configenv/configenv.go +++ b/plugins/config/environment.go @@ -1,4 +1,4 @@ -package configenv +package config import ( "errors" @@ -21,16 +21,16 @@ import ( type ExportFormat int const ( - //Exports format: Sourceable exports - Exports ExportFormat = iota - //Envfile format: dotenv file - Envfile - //DockerArgs format: --env args for docker - DockerArgs - //Shell format: env arguments for shell - Shell - //Pretty format: pretty-printed in columns - Pretty + //ExportFormatExports format: Sourceable exports + ExportFormatExports ExportFormat = iota + //ExportFormatEnvfile format: dotenv file + ExportFormatEnvfile + //ExportFormatDockerArgs format: --env args for docker + ExportFormatDockerArgs + //ExportFormatShell format: env arguments for shell + ExportFormatShell + //ExportFormatPretty format: pretty-printed in columns + ExportFormatPretty ) //Env is a representation for global or app environment @@ -40,8 +40,8 @@ type Env struct { env map[string]string } -//NewFromString creates an env from the given ENVFILE contents representation -func NewFromString(rep string) (env *Env, err error) { +//newEnvFromString creates an env from the given ENVFILE contents representation +func newEnvFromString(rep string) (env *Env, err error) { envMap, err := godotenv.Unmarshal(rep) env = &Env{ "", @@ -51,8 +51,8 @@ func NewFromString(rep string) (env *Env, err error) { return } -//LoadApp loads an environment for the given app -func LoadApp(appName string) (env *Env, err error) { +//LoadAppEnv loads an environment for the given app +func LoadAppEnv(appName string) (env *Env, err error) { appfile, err := getAppFile(appName) if err != nil { return @@ -60,8 +60,24 @@ func LoadApp(appName string) (env *Env, err error) { return loadFromFile(appName, appfile) } -//LoadGlobal loads the global environmen -func LoadGlobal() (*Env, error) { +//LoadMergedAppEnv loads an app environment merged with the global environment +func LoadMergedAppEnv(appName string) (env *Env, err error) { + env, err = LoadAppEnv(appName) + if err != nil { + return + } + global, err := LoadGlobalEnv() + if err != nil { + common.LogFail(err.Error()) + } + global.Merge(env) + global.filename = "" + global.name = env.name + return global, err +} + +//LoadGlobalEnv loads the global environment +func LoadGlobalEnv() (*Env, error) { return loadFromFile("global", getGlobalFile()) } @@ -142,16 +158,16 @@ func (e *Env) Write() error { //Export the Env in the given format func (e *Env) Export(format ExportFormat) string { switch format { - case Exports: + case ExportFormatExports: return e.ExportfileString() - case Envfile: + case ExportFormatEnvfile: return e.EnvfileString() - case DockerArgs: + case ExportFormatDockerArgs: return e.DockerArgsString() - case Shell: + case ExportFormatShell: return e.ShellString() - case Pretty: - return PrettyPrintEnvEntries("", e.Map()) + case ExportFormatPretty: + return prettyPrintEnvEntries("", e.Map()) default: common.LogFail(fmt.Sprintf("Unknown export format: %v", format)) return "" @@ -166,18 +182,18 @@ func (e *Env) EnvfileString() string { //ExportfileString returns the contents of this Env as bash exports func (e *Env) ExportfileString() string { - return e.stringWithPrefixAndSeparator("export ", "\n", true) + return e.stringWithPrefixAndSeparator("export ", "\n") } //DockerArgsString gets the contents of this Env in the form -env=KEY=VALUE --env... func (e *Env) DockerArgsString() string { - return e.stringWithPrefixAndSeparator("--env=", " ", true) + return e.stringWithPrefixAndSeparator("--env=", " ") } //ShellString gets the contents of this Env in the form "KEY='value' KEY2='value'" // for passing the environment in the shell func (e *Env) ShellString() string { - return e.stringWithPrefixAndSeparator("", " ", true) + return e.stringWithPrefixAndSeparator("", " ") } //ExportBundle writes a tarfile of the environmnet to the given io.Writer. @@ -202,13 +218,25 @@ func (e *Env) ExportBundle(dest io.Writer) error { return nil } -//SingleQuoteEscape escapes the value as if it were shell-quoted in single quotes -func SingleQuoteEscape(value string) string { // so that 'esc'apped' -> 'esc'\''aped' +//stringWithPrefixAndSeparator makes a string of the environment +// with the given prefix and separator for each entry +func (e *Env) stringWithPrefixAndSeparator(prefix string, separator string) string { + keys := e.Keys() + entries := make([]string, len(keys)) + for i, k := range keys { + v := singleQuoteEscape(e.env[k]) + entries[i] = fmt.Sprintf("%s%s='%s'", prefix, k, v) + } + return strings.Join(entries, separator) +} + +//singleQuoteEscape escapes the value as if it were shell-quoted in single quotes +func singleQuoteEscape(value string) string { // so that 'esc'apped' -> 'esc'\''aped' return strings.Replace(value, "'", "'\\''", -1) } -//PrettyPrintEnvEntries in columns -func PrettyPrintEnvEntries(prefix string, entries map[string]string) (representation string) { +//prettyPrintEnvEntries in columns +func prettyPrintEnvEntries(prefix string, entries map[string]string) (representation string) { colConfig := columnize.DefaultConfig() colConfig.Prefix = prefix colConfig.Delim = "\x00" @@ -233,21 +261,6 @@ func loadFromFile(name string, filename string) (env *Env, err error) { return } -//stringWithPrefixAndSeparator makes a string of the environment -// with the given prefix and separator for each entry -func (e *Env) stringWithPrefixAndSeparator(prefix string, separator string, allowNewlines bool) string { - keys := e.Keys() - entries := make([]string, len(keys)) - for i, k := range keys { - v := SingleQuoteEscape(e.env[k]) - if !allowNewlines { - v = strings.Replace(v, "\n", "'$'\\n''", -1) - } - entries[i] = fmt.Sprintf("%s%s='%s'", prefix, k, v) - } - return strings.Join(entries, separator) -} - func getAppFile(appName string) (string, error) { err := common.VerifyAppName(appName) if err != nil { diff --git a/plugins/config/environment_test.go b/plugins/config/environment_test.go new file mode 100644 index 000000000..c7bef686f --- /dev/null +++ b/plugins/config/environment_test.go @@ -0,0 +1,102 @@ +package config + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestExportfileRoundtrip(t *testing.T) { + RegisterTestingT(t) + env, err := newEnvFromString("HI='ho'\nFoo='Bar'\n\nBaz='BOFF'") + Expect(err).NotTo(HaveOccurred()) + Expect(env.Map()).To(Equal(pairs("Baz", "BOFF", "Foo", "Bar", "HI", "ho"))) + Expect(env.String()).To(Equal("Baz=\"BOFF\"\nFoo=\"Bar\"\nHI=\"ho\"")) + + env, err = newEnvFromString(`export HI="h\no"`) + Expect(err).NotTo(HaveOccurred()) + Expect(env.Map()).To(Equal(pairs("HI", "h\no"))) + Expect(env.String()).To(Equal(`HI="h\no"`)) + + env, err = newEnvFromString("HI='ho'\nFOO=\"'\\nBAR=''\"") + Expect(err).NotTo(HaveOccurred()) + Expect(env.Map()).To(Equal(pairs("HI", "ho", "FOO", "'\nBAR=''"))) + Expect(env.String()).To(Equal("FOO=\"'\\nBAR=''\"\nHI=\"ho\"")) + + env, err = newEnvFromString("FOO='bar' ") + Expect(err).NotTo(HaveOccurred()) + Expect(env.Map()).To(Equal(pairs("FOO", "bar"))) + Expect(env.String()).To(Equal(`FOO="bar"`)) + +} + +func TestExportfileErrors(t *testing.T) { + RegisterTestingT(t) + + _, err := newEnvFromString("F\nOO='bar'") //keys cannot have embedded newlines + Expect(err).To(HaveOccurred()) +} + +func TestMerge(t *testing.T) { + RegisterTestingT(t) + e, _ := newEnvFromString("FOO='bar'") + e2, _ := newEnvFromString("BAR='baz'") + e.Merge(e2) + Expect(e.Map()).To(Equal(pairs("BAR", "baz", "FOO", "bar"))) + + e3, _ := newEnvFromString("FOO='ba \\nz'") + e.Merge(e3) + Expect(e.Map()).To(Equal(pairs("BAR", "baz", "FOO", "ba \nz"))) +} + +func TestExport(t *testing.T) { + RegisterTestingT(t) + e, _ := newEnvFromString("BAR='BAZ'\nFOO='b'ar '\nBAZ='a\\nb'") + Expect(e.Export(ExportFormatEnvfile)).To(Equal("BAR=\"BAZ\"\nBAZ=\"a\\nb\"\nFOO=\"b'ar \"")) + Expect(e.Export(ExportFormatDockerArgs)).To(Equal("--env=BAR='BAZ' --env=BAZ='a\nb' --env=FOO='b'\\''ar '")) + Expect(e.Export(ExportFormatShell)).To(Equal("BAR='BAZ' BAZ='a\nb' FOO='b'\\''ar '")) + Expect(e.Export(ExportFormatExports)).To(Equal("export BAR='BAZ'\nexport BAZ='a\nb'\nexport FOO='b'\\''ar '")) +} + +func TestGet(t *testing.T) { + RegisterTestingT(t) + e, err := newEnvFromString("BAR='BAZ'\nFOO='ba\\nr '\nGO='1'\nNOGO='0'") + Expect(err).To(Succeed()) + + v, ok := e.Get("GO") + Expect(ok).To(Equal(true)) + Expect(v).To(Equal("1")) + + v = e.GetDefault("GO", "default") + Expect(v).To(Equal("1")) + + v = e.GetDefault("dne", "default") + Expect(v).To(Equal("default")) + + b := e.GetBoolDefault("dne", true) + Expect(b).To(Equal(true)) + + b = e.GetBoolDefault("dne", false) + Expect(b).To(Equal(false)) + + b = e.GetBoolDefault("GO", false) + Expect(b).To(Equal(true)) + + b = e.GetBoolDefault("NOGO", true) + Expect(b).To(Equal(false)) + + b = e.GetBoolDefault("BAR", false) + Expect(b).To(Equal(true)) //anything but "0" is true +} + +func pairs(vars ...string) map[string]string { + res := map[string]string{} + var i = 0 + for i < len(vars)-1 { + key := vars[i] + value := vars[i+1] + res[key] = value + i += 2 + } + return res +} diff --git a/plugins/config/src/commands/commands.go b/plugins/config/src/commands/commands.go index afbe5246f..1b7f44f40 100644 --- a/plugins/config/src/commands/commands.go +++ b/plugins/config/src/commands/commands.go @@ -7,9 +7,6 @@ import ( "strconv" "strings" - "github.com/dokku/dokku/plugins/common" - "github.com/dokku/dokku/plugins/config/src/configenv" - "github.com/dokku/dokku/plugins/config" columnize "github.com/ryanuber/columnize" ) @@ -43,25 +40,9 @@ func main() { global := args.Bool("global", false, "--global: use the global environment") shell := args.Bool("shell", false, "--shell: in a single-line for usage in command-line utilities [deprecated]") export := args.Bool("export", false, "--export: print the env as eval-compatible exports [deprecated]") + merged := args.Bool("merged", false, "--merged: display the app's envionment merged with the global environment") args.Parse(os.Args[2:]) - appName, _ := config.GetCommonArgs(*global, args.Args()) - env := config.GetConfig(appName, false) - - if *shell && *export { - common.LogFail("Only one of --shell and --export can be given") - } - if *shell { - fmt.Print(env.Export(configenv.Shell)) - } else if *export { - fmt.Println(env.Export(configenv.Exports)) - } else { - contextName := "global" - if appName != "" { - contextName = appName - } - common.LogInfo2(contextName + " config vars") - fmt.Println(env.Export(configenv.Pretty)) - } + config.CommandShow(args.Args(), *global, *shell, *export, *merged) case "config:help": case "help": usage() diff --git a/plugins/config/src/configenv/configenv_test.go b/plugins/config/src/configenv/configenv_test.go deleted file mode 100644 index 9d423e95e..000000000 --- a/plugins/config/src/configenv/configenv_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package configenv - -import ( - "testing" - - . "github.com/onsi/gomega" -) - -func TestExportfileRoundtrip(t *testing.T) { - RegisterTestingT(t) - env, err := NewFromString("HI='ho'\nFoo='Bar'\n\nBaz='BOFF'") - Expect(err).NotTo(HaveOccurred()) - Expect(env.Map()).To(Equal(pairs("Baz", "BOFF", "Foo", "Bar", "HI", "ho"))) - Expect(env.String()).To(Equal("Baz=\"BOFF\"\nFoo=\"Bar\"\nHI=\"ho\"")) - - env, err = NewFromString(`export HI="h\no"`) - Expect(err).NotTo(HaveOccurred()) - Expect(env.Map()).To(Equal(pairs("HI", "h\no"))) - Expect(env.String()).To(Equal(`HI="h\no"`)) - - env, err = NewFromString("HI='ho'\nFOO=\"'\\nBAR=''\"") - Expect(err).NotTo(HaveOccurred()) - Expect(env.Map()).To(Equal(pairs("HI", "ho", "FOO", "'\nBAR=''"))) - Expect(env.String()).To(Equal("FOO=\"'\\nBAR=''\"\nHI=\"ho\"")) - - env, err = NewFromString("FOO='bar' ") - Expect(err).NotTo(HaveOccurred()) - Expect(env.Map()).To(Equal(pairs("FOO", "bar"))) - Expect(env.String()).To(Equal(`FOO="bar"`)) - -} - -func TestExportfileErrors(t *testing.T) { - RegisterTestingT(t) - - _, err := NewFromString("F\nOO='bar'") //keys cannot have embedded newlines - Expect(err).To(HaveOccurred()) -} - -func TestMerge(t *testing.T) { - RegisterTestingT(t) - e, _ := NewFromString("FOO='bar'") - e2, _ := NewFromString("BAR='baz'") - e.Merge(e2) - Expect(e.Map()).To(Equal(pairs("BAR", "baz", "FOO", "bar"))) -} - -func TestArrayExport(t *testing.T) { - RegisterTestingT(t) - e, _ := NewFromString("BAR='BAZ'\nFOO='b'ar '") - Expect(e.EnvfileString()).To(Equal("BAR=\"BAZ\"\nFOO=\"b'ar \"")) - Expect(e.DockerArgsString()).To(Equal("--env=BAR='BAZ' --env=FOO='b'\\''ar '")) - Expect(e.ShellString()).To(Equal("BAR='BAZ' FOO='b'\\''ar '")) -} - -func pairs(vars ...string) map[string]string { - res := map[string]string{} - var i = 0 - for i < len(vars)-1 { - key := vars[i] - value := vars[i+1] - res[key] = value - i += 2 - } - return res -} diff --git a/plugins/config/src/subcommands/bundle/bundle.go b/plugins/config/src/subcommands/bundle/bundle.go index fc3b11407..3bb03a16d 100644 --- a/plugins/config/src/subcommands/bundle/bundle.go +++ b/plugins/config/src/subcommands/bundle/bundle.go @@ -2,10 +2,8 @@ package main import ( "flag" - "fmt" "os" - "github.com/dokku/dokku/plugins/common" "github.com/dokku/dokku/plugins/config" ) @@ -14,11 +12,5 @@ func main() { global := args.Bool("global", false, "--global: use the global environment") merged := args.Bool("merged", false, "--merged: merge app environment and global environment") args.Parse(os.Args[2:]) - - appName, trailingArgs := config.GetCommonArgs(*global, args.Args()) - if len(trailingArgs) > 0 { - common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs)) - } - config := config.GetConfig(appName, *merged) - config.ExportBundle(os.Stdout) + config.CommandBundle(args.Args(), *global, *merged) } diff --git a/plugins/config/src/subcommands/export/export.go b/plugins/config/src/subcommands/export/export.go index 0c9027fbc..86fd8df8f 100644 --- a/plugins/config/src/subcommands/export/export.go +++ b/plugins/config/src/subcommands/export/export.go @@ -2,12 +2,9 @@ package main import ( "flag" - "fmt" "os" - common "github.com/dokku/dokku/plugins/common" "github.com/dokku/dokku/plugins/config" - "github.com/dokku/dokku/plugins/config/src/configenv" ) // print the environment to stdout @@ -19,28 +16,5 @@ func main() { merged := args.Bool("merged", false, "--merged: merge app environment and global environment") format := args.String("format", "exports", "--format: [ exports | envfile | docker-args | shell ] which format to export as)") args.Parse(os.Args[2:]) - - appName, trailingArgs := config.GetCommonArgs(*global, args.Args()) - if len(trailingArgs) > 0 { - common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs)) - } - - env := config.GetConfig(appName, *merged) - exportType := configenv.Exports - suffix := "\n" - switch *format { - case "exports": - exportType = configenv.Exports - case "envfile": - exportType = configenv.Envfile - case "docker-args": - exportType = configenv.DockerArgs - case "shell": - exportType = configenv.Shell - suffix = " " - default: - common.LogFail(fmt.Sprintf("Unknown export format: %v", *format)) - } - exported := env.Export(exportType) - fmt.Print(exported + suffix) + config.CommandExport(args.Args(), *global, *merged, *format) } diff --git a/plugins/config/src/subcommands/get/get.go b/plugins/config/src/subcommands/get/get.go index 6419bb8c7..52840b3fa 100644 --- a/plugins/config/src/subcommands/get/get.go +++ b/plugins/config/src/subcommands/get/get.go @@ -2,12 +2,9 @@ package main import ( "flag" - "fmt" "os" - common "github.com/dokku/dokku/plugins/common" "github.com/dokku/dokku/plugins/config" - "github.com/dokku/dokku/plugins/config/src/configenv" ) // get the given entries from the specified environment @@ -16,22 +13,5 @@ func main() { global := args.Bool("global", false, "--global: use the global environment") quoted := args.Bool("quoted", false, "--quoted: get the value quoted") args.Parse(os.Args[2:]) - - appName, keys := config.GetCommonArgs(*global, args.Args()) - if len(keys) > 1 { - common.LogFail(fmt.Sprintf("Unexpected argument(s): %v", keys[1:])) - } - if len(keys) == 0 { - common.LogFail("Expected: key") - } - if !config.HasKey(appName, keys[0]) { - os.Exit(1) - } - - value := config.GetWithDefault(appName, keys[0], "") - if *quoted { - fmt.Printf("'%s'", configenv.SingleQuoteEscape(value)) - } else { - fmt.Printf("%s", value) - } + config.CommandGet(args.Args(), *global, *quoted) } diff --git a/plugins/config/src/subcommands/keys/keys.go b/plugins/config/src/subcommands/keys/keys.go index 0694e4dc3..64de1c02b 100644 --- a/plugins/config/src/subcommands/keys/keys.go +++ b/plugins/config/src/subcommands/keys/keys.go @@ -2,10 +2,8 @@ package main import ( "flag" - "fmt" "os" - "github.com/dokku/dokku/plugins/common" "github.com/dokku/dokku/plugins/config" ) @@ -14,13 +12,5 @@ func main() { global := args.Bool("global", false, "--global: use the global environment") merged := args.Bool("merged", false, "--merged: merge app environment and global environment") args.Parse(os.Args[2:]) - - appName, trailingArgs := config.GetCommonArgs(*global, args.Args()) - if len(trailingArgs) > 0 { - common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs)) - } - config := config.GetConfig(appName, *merged) - for _, k := range config.Keys() { - fmt.Println(k) - } + config.CommandKeys(args.Args(), *global, *merged) } diff --git a/plugins/config/src/subcommands/set/set.go b/plugins/config/src/subcommands/set/set.go index 276b8dc79..2bee318f7 100644 --- a/plugins/config/src/subcommands/set/set.go +++ b/plugins/config/src/subcommands/set/set.go @@ -1,14 +1,10 @@ package main import ( - "encoding/base64" "flag" - "fmt" "os" - "strings" - common "github.com/dokku/dokku/plugins/common" - config "github.com/dokku/dokku/plugins/config" + "github.com/dokku/dokku/plugins/config" ) // set the given entries from the specified environment @@ -18,23 +14,5 @@ func main() { encoded := args.Bool("encoded", false, "--encoded: interpret VALUEs as base64") noRestart := args.Bool("no-restart", false, "--no-restart: no restart") args.Parse(os.Args[2:]) - appName, pairs := config.GetCommonArgs(*global, args.Args()) - - updated := make(map[string]string) - for _, e := range pairs { - parts := strings.SplitN(e, "=", 2) - if len(parts) == 1 { - common.LogFail("Invalid env pair: " + e) - } - key, value := parts[0], parts[1] - if *encoded { - decoded, err := base64.StdEncoding.DecodeString(value) - if err != nil { - common.LogFail(fmt.Sprintf("%s for key '%s'", err.Error(), key)) - } - value = string(decoded) - } - updated[key] = value - } - config.SetMany(appName, updated, !*noRestart) + config.CommandSet(args.Args(), *global, *noRestart, *encoded) } diff --git a/plugins/config/src/subcommands/unset/unset.go b/plugins/config/src/subcommands/unset/unset.go index 9d388a762..c6c4ee814 100644 --- a/plugins/config/src/subcommands/unset/unset.go +++ b/plugins/config/src/subcommands/unset/unset.go @@ -12,8 +12,6 @@ func main() { args := flag.NewFlagSet("config:unset", flag.ExitOnError) global := args.Bool("global", false, "--global: use the global environment") noRestart := args.Bool("no-restart", false, "--no-restart: no restart") - args.Parse(os.Args[2:]) - appName, keys := config.GetCommonArgs(*global, args.Args()) - config.UnsetMany(appName, keys, !*noRestart) + config.CommandUnset(args.Args(), *global, *noRestart) } diff --git a/plugins/config/subcommands.go b/plugins/config/subcommands.go new file mode 100644 index 000000000..a5b564933 --- /dev/null +++ b/plugins/config/subcommands.go @@ -0,0 +1,167 @@ +package config + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +//CommandShow implementes config:show +func CommandShow(args []string, global bool, shell bool, export bool, merged bool) { + appName, _ := getCommonArgs(global, args) + env := getEnvironment(appName, merged) + if shell && export { + common.LogFail("Only one of --shell and --export can be given") + } + if shell { + fmt.Print(env.Export(ExportFormatShell)) + } else if export { + fmt.Println(env.Export(ExportFormatExports)) + } else { + contextName := "global" + if appName != "" { + contextName = appName + } + common.LogInfo2(contextName + " env vars") + fmt.Println(env.Export(ExportFormatPretty)) + } +} + +//CommandGet implements config:get +func CommandGet(args []string, global bool, quoted bool) { + appName, keys := getCommonArgs(global, args) + if len(keys) > 1 { + common.LogFail(fmt.Sprintf("Unexpected argument(s): %v", keys[1:])) + } + if len(keys) == 0 { + common.LogFail("Expected: key") + } + if value, ok := Get(appName, keys[0]); !ok { + os.Exit(1) + } else { + if quoted { + fmt.Printf("'%s'", singleQuoteEscape(value)) + } else { + fmt.Printf("%s", value) + } + } +} + +//CommandUnset implements config:unset +func CommandUnset(args []string, global bool, noRestart bool) { + appName, keys := getCommonArgs(global, args) + err := UnsetMany(appName, keys, !noRestart) + if err != nil { + common.LogFail(err.Error()) + } +} + +//CommandSet implements config:set +func CommandSet(args []string, global bool, noRestart bool, encoded bool) { + appName, pairs := getCommonArgs(global, args) + updated := make(map[string]string) + for _, e := range pairs { + parts := strings.SplitN(e, "=", 2) + if len(parts) == 1 { + common.LogFail("Invalid env pair: " + e) + } + key, value := parts[0], parts[1] + if encoded { + decoded, err := base64.StdEncoding.DecodeString(value) + if err != nil { + common.LogFail(fmt.Sprintf("%s for key '%s'", err.Error(), key)) + } + value = string(decoded) + } + updated[key] = value + } + err := SetMany(appName, updated, !noRestart) + if err != nil { + common.LogFail(err.Error()) + } +} + +//CommandKeys implements config:keys +func CommandKeys(args []string, global bool, merged bool) { + appName, trailingArgs := getCommonArgs(global, args) + if len(trailingArgs) > 0 { + common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs)) + } + env := getEnvironment(appName, merged) + for _, k := range env.Keys() { + fmt.Println(k) + } +} + +//CommandExport implements config:export +func CommandExport(args []string, global bool, merged bool, format string) { + appName, trailingArgs := getCommonArgs(global, args) + if len(trailingArgs) > 0 { + common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs)) + } + env := getEnvironment(appName, merged) + exportType := ExportFormatExports + suffix := "\n" + switch format { + case "exports": + exportType = ExportFormatExports + case "envfile": + exportType = ExportFormatEnvfile + case "docker-args": + exportType = ExportFormatDockerArgs + case "shell": + exportType = ExportFormatShell + suffix = " " + case "pretty": + exportType = ExportFormatPretty + default: + common.LogFail(fmt.Sprintf("Unknown export format: %v", format)) + } + exported := env.Export(exportType) + fmt.Print(exported + suffix) +} + +//CommandBundle implements config:bundle +func CommandBundle(args []string, global bool, merged bool) { + appName, trailingArgs := getCommonArgs(global, args) + if len(trailingArgs) > 0 { + common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs)) + } + env := getEnvironment(appName, merged) + env.ExportBundle(os.Stdout) +} + +//getEnvironment for the given app (global config if appName is empty). Merge with global environment if merged is true. +func getEnvironment(appName string, merged bool) (env *Env) { + env, err := loadAppOrGlobalEnv(appName) + if err != nil { + common.LogFail(err.Error()) + } + if appName != "" && merged { + env, err = LoadMergedAppEnv(appName) + if err != nil { + common.LogFail(err.Error()) + } + } + return env +} + +//getCommonArgs extracts common positional args (appName and keys) +func getCommonArgs(global bool, args []string) (appName string, keys []string) { + nextArg := 0 + if !global { + if len(args) > 0 { + appName = args[0] + } + if appName == "" { + common.LogFail("Please specify an app or --global") + } else { + nextArg++ + } + } + keys = args[nextArg:] + return appName, keys +}