Merge pull request #5183 from dokku/user-access-to-app

Filter apps when verifying app names
This commit is contained in:
Jose Diaz-Gonzalez
2022-05-29 12:50:44 -04:00
committed by GitHub
13 changed files with 331 additions and 142 deletions

View File

@@ -581,6 +581,11 @@ func VerifyAppName(appName string) error {
return &AppDoesNotExist{appName}
}
apps, _ := filterApps([]string{appName})
if len(apps) != 1 {
return &AppDoesNotExist{appName}
}
return nil
}

View File

@@ -20,6 +20,10 @@ var (
testEnvLine2 = "export testKey=TESTING"
)
func setupTests() (err error) {
return os.Setenv("PLUGIN_ENABLED_PATH", "/var/lib/dokku/plugins/enabled")
}
func setupTestApp() (err error) {
Expect(os.MkdirAll(testAppDir, 0644)).To(Succeed())
b := []byte(testEnvLine + "\n")
@@ -48,32 +52,37 @@ func teardownTestApp2() {
func TestCommonGetEnv(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(MustGetEnv("DOKKU_ROOT")).To(Equal("/home/dokku"))
}
func TestCommonGetAppImageRepo(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(GetAppImageRepo("testapp")).To(Equal("dokku/testapp"))
}
func TestCommonVerifyImageInvalid(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(VerifyImage("testapp")).To(Equal(false))
}
func TestCommonVerifyAppNameInvalid(t *testing.T) {
RegisterTestingT(t)
err := VerifyAppName("1994testApp")
Expect(err).To(HaveOccurred())
Expect(setupTests()).To(Succeed())
Expect(VerifyAppName("1994testApp")).To(HaveOccurred())
}
func TestCommonVerifyAppName(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
Expect(VerifyAppName(testAppName)).To(Succeed())
teardownTestApp()
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp2()).To(Succeed())
Expect(VerifyAppName(testAppName2)).To(Succeed())
teardownTestApp2()
@@ -81,13 +90,14 @@ func TestCommonVerifyAppName(t *testing.T) {
func TestCommonDokkuAppsError(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
_, err := DokkuApps()
Expect(err).To(HaveOccurred())
}
func TestCommonDokkuApps(t *testing.T) {
RegisterTestingT(t)
os.Setenv("PLUGIN_ENABLED_PATH", "/var/lib/dokku/plugins/enabled")
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
apps, err := DokkuApps()
Expect(err).NotTo(HaveOccurred())
@@ -98,6 +108,7 @@ func TestCommonDokkuApps(t *testing.T) {
func TestCommonStripInlineComments(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
text := StripInlineComments(strings.Join([]string{testEnvLine, "# testing comment"}, " "))
Expect(text).To(Equal(testEnvLine))
}

View File

@@ -250,23 +250,12 @@ is_valid_app_name_old() {
verify_app_name() {
declare desc="verify app name format and app existence"
declare APP="$1"
local VALID_NEW=false
local VALID_OLD=false
if fn-is-valid-app-name "$APP" 2>/dev/null; then
VALID_NEW=true
if "$PLUGIN_CORE_AVAILABLE_PATH/common/common" --quiet verify-app-name "$APP"; then
return 0
fi
if fn-is-valid-app-name-old "$APP" 2>/dev/null; then
VALID_OLD=true
fi
if [[ "$VALID_NEW" == "false" ]] && [[ "$VALID_OLD" == "false" ]]; then
dokku_log_fail "App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, colons, or underscores"
fi
[[ ! -d "$DOKKU_ROOT/$APP" ]] && DOKKU_FAIL_EXIT_CODE=20 dokku_log_fail "App $APP does not exist"
return 0
DOKKU_FAIL_EXIT_CODE=20 dokku_log_fail "App $APP does not exist"
}
verify_image() {

View File

@@ -61,6 +61,19 @@ func LogFailWithError(err error) {
os.Exit(1)
}
// LogFailWithErrorQuiet is the failure log formatter (with quiet option)
// prints text to stderr and exits with the specified exit code
// The error message is not printed if DOKKU_QUIET_OUTPUT has any value
func LogFailWithErrorQuiet(err error) {
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
fmt.Fprintln(os.Stderr, fmt.Sprintf(" ! %s", err.Error()))
}
if errExit, ok := err.(ErrWithExitCode); ok {
os.Exit(errExit.ExitCode())
}
os.Exit(1)
}
// LogFailQuiet is the failure log formatter (with quiet option)
// prints text to stderr and exits with status 1
func LogFailQuiet(text string) {

View File

@@ -38,11 +38,14 @@ func main() {
appName = "--global"
}
fmt.Print(common.GetAppScheduler(appName))
case "verify-app-name":
appName := flag.Arg(1)
err = common.VerifyAppName(appName)
default:
err = fmt.Errorf("Invalid common command call: %v", cmd)
}
if err != nil {
common.LogFailQuiet(err.Error())
common.LogFailWithErrorQuiet(err)
}
}

View File

@@ -1,6 +1,12 @@
SUBCOMMANDS = subcommands/bundle subcommands/clear subcommands/export subcommands/get subcommands/keys subcommands/show subcommands/set subcommands/unset
TRIGGERS = triggers/config-export triggers/config-get triggers/config-get-global
BUILD = commands subcommands triggers
BUILD = commands config_sub subcommands triggers
PLUGIN_NAME = config
clean-config_sub:
rm -rf config_sub
config_sub: clean-config_sub **/**/config_sub.go
go build -ldflags="-s -w" $(GO_ARGS) -o config_sub src/config_sub/config_sub.go
include ../../common.mk

View File

@@ -18,6 +18,10 @@ var (
globalConfigFile = strings.Join([]string{dokkuRoot, "ENV"}, "/")
)
func setupTests() (err error) {
return os.Setenv("PLUGIN_ENABLED_PATH", "/var/lib/dokku/plugins/enabled")
}
func setupTestApp() (err error) {
Expect(os.MkdirAll(testAppDir, 0766)).To(Succeed())
b := []byte("export testKey=TESTING\n")
@@ -38,6 +42,7 @@ func teardownTestApp() {
func TestConfigGetWithDefault(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
Expect(GetWithDefault(testAppName, "unknownKey", "UNKNOWN")).To(Equal("UNKNOWN"))
Expect(GetWithDefault(testAppName, "testKey", "testKey")).To(Equal("TESTING"))
@@ -47,6 +52,7 @@ func TestConfigGetWithDefault(t *testing.T) {
func TestConfigGet(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
defer teardownTestApp()
@@ -59,45 +65,48 @@ func TestConfigGet(t *testing.T) {
func TestConfigSetMany(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
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())
vals := []string{"testKey=updated", "testKey2=new"}
Expect(CommandSet(testAppName, vals, false, true, 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())
vals = []string{"testKey=updated_global", "testKey2=new_global"}
Expect(CommandSet("", vals, true, true, 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())
Expect(CommandSet(testAppName+"does_not_exist", vals, false, true, false)).ToNot(Succeed())
}
func TestConfigUnsetAll(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
defer teardownTestApp()
expectValue(testAppName, "testKey", "TESTING")
expectValue("", "testKey", "GLOBAL_TESTING")
Expect(UnsetAll(testAppName, false)).To(Succeed())
Expect(CommandClear(testAppName, false, true)).To(Succeed())
expectNoValue(testAppName, "testKey")
expectNoValue(testAppName, "noKey")
expectNoValue(testAppName, "globalKey")
Expect(UnsetAll(testAppName+"does-not-exist", false)).ToNot(Succeed())
Expect(CommandClear(testAppName+"does-not-exist", false, true)).ToNot(Succeed())
}
func TestConfigUnsetMany(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
defer teardownTestApp()
@@ -105,19 +114,20 @@ func TestConfigUnsetMany(t *testing.T) {
expectValue("", "testKey", "GLOBAL_TESTING")
keys := []string{"testKey", "noKey"}
Expect(UnsetMany(testAppName, keys, false)).To(Succeed())
Expect(CommandUnset(testAppName, keys, false, true)).To(Succeed())
expectNoValue(testAppName, "testKey")
expectValue("", "testKey", "GLOBAL_TESTING")
Expect(UnsetMany(testAppName, keys, false)).To(Succeed())
Expect(CommandUnset(testAppName, keys, false, true)).To(Succeed())
expectNoValue(testAppName, "testKey")
expectNoValue(testAppName, "globalKey")
Expect(UnsetMany(testAppName+"does-not-exist", keys, false)).ToNot(Succeed())
Expect(CommandUnset(testAppName+"does-not-exist", keys, false, true)).ToNot(Succeed())
}
func TestEnvironmentLoading(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
defer teardownTestApp()
@@ -142,6 +152,7 @@ func TestEnvironmentLoading(t *testing.T) {
func TestInvalidKeys(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
defer teardownTestApp()
@@ -158,6 +169,7 @@ func TestInvalidKeys(t *testing.T) {
func TestInvalidEnvOnDisk(t *testing.T) {
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
Expect(setupTestApp()).To(Succeed())
defer teardownTestApp()

View File

@@ -60,10 +60,6 @@ func newEnvFromString(rep string) (env *Env, err error) {
//LoadAppEnv loads an environment for the given app
func LoadAppEnv(appName string) (env *Env, err error) {
err = common.VerifyAppName(appName)
if err != nil {
return
}
appfile, err := getAppFile(appName)
if err != nil {
return
@@ -357,10 +353,6 @@ func loadFromFile(name string, filename string) (env *Env, err error) {
}
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
}

View File

@@ -7,7 +7,7 @@ config_sub() {
declare desc="executes a config subcommand"
local name="$1"
shift
"$PLUGIN_AVAILABLE_PATH/config/subcommands/$name" "config:$name" "$@"
"$PLUGIN_AVAILABLE_PATH/config/config_sub" "$name" "$@"
}
config_export() {

View File

@@ -1,13 +1,16 @@
package config
import "fmt"
import (
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
func export(appName string, global bool, merged bool, format string) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
"github.com/dokku/dokku/plugins/common"
)
func export(appName string, merged bool, format string) error {
env := getEnvironment(appName, merged)
exportType := ExportFormatExports
suffix := "\n"
@@ -37,3 +40,113 @@ func export(appName string, global bool, merged bool, format string) error {
fmt.Print(exported + suffix)
return nil
}
// SubBundle implements the logic for config:bundle without app name validation
func SubBundle(appName string, merged bool) error {
env := getEnvironment(appName, merged)
return env.ExportBundle(os.Stdout)
}
// SubClear implements the logic for config:clear without app name validation
func SubClear(appName string, noRestart bool) error {
return UnsetAll(appName, !noRestart)
}
// SubExport implements the logic for config:export without app name validation
func SubExport(appName string, merged bool, format string) error {
return export(appName, merged, format)
}
// SubGet implements the logic for config:get without app name validation
func SubGet(appName string, keys []string, quoted bool) error {
if len(keys) == 0 {
return errors.New("Expected: key")
}
if len(keys) != 1 {
return fmt.Errorf("Unexpected argument(s): %v", keys[1:])
}
value, ok := Get(appName, keys[0])
if !ok {
os.Exit(1)
return nil
}
if quoted {
fmt.Printf("'%s'\n", singleQuoteEscape(value))
} else {
fmt.Printf("%s\n", value)
}
return nil
}
// SubKeys implements the logic for config:keys without app name validation
func SubKeys(appName string, merged bool) error {
env := getEnvironment(appName, merged)
for _, k := range env.Keys() {
fmt.Println(k)
}
return nil
}
// SubSet implements the logic for config:set without app name validation
func SubSet(appName string, pairs []string, noRestart bool, encoded bool) error {
if len(pairs) == 0 {
return errors.New("At least one env pair must be given")
}
updated := make(map[string]string)
for _, e := range pairs {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 1 {
return fmt.Errorf("Invalid env pair: %v", e)
}
key, value := parts[0], parts[1]
if encoded {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return fmt.Errorf("%s for key '%s'", err.Error(), key)
}
value = string(decoded)
}
updated[key] = value
}
return SetMany(appName, updated, !noRestart)
}
// SubShow implements the logic for config:show without app name validation
func SubShow(appName string, merged bool, shell bool, export bool) error {
env := getEnvironment(appName, merged)
if shell && export {
return errors.New("Only one of --shell and --export can be given")
}
if shell {
common.LogWarn("Deprecated: Use 'config:export --format shell' instead")
fmt.Print(env.Export(ExportFormatShell))
} else if export {
common.LogWarn("Deprecated: Use 'config:export --format exports' instead")
fmt.Println(env.Export(ExportFormatExports))
} else {
contextName := "global"
if appName != "" {
contextName = appName
}
common.LogInfo2Quiet(contextName + " env vars")
fmt.Println(env.Export(ExportFormatPretty))
}
return nil
}
// SubUnset implements the logic for config:unset without app name validation
func SubUnset(appName string, keys []string, noRestart bool) error {
if len(keys) == 0 {
return fmt.Errorf("At least one key must be given")
}
return UnsetMany(appName, keys, !noRestart)
}

View File

@@ -0,0 +1,112 @@
package main
import (
"fmt"
"os"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
flag "github.com/spf13/pflag"
)
func getKeys(args []string, global bool) []string {
keys := args
if !global && len(keys) > 0 {
keys = keys[1:]
}
return keys
}
// main entrypoint to all subcommands
func main() {
action := os.Args[1]
var err error
appName := "--global"
switch action {
case "bundle":
args := flag.NewFlagSet("bundle", flag.ExitOnError)
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:])
if !*global {
appName = args.Arg(0)
}
err = config.SubBundle(appName, *merged)
case "clear":
args := flag.NewFlagSet("clear", 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:])
if !*global {
appName = args.Arg(0)
}
err = config.SubClear(appName, *noRestart)
case "export":
args := flag.NewFlagSet("export", flag.ExitOnError)
global := args.Bool("global", false, "--global: use the global environment")
merged := args.Bool("merged", false, "--merged: merge app environment and global environment")
format := args.String("format", "exports", "--format: [ docker-args | docker-args-keys | exports | envfile | json | json-list | pack-keys | pretty | shell ] which format to export as)")
args.Parse(os.Args[2:])
if !*global {
appName = args.Arg(0)
}
err = config.SubExport(appName, *merged, *format)
case "get":
args := flag.NewFlagSet("get", flag.ExitOnError)
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:])
if !*global {
appName = args.Arg(0)
}
keys := getKeys(args.Args(), *global)
err = config.SubGet(appName, keys, *quoted)
case "keys":
args := flag.NewFlagSet("keys", flag.ExitOnError)
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:])
if !*global {
appName = args.Arg(0)
}
err = config.SubKeys(appName, *merged)
case "show":
args := flag.NewFlagSet("show", flag.ExitOnError)
global := args.Bool("global", false, "--global: use the global environment")
merged := args.Bool("merged", false, "--merged: display the app's environment merged with the global environment")
args.Parse(os.Args[2:])
if !*global {
appName = args.Arg(0)
}
err = config.SubShow(appName, *merged, false, false)
case "set":
args := flag.NewFlagSet("set", flag.ExitOnError)
global := args.Bool("global", false, "--global: use the global environment")
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:])
if !*global {
appName = args.Arg(0)
}
pairs := getKeys(args.Args(), *global)
err = config.SubSet(appName, pairs, *noRestart, *encoded)
case "unset":
args := flag.NewFlagSet("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:])
if !*global {
appName = args.Arg(0)
}
keys := getKeys(args.Args(), *global)
err = config.SubUnset(appName, keys, *noRestart)
default:
err = fmt.Errorf("Invalid plugin config_sub call: %s", action)
}
if err != nil {
common.LogFailWithError(err)
}
}

View File

@@ -1,156 +1,83 @@
package config
import (
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
)
// CommandBundle implements config:bundle
// CommandBundle creates a tarball of a .env.d directory
// containing env vars for the app
func CommandBundle(appName string, global bool, merged bool) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
env := getEnvironment(appName, merged)
return env.ExportBundle(os.Stdout)
return SubBundle(appName, merged)
}
// CommandClear implements config:clear
// CommandClear unsets all environment variables in use
func CommandClear(appName string, global bool, noRestart bool) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
return UnsetAll(appName, !noRestart)
return SubClear(appName, noRestart)
}
// CommandExport implements config:export
// CommandExport outputs all env vars (merged or not, global or not)
// in the specified format for consumption by other tools
func CommandExport(appName string, global bool, merged bool, format string) error {
return export(appName, global, merged, format)
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
return SubExport(appName, merged, format)
}
// CommandGet implements config:get
// CommandGet gets the value for the specified environment variable
func CommandGet(appName string, keys []string, global bool, quoted bool) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
if len(keys) == 0 {
return errors.New("Expected: key")
}
if len(keys) != 1 {
return fmt.Errorf("Unexpected argument(s): %v", keys[1:])
}
value, ok := Get(appName, keys[0])
if !ok {
os.Exit(1)
return nil
}
if quoted {
fmt.Printf("'%s'\n", singleQuoteEscape(value))
} else {
fmt.Printf("%s\n", value)
}
return nil
return SubGet(appName, keys, quoted)
}
// CommandKeys implements config:keys
// CommandKeys shows the keys set for the specified environment
func CommandKeys(appName string, global bool, merged bool) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
env := getEnvironment(appName, merged)
for _, k := range env.Keys() {
fmt.Println(k)
}
return nil
return SubKeys(appName, merged)
}
// CommandSet implements config:set
// CommandSet sets one or more environment variable pairs
func CommandSet(appName string, pairs []string, global bool, noRestart bool, encoded bool) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
if len(pairs) == 0 {
return errors.New("At least one env pair must be given")
}
updated := make(map[string]string)
for _, e := range pairs {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 1 {
return fmt.Errorf("Invalid env pair: %v", e)
}
key, value := parts[0], parts[1]
if encoded {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return fmt.Errorf("%s for key '%s'", err.Error(), key)
}
value = string(decoded)
}
updated[key] = value
}
return SetMany(appName, updated, !noRestart)
return SubSet(appName, pairs, noRestart, encoded)
}
// CommandShow implements config:show
// CommandShow pretty-prints the specified environment vaiables
func CommandShow(appName string, global bool, merged bool, shell bool, export bool) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
env := getEnvironment(appName, merged)
if shell && export {
return errors.New("Only one of --shell and --export can be given")
}
if shell {
common.LogWarn("Deprecated: Use 'config:export --format shell' instead")
fmt.Print(env.Export(ExportFormatShell))
} else if export {
common.LogWarn("Deprecated: Use 'config:export --format exports' instead")
fmt.Println(env.Export(ExportFormatExports))
} else {
contextName := "global"
if appName != "" {
contextName = appName
}
common.LogInfo2Quiet(contextName + " env vars")
fmt.Println(env.Export(ExportFormatPretty))
}
return nil
return SubShow(appName, merged, shell, export)
}
// CommandUnset implements config:unset
// CommandUnset unsets one or more keys in a specified environment
func CommandUnset(appName string, keys []string, global bool, noRestart bool) error {
appName, err := getAppNameOrGlobal(appName, global)
if err != nil {
return err
}
if len(keys) == 0 {
return fmt.Errorf("At least one key must be given")
}
return UnsetMany(appName, keys, !noRestart)
return SubUnset(appName, keys, noRestart)
}

View File

@@ -16,7 +16,13 @@ func TriggerConfigExport(appName string, global string, merged string, format st
if err != nil {
return err
}
return export(appName, g, m, format)
appName, err = getAppNameOrGlobal(appName, g)
if err != nil {
return err
}
return export(appName, m, format)
}
// TriggerConfigGet returns an app config value by key