2017-10-15 20:10:46 -04:00
|
|
|
package config
|
2017-01-16 23:13:32 -05:00
|
|
|
|
|
|
|
|
import (
|
2019-04-21 17:00:04 -04:00
|
|
|
"archive/tar"
|
|
|
|
|
"encoding/json"
|
2017-01-16 23:13:32 -05:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2017-04-22 15:58:03 -04:00
|
|
|
"io"
|
2019-04-21 17:00:04 -04:00
|
|
|
"os"
|
2017-01-16 23:13:32 -05:00
|
|
|
"path/filepath"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2017-10-16 02:09:26 -04:00
|
|
|
"github.com/dokku/dokku/plugins/common"
|
|
|
|
|
"github.com/joho/godotenv"
|
2017-10-12 23:42:14 -04:00
|
|
|
"github.com/ryanuber/columnize"
|
2017-01-16 23:13:32 -05:00
|
|
|
)
|
|
|
|
|
|
2017-07-16 19:50:50 -04:00
|
|
|
//ExportFormat types of possible exports
|
2017-05-20 14:38:27 -04:00
|
|
|
type ExportFormat int
|
|
|
|
|
|
|
|
|
|
const (
|
2017-10-15 20:10:46 -04:00
|
|
|
//ExportFormatExports format: Sourceable exports
|
|
|
|
|
ExportFormatExports ExportFormat = iota
|
|
|
|
|
//ExportFormatEnvfile format: dotenv file
|
|
|
|
|
ExportFormatEnvfile
|
2021-02-11 03:50:51 -05:00
|
|
|
//ExportFormatDockerArgs format: --env KEY=VALUE args for docker
|
2017-10-15 20:10:46 -04:00
|
|
|
ExportFormatDockerArgs
|
2022-01-15 08:34:02 -05:00
|
|
|
//ExportFormatDockerArgsKeys format: --env=KEY args for docker
|
2021-02-11 03:50:51 -05:00
|
|
|
ExportFormatDockerArgsKeys
|
2017-10-15 20:10:46 -04:00
|
|
|
//ExportFormatShell format: env arguments for shell
|
|
|
|
|
ExportFormatShell
|
|
|
|
|
//ExportFormatPretty format: pretty-printed in columns
|
|
|
|
|
ExportFormatPretty
|
2019-04-21 17:00:04 -04:00
|
|
|
//ExportFormatJSON format: json key/value output
|
|
|
|
|
ExportFormatJSON
|
|
|
|
|
//ExportFormatJSONList format: json output as a list of objects
|
|
|
|
|
ExportFormatJSONList
|
2022-01-15 08:34:02 -05:00
|
|
|
//ExportFormatPackArgKeys format: --env KEY args for pack
|
|
|
|
|
ExportFormatPackArgKeys
|
2017-05-20 14:38:27 -04:00
|
|
|
)
|
|
|
|
|
|
2017-01-16 23:13:32 -05:00
|
|
|
//Env is a representation for global or app environment
|
|
|
|
|
type Env struct {
|
2017-05-20 14:38:27 -04:00
|
|
|
name string
|
|
|
|
|
filename string
|
|
|
|
|
env map[string]string
|
2017-01-16 23:13:32 -05:00
|
|
|
}
|
|
|
|
|
|
2017-10-15 20:10:46 -04:00
|
|
|
//newEnvFromString creates an env from the given ENVFILE contents representation
|
|
|
|
|
func newEnvFromString(rep string) (env *Env, err error) {
|
2017-10-12 23:48:16 -04:00
|
|
|
envMap, err := godotenv.Unmarshal(rep)
|
|
|
|
|
env = &Env{
|
2017-10-24 09:11:42 -04:00
|
|
|
name: "<unknown>",
|
|
|
|
|
filename: "",
|
|
|
|
|
env: envMap,
|
2017-10-12 23:48:16 -04:00
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-15 20:10:46 -04:00
|
|
|
//LoadAppEnv loads an environment for the given app
|
|
|
|
|
func LoadAppEnv(appName string) (env *Env, err error) {
|
2020-12-21 18:35:09 -05:00
|
|
|
err = common.VerifyAppName(appName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-10-12 23:48:16 -04:00
|
|
|
appfile, err := getAppFile(appName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
return loadFromFile(appName, appfile)
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-15 20:10:46 -04:00
|
|
|
//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 {
|
2021-02-03 15:20:46 -05:00
|
|
|
common.LogFailWithError(err)
|
2017-10-15 20:10:46 -04:00
|
|
|
}
|
|
|
|
|
global.Merge(env)
|
|
|
|
|
global.filename = ""
|
|
|
|
|
global.name = env.name
|
|
|
|
|
return global, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//LoadGlobalEnv loads the global environment
|
|
|
|
|
func LoadGlobalEnv() (*Env, error) {
|
2017-11-03 13:41:49 +09:00
|
|
|
return loadFromFile("<global>", getGlobalFile())
|
2017-10-12 23:48:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Get an environment variable
|
|
|
|
|
func (e *Env) Get(key string) (value string, ok bool) {
|
|
|
|
|
value, ok = e.env[key]
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-03 12:42:00 +09:00
|
|
|
//GetDefault an environment variable or a default if it doesn't exist
|
2017-10-12 23:48:16 -04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-03 12:42:00 +09:00
|
|
|
//Len returns the number of items in this environment
|
2017-10-12 23:48:16 -04:00
|
|
|
func (e *Env) Len() int {
|
|
|
|
|
return len(e.env)
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-03 12:42:00 +09:00
|
|
|
//Map returns the Env as a map
|
2017-10-12 23:48:16 -04:00
|
|
|
func (e *Env) Map() map[string]string {
|
|
|
|
|
return e.env
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-16 23:13:32 -05:00
|
|
|
func (e *Env) String() string {
|
|
|
|
|
return e.EnvfileString()
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-03 12:42:00 +09:00
|
|
|
//Merge merges the given environment on top of the receiver
|
2017-10-12 23:48:16 -04:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-16 19:50:50 -04:00
|
|
|
//Export the Env in the given format
|
2017-05-20 14:38:27 -04:00
|
|
|
func (e *Env) Export(format ExportFormat) string {
|
|
|
|
|
switch format {
|
2017-10-15 20:10:46 -04:00
|
|
|
case ExportFormatExports:
|
2017-05-20 14:38:27 -04:00
|
|
|
return e.ExportfileString()
|
2017-10-15 20:10:46 -04:00
|
|
|
case ExportFormatEnvfile:
|
2017-05-20 14:38:27 -04:00
|
|
|
return e.EnvfileString()
|
2017-10-15 20:10:46 -04:00
|
|
|
case ExportFormatDockerArgs:
|
2017-05-20 14:38:27 -04:00
|
|
|
return e.DockerArgsString()
|
2021-02-11 03:50:51 -05:00
|
|
|
case ExportFormatDockerArgsKeys:
|
|
|
|
|
return e.DockerArgsKeysString()
|
2017-10-15 20:10:46 -04:00
|
|
|
case ExportFormatShell:
|
2017-07-16 19:50:50 -04:00
|
|
|
return e.ShellString()
|
2017-10-15 20:10:46 -04:00
|
|
|
case ExportFormatPretty:
|
|
|
|
|
return prettyPrintEnvEntries("", e.Map())
|
2019-04-21 17:00:04 -04:00
|
|
|
case ExportFormatJSON:
|
|
|
|
|
return e.JSONString()
|
|
|
|
|
case ExportFormatJSONList:
|
|
|
|
|
return e.JSONListString()
|
2022-01-15 08:34:02 -05:00
|
|
|
case ExportFormatPackArgKeys:
|
|
|
|
|
return e.PackArgKeysAsString()
|
2017-05-20 14:38:27 -04:00
|
|
|
default:
|
|
|
|
|
common.LogFail(fmt.Sprintf("Unknown export format: %v", format))
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//EnvfileString returns the contents of this Env in dotenv format
|
2017-01-16 23:13:32 -05:00
|
|
|
func (e *Env) EnvfileString() string {
|
2017-09-23 16:27:25 -04:00
|
|
|
rep, _ := godotenv.Marshal(e.Map())
|
2017-05-20 14:38:27 -04:00
|
|
|
return rep
|
2017-01-16 23:13:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//ExportfileString returns the contents of this Env as bash exports
|
|
|
|
|
func (e *Env) ExportfileString() string {
|
2017-10-15 20:10:46 -04:00
|
|
|
return e.stringWithPrefixAndSeparator("export ", "\n")
|
2017-05-20 14:38:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//DockerArgsString gets the contents of this Env in the form -env=KEY=VALUE --env...
|
|
|
|
|
func (e *Env) DockerArgsString() string {
|
2017-10-15 20:10:46 -04:00
|
|
|
return e.stringWithPrefixAndSeparator("--env=", " ")
|
2017-01-16 23:13:32 -05:00
|
|
|
}
|
|
|
|
|
|
2021-02-11 04:02:06 -05:00
|
|
|
//DockerArgsKeysString gets the contents of this Env in the form -env=KEY --env...
|
2021-02-11 03:50:51 -05:00
|
|
|
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, " ")
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-21 17:00:04 -04:00
|
|
|
//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)
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-15 08:34:02 -05:00
|
|
|
//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, " ")
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-16 19:50:50 -04:00
|
|
|
//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 {
|
2017-10-15 20:10:46 -04:00
|
|
|
return e.stringWithPrefixAndSeparator("", " ")
|
2017-07-16 19:50:50 -04:00
|
|
|
}
|
|
|
|
|
|
2017-11-03 12:42:00 +09:00
|
|
|
//ExportBundle writes a tarfile of the environment to the given io.Writer.
|
2017-04-22 15:58:03 -04:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-15 20:10:46 -04:00
|
|
|
//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
|
2017-11-03 12:42:00 +09:00
|
|
|
func singleQuoteEscape(value string) string { // so that 'esc'aped' -> 'esc'\''aped'
|
2017-10-12 23:48:16 -04:00
|
|
|
return strings.Replace(value, "'", "'\\''", -1)
|
2017-01-16 23:13:32 -05:00
|
|
|
}
|
|
|
|
|
|
2017-10-15 20:10:46 -04:00
|
|
|
//prettyPrintEnvEntries in columns
|
2017-11-03 12:46:16 +09:00
|
|
|
func prettyPrintEnvEntries(prefix string, entries map[string]string) string {
|
2017-10-12 23:48:16 -04:00
|
|
|
colConfig := columnize.DefaultConfig()
|
|
|
|
|
colConfig.Prefix = prefix
|
|
|
|
|
colConfig.Delim = "\x00"
|
2018-10-27 15:35:40 -04:00
|
|
|
|
|
|
|
|
//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]))
|
2017-05-20 14:38:27 -04:00
|
|
|
}
|
2017-10-12 23:48:16 -04:00
|
|
|
return columnize.Format(lines, colConfig)
|
2017-05-20 14:38:27 -04:00
|
|
|
}
|
2017-10-12 23:41:42 -04:00
|
|
|
|
2017-05-21 16:13:53 -04:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-28 16:57:55 -05:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-21 16:13:53 -04:00
|
|
|
env = &Env{
|
2017-10-24 09:11:42 -04:00
|
|
|
name: name,
|
|
|
|
|
filename: filename,
|
|
|
|
|
env: envMap,
|
2017-05-20 14:38:27 -04:00
|
|
|
}
|
2017-05-21 16:13:53 -04:00
|
|
|
return
|
2017-01-16 23:13:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getAppFile(appName string) (string, error) {
|
|
|
|
|
err := common.VerifyAppName(appName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "ENV"), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getGlobalFile() string {
|
|
|
|
|
return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), "ENV")
|
|
|
|
|
}
|