package config import ( "archive/tar" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "sort" "strings" "github.com/dokku/dokku/plugins/common" "github.com/joho/godotenv" "github.com/ryanuber/columnize" ) // ExportFormat types of possible exports type ExportFormat int const ( //ExportFormatExports format: Sourceable exports ExportFormatExports ExportFormat = iota //ExportFormatEnvfile format: dotenv file ExportFormatEnvfile //ExportFormatDockerArgs format: --env KEY=VALUE args for docker ExportFormatDockerArgs //ExportFormatDockerArgsKeys format: --env=KEY args for docker ExportFormatDockerArgsKeys //ExportFormatShell format: env arguments for shell ExportFormatShell //ExportFormatPretty format: pretty-printed in columns ExportFormatPretty //ExportFormatJSON format: json key/value output ExportFormatJSON //ExportFormatJSONList format: json output as a list of objects ExportFormatJSONList //ExportFormatPackArgKeys format: --env KEY args for pack ExportFormatPackArgKeys ) // Env is a representation for global or app environment type Env struct { name string filename string env map[string]string } // 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{ name: "", filename: "", env: envMap, } return } // LoadAppEnv loads an environment for the given app func LoadAppEnv(appName string) (env *Env, err error) { appfile, err := getAppFile(appName) if err != nil { return } return loadFromFile(appName, appfile) } // 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.LogFailWithError(err) } global.Merge(env) global.filename = "" global.name = env.name return global, err } // LoadGlobalEnv loads the global environment func LoadGlobalEnv() (*Env, error) { return loadFromFile("", getGlobalFile()) } // Filename returns the full path on disk to the file holding the env vars func (e *Env) Filename() string { return e.filename } // Get an environment variable func (e *Env) Get(key string) (value string, ok bool) { value, ok = e.env[key] return } // GetDefault an environment variable or a default if it doesn't exist func (e *Env) GetDefault(key string, defaultValue string) string { v, ok := e.env[key] if !ok { return defaultValue } return v } // GetBoolDefault gets the bool value of the given key with the given default // right now that is evaluated as `value != "0"` func (e *Env) GetBoolDefault(key string, defaultValue bool) bool { v, ok := e.Get(key) if !ok { return defaultValue } return v != "0" } // Set an environment variable func (e *Env) Set(key string, value string) { e.env[key] = value } // Unset an environment variable func (e *Env) Unset(key string) { delete(e.env, key) } // Keys gets the keys in this environment func (e *Env) Keys() (keys []string) { keys = make([]string, 0, len(e.env)) for k := range e.env { keys = append(keys, k) } sort.Strings(keys) return } // Len returns the number of items in this environment func (e *Env) Len() int { return len(e.env) } // Map returns the Env as a map func (e *Env) Map() map[string]string { return e.env } func (e *Env) String() string { return e.EnvfileString() } // Merge merges the given environment on top of the receiver func (e *Env) Merge(other *Env) { for _, k := range other.Keys() { e.Set(k, other.GetDefault(k, "")) } } // Write an Env back to the file it was read from as an exportfile func (e *Env) Write() error { if e.filename == "" { return errors.New("this Env was created unbound to a file") } return godotenv.Write(e.Map(), e.filename) } // Export the Env in the given format func (e *Env) Export(format ExportFormat) string { switch format { case ExportFormatExports: return e.ExportfileString() case ExportFormatEnvfile: return e.EnvfileString() case ExportFormatDockerArgs: return e.DockerArgsString() case ExportFormatDockerArgsKeys: return e.DockerArgsKeysString() case ExportFormatShell: return e.ShellString() case ExportFormatPretty: return prettyPrintEnvEntries("", e.Map()) case ExportFormatJSON: return e.JSONString() case ExportFormatJSONList: return e.JSONListString() case ExportFormatPackArgKeys: return e.PackArgKeysAsString() default: common.LogFail(fmt.Sprintf("Unknown export format: %v", format)) return "" } } // EnvfileString returns the contents of this Env in dotenv format func (e *Env) EnvfileString() string { rep, _ := godotenv.Marshal(e.Map()) return rep } // ExportfileString returns the contents of this Env as bash exports func (e *Env) ExportfileString() string { 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=", " ") } // DockerArgsKeysString gets the contents of this Env in the form -env=KEY --env... func (e *Env) DockerArgsKeysString() string { keys := e.Keys() entries := make([]string, len(keys)) for i, k := range keys { entries[i] = fmt.Sprintf("%s%s", "--env=", k) } return strings.Join(entries, " ") } // JSONString returns the contents of this Env as a key/value json object func (e *Env) JSONString() string { data, err := json.Marshal(e.Map()) if err != nil { return "{}" } return string(data) } // JSONListString returns the contents of this Env as a json list of objects containing the name and the value of the env var func (e *Env) JSONListString() string { var list []map[string]string for _, key := range e.Keys() { value, _ := e.Get(key) list = append(list, map[string]string{ "name": key, "value": value, }) } data, err := json.Marshal(list) if err != nil { return "[]" } return string(data) } // PackArgKeysAsString gets the contents of this Env in the form -env KEY --env... func (e *Env) PackArgKeysAsString() string { keys := e.Keys() entries := make([]string, len(keys)) for i, k := range keys { entries[i] = fmt.Sprintf("%s%s", "--env ", k) } return strings.Join(entries, " ") } // 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("", " ") } // ExportBundle writes a tarfile of the environment to the given io.Writer. // for every environment variable there is a file with the variable's key // with its content set to the variable's value func (e *Env) ExportBundle(dest io.Writer) error { tarfile := tar.NewWriter(dest) defer tarfile.Close() for _, k := range e.Keys() { val, _ := e.Get(k) valbin := []byte(val) header := &tar.Header{ Name: k, Mode: 0600, Size: int64(len(valbin)), } tarfile.WriteHeader(header) tarfile.Write(valbin) } return nil } // 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'aped' -> 'esc'\''aped' return strings.Replace(value, "'", "'\\''", -1) } // prettyPrintEnvEntries in columns func prettyPrintEnvEntries(prefix string, entries map[string]string) string { colConfig := columnize.DefaultConfig() colConfig.Prefix = prefix colConfig.Delim = "\x00" //some keys may be prefixes of each other so we need to sort them rather than the resulting lines keys := make([]string, 0, len(entries)) for k := range entries { keys = append(keys, k) } sort.Strings(keys) lines := make([]string, 0, len(keys)) for _, k := range keys { lines = append(lines, fmt.Sprintf("%s:\x00%s", k, entries[k])) } return columnize.Format(lines, colConfig) } func loadFromFile(name string, filename string) (env *Env, err error) { envMap := make(map[string]string) if _, err := os.Stat(filename); err == nil { envMap, err = godotenv.Read(filename) } dirty := false for k := range envMap { if err := validateKey(k); err != nil { common.LogInfo1(fmt.Sprintf("Deleting invalid key %s from config for %s", k, name)) delete(envMap, k) dirty = true } } if dirty { if err := godotenv.Write(envMap, filename); err != nil { common.LogFail(fmt.Sprintf("Error writing back config for %s after removing invalid keys", name)) } } env = &Env{ name: name, filename: filename, env: envMap, } return } func getAppFile(appName string) (string, error) { return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "ENV"), nil } func getGlobalFile() string { return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), "ENV") }