refactor: unify property function implementations

Rather than reimplementing the property functions in two different languages - and potentially having compatibility issues - standardize on golang for the implementation through the use of a generated binary.

This also implements all the list functions for use with shell-based plugins.

Note that the interface to the prop binary is experimental, and it should not be used directly.
This commit is contained in:
Jose Diaz-Gonzalez
2019-11-27 16:35:52 -05:00
parent 72d480c9dd
commit 4d9e8f5f7d
6 changed files with 328 additions and 103 deletions

1
plugins/common/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/prop

10
plugins/common/Makefile Normal file
View File

@@ -0,0 +1,10 @@
BUILD = prop
PLUGIN_NAME = common
clean-prop:
rm -rf prop
prop: clean-prop **/**/prop.go
go build $(GO_ARGS) -o prop src/prop/prop.go
include ../../common.mk

View File

@@ -262,6 +262,15 @@ func MustGetEnv(key string) (val string) {
return
}
// GetenvWithDefault returns env variable or defaultValue if it's not set
func GetenvWithDefault(key string, defaultValue string) (val string) {
val = os.Getenv(key)
if val == "" {
val = defaultValue
}
return
}
// ReadFirstLine gets the first line of a file that has contents and returns it
// if there are no contents, an empty string is returned
// will also return an empty string if the file does not exist

View File

@@ -115,7 +115,7 @@ func PropertyGetDefault(pluginName, appName, property, defaultValue string) (val
// PropertyListAdd adds a property to a list at an optionally specified index
func PropertyListAdd(pluginName string, appName string, property string, value string, index int) error {
if err := PropertyTouch(pluginName, appName, property); err != nil {
if err := propertyTouch(pluginName, appName, property); err != nil {
return err
}
@@ -262,9 +262,45 @@ func PropertyListRemove(pluginName string, appName string, property string, valu
return nil
}
// PropertyListRemoveByPrefix removes a value by prefix from a property list
func PropertyListRemoveByPrefix(pluginName string, appName string, property string, prefix string) error {
lines, err := PropertyListGet(pluginName, appName, property)
if err != nil {
return err
}
propertyPath := getPropertyPath(pluginName, appName, property)
file, err := os.OpenFile(propertyPath, os.O_RDWR|os.O_TRUNC, 0600)
if err != nil {
return err
}
found := false
w := bufio.NewWriter(file)
for _, line := range lines {
if strings.HasPrefix(line, prefix) {
found = true
continue
}
fmt.Fprintln(w, line)
}
if err = w.Flush(); err != nil {
return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error())
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
if !found {
return errors.New("Property not found, nothing was removed")
}
return nil
}
// PropertyListSet sets a value within a property list at a specified index
func PropertyListSet(pluginName string, appName string, property string, value string, index int) error {
if err := PropertyTouch(pluginName, appName, property); err != nil {
if err := propertyTouch(pluginName, appName, property); err != nil {
return err
}
@@ -310,8 +346,8 @@ func PropertyListSet(pluginName string, appName string, property string, value s
return nil
}
// PropertyTouch ensures a given application property file exists
func PropertyTouch(pluginName string, appName string, property string) error {
// propertyTouch ensures a given application property file exists
func propertyTouch(pluginName string, appName string, property string) error {
if err := makePluginAppPropertyPath(pluginName, appName); err != nil {
return fmt.Errorf("Unable to create %s config directory for %s: %s", pluginName, appName, err.Error())
}
@@ -332,7 +368,7 @@ func PropertyTouch(pluginName string, appName string, property string) error {
// PropertyWrite writes a value for a given application property
func PropertyWrite(pluginName string, appName string, property string, value string) error {
if err := PropertyTouch(pluginName, appName, property); err != nil {
if err := propertyTouch(pluginName, appName, property); err != nil {
return err
}
@@ -350,10 +386,13 @@ func PropertyWrite(pluginName string, appName string, property string, value str
}
// PropertySetup creates the plugin config root
func PropertySetup(pluginName string) (err error) {
func PropertySetup(pluginName string) error {
pluginConfigRoot := getPluginConfigPath(pluginName)
if err = os.MkdirAll(pluginConfigRoot, 0755); err != nil {
return
if err := os.MkdirAll(pluginConfigRoot, 0755); err != nil {
return err
}
if err := setPermissions(path.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config"), 0755); err != nil {
return err
}
return setPermissions(pluginConfigRoot, 0755)
}
@@ -374,46 +413,40 @@ func getPluginConfigPath(pluginName string) string {
}
// makePluginAppPropertyPath ensures that a property path exists
func makePluginAppPropertyPath(pluginName string, appName string) (err error) {
func makePluginAppPropertyPath(pluginName string, appName string) error {
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
if err = os.MkdirAll(pluginAppConfigRoot, 0755); err != nil {
return
if err := os.MkdirAll(pluginAppConfigRoot, 0755); err != nil {
return err
}
return setPermissions(pluginAppConfigRoot, 0755)
}
// setPermissions sets the proper owner and filemode for a given file
func setPermissions(path string, fileMode os.FileMode) (err error) {
if err = os.Chmod(path, fileMode); err != nil {
func setPermissions(path string, fileMode os.FileMode) error {
if err := os.Chmod(path, fileMode); err != nil {
return err
}
systemGroup := os.Getenv("DOKKU_SYSTEM_GROUP")
systemUser := os.Getenv("DOKKU_SYSTEM_USER")
if systemGroup == "" {
systemGroup = "dokku"
}
if systemUser == "" {
systemUser = "dokku"
}
systemGroup := GetenvWithDefault("DOKKU_SYSTEM_GROUP", "dokku")
systemUser := GetenvWithDefault("DOKKU_SYSTEM_USER", "dokku")
group, err := user.LookupGroup(systemGroup)
if err != nil {
return
return err
}
user, err := user.Lookup(systemUser)
if err != nil {
return
return err
}
uid, err := strconv.Atoi(user.Uid)
if err != nil {
return
return err
}
gid, err := strconv.Atoi(group.Gid)
if err != nil {
return
return err
}
return os.Chown(path, uid, gid)
}

View File

@@ -2,102 +2,93 @@
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
fn-plugin-property-delete() {
declare desc="delete a key from the property store for an app"
declare PLUGIN="$1" APP="$2" KEY="$3"
fn-plugin-property-get() {
declare desc="returns the value for a given property"
declare PLUGIN="$1" APP="$2" KEY="$3" DEFAULT="$4"
if [[ -z "$PLUGIN" ]] || [[ -z "$APP" ]] || [[ -z "$KEY" ]]; then
return 1
fi
rm -f "${DOKKU_LIB_ROOT}/config/${PLUGIN}/${APP}/${KEY}"
fn-plugin-property-get-default "$PLUGIN" "$APP" "$KEY" "$DEFAULT"
}
fn-plugin-property-destroy() {
declare desc="destroy the properties for an app"
declare PLUGIN="$1" APP="$2"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "destroy" "$PLUGIN" "$APP" "$KEY"
}
if [[ -z "$PLUGIN" ]] || [[ -z "$APP" ]]; then
return 1
fi
if [[ "$APP" == "_all_" ]]; then
rm -rf "${DOKKU_LIB_ROOT}/config/${PLUGIN}"
else
rm -rf "${DOKKU_LIB_ROOT}/config/${PLUGIN}/${APP}"
fi
fn-plugin-property-delete() {
declare desc="delete a key from the property store for an app"
declare PLUGIN="$1" APP="$2" KEY="$3"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "del" "$PLUGIN" "$APP" "$KEY"
}
fn-plugin-property-exists() {
declare desc="returns whether the property store has a value for an app"
declare PLUGIN="$1" APP="$2" KEY="$3" DEFAULT="$4"
local CONFIG_VALUE
if [[ -z "$PLUGIN" ]] || [[ -z "$APP" ]] || [[ -z "$KEY" ]]; then
return 1
fi
if [[ ! -f "${DOKKU_LIB_ROOT:?}/config/${PLUGIN}/${APP}/${KEY}" ]]; then
return 1
fi
}
fn-plugin-property-get() {
declare desc="returns the property store value for an app"
declare PLUGIN="$1" APP="$2" KEY="$3" DEFAULT="$4"
local CONFIG_VALUE
if [[ -z "$PLUGIN" ]] || [[ -z "$APP" ]] || [[ -z "$KEY" ]]; then
return 1
fi
CONFIG_VALUE="$(fn-plugin-property-read "$PLUGIN" "$APP" "$KEY" || true)"
if [[ -z "$CONFIG_VALUE" ]]; then
CONFIG_VALUE="$DEFAULT"
fi
echo "$CONFIG_VALUE"
}
fn-plugin-property-setup() {
declare desc="creates the plugin config root"
declare PLUGIN="$1"
if [[ -z "$PLUGIN" ]]; then
return 1
fi
mkdir -p "${DOKKU_LIB_ROOT}/config/${PLUGIN}"
chmod 0755 "${DOKKU_LIB_ROOT}/config/${PLUGIN}"
local dokku_system_group=${DOKKU_SYSTEM_GROUP:="dokku"}
local dokku_system_user=${DOKKU_SYSTEM_USER:="dokku"}
chown "${dokku_system_user}:${dokku_system_group}" "${DOKKU_LIB_ROOT}/config/${PLUGIN}"
}
fn-plugin-property-read() {
declare desc="read a key from the property store for an app"
declare PLUGIN="$1" APP="$2" KEY="$3"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "exists" "$PLUGIN" "$APP" "$KEY"
}
if [[ -z "$PLUGIN" ]] || [[ -z "$APP" ]] || [[ -z "$KEY" ]]; then
return 1
fi
fn-plugin-property-get-all() {
declare desc="returns a map of all properties for a given app"
declare PLUGIN="$1" APP="$2"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "get-all" "$PLUGIN" "$APP"
}
if [[ ! -f "${DOKKU_LIB_ROOT:?}/config/${PLUGIN}/${APP}/${KEY}" ]]; then
return 1
fi
fn-plugin-property-get-default() {
declare desc="returns the value for a given property with a specified default value"
declare PLUGIN="$1" APP="$2" KEY="$3" DEFAULT="$4"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "get-with-default" "$PLUGIN" "$APP" "$KEY" "$DEFAULT"
}
cat "${DOKKU_LIB_ROOT}/config/${PLUGIN}/${APP}/${KEY}"
fn-plugin-property-list-add() {
declare desc="adds a property to a list at an optionally specified index"
declare PLUGIN="$1" APP="$2" KEY="$3" VALUE="$4" INDEX="$5"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "rpush" "$PLUGIN" "$APP" "$KEY" "$VALUE" "$INDEX"
}
fn-plugin-property-list-get() {
declare desc="returns a property list"
declare PLUGIN="$1" APP="$2" KEY="$3"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "lrange" "$PLUGIN" "$APP" "$KEY"
}
fn-plugin-property-list-get-by-index() {
declare desc="returns an entry within property list by index"
declare PLUGIN="$1" APP="$2" KEY="$3" INDEX="$4"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "lindex" "$PLUGIN" "$APP" "$KEY" "$INDEX"
}
fn-plugin-property-list-get-by-value() {
declare desc="returns an entry within property list by value"
declare PLUGIN="$1" APP="$2" KEY="$3" VALUE="$4"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "lismember" "$PLUGIN" "$APP" "$KEY" "$VALUE"
}
fn-plugin-property-list-remove() {
declare desc="removes a value from a property list"
declare PLUGIN="$1" APP="$2" KEY="$3" VALUE="$4"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "lrem" "$PLUGIN" "$APP" "$KEY" "$VALUE"
}
fn-plugin-property-list-remove-by-prefix() {
declare desc="removes a value from a property list"
declare PLUGIN="$1" APP="$2" KEY="$3" PREFIX="$4"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "lrem-by-prefix" "$PLUGIN" "$APP" "$KEY" "$PREFIX"
}
fn-plugin-property-list-set() {
declare desc="sets a value within a property list at a specified index"
declare PLUGIN="$1" APP="$2" KEY="$3" VALUE="$4" INDEX="$5"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "lset" "$PLUGIN" "$APP" "$KEY" "$VALUE" "$INDEX"
}
fn-plugin-property-write() {
declare desc="read a key from the property store for an app"
declare PLUGIN="$1" APP="$2" KEY="$3" VALUE="$4"
if [[ -z "$PLUGIN" ]] || [[ -z "$APP" ]] || [[ -z "$KEY" ]]; then
return 1
fi
mkdir -p "${DOKKU_LIB_ROOT}/config/${PLUGIN}/${APP}"
echo "$VALUE" >"${DOKKU_LIB_ROOT}/config/${PLUGIN}/${APP}/${KEY}"
chmod 600 "${DOKKU_LIB_ROOT}/config/${PLUGIN}/${APP}/${KEY}"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "set" "$PLUGIN" "$APP" "$KEY" "$VALUE"
}
fn-plugin-property-setup() {
declare desc="creates the plugin config root"
declare PLUGIN="$1"
"$PLUGIN_CORE_AVAILABLE_PATH/common/prop" "setup" "$PLUGIN"
}

View File

@@ -0,0 +1,181 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
)
func strToInt(s string, defaultValue int, allowEmpty bool) int {
if s == "" {
if !allowEmpty {
os.Exit(1)
}
return defaultValue
}
index, err := strconv.Atoi(s)
if err != nil {
os.Exit(1)
}
return index
}
func main() {
flag.Parse()
cmd := flag.Arg(0)
pluginName := flag.Arg(1)
switch cmd {
case "rpush":
appName := flag.Arg(2)
property := flag.Arg(3)
value := flag.Arg(4)
index := strToInt(flag.Arg(5), 0, true)
err := common.PropertyListAdd(pluginName, appName, property, value, index)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "llen":
appName := flag.Arg(2)
property := flag.Arg(3)
lines, err := common.PropertyListGet(pluginName, appName, property)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
println(len(lines))
case "lrange":
appName := flag.Arg(2)
property := flag.Arg(3)
lines, err := common.PropertyListGet(pluginName, appName, property)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
for _, line := range lines {
println(line)
}
case "lindex":
appName := flag.Arg(2)
property := flag.Arg(3)
index := strToInt(flag.Arg(4), 0, false)
propertyValue, err := common.PropertyListGetByIndex(pluginName, appName, property, index)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
println(propertyValue)
case "lismember":
appName := flag.Arg(2)
property := flag.Arg(3)
value := flag.Arg(4)
_, err := common.PropertyListGetByValue(pluginName, appName, property, value)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "lrem":
appName := flag.Arg(2)
property := flag.Arg(3)
value := flag.Arg(4)
err := common.PropertyListRemove(pluginName, appName, property, value)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "lrem-by-prefix":
appName := flag.Arg(2)
property := flag.Arg(3)
prefix := flag.Arg(4)
err := common.PropertyListRemoveByPrefix(pluginName, appName, property, prefix)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "lset":
appName := flag.Arg(2)
property := flag.Arg(3)
value := flag.Arg(4)
index := strToInt(flag.Arg(5), 0, false)
err := common.PropertyListSet(pluginName, appName, property, value, index)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "set":
appName := flag.Arg(2)
property := flag.Arg(3)
value := flag.Arg(4)
err := common.PropertyWrite(pluginName, appName, property, value)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "get":
appName := flag.Arg(2)
property := flag.Arg(3)
value := common.PropertyGet(pluginName, appName, property)
if value != "" {
println(value)
}
case "get-with-default":
appName := flag.Arg(2)
property := flag.Arg(3)
defaultValue := flag.Arg(4)
value := common.PropertyGetDefault(pluginName, appName, property, defaultValue)
if value != "" {
println(value)
}
case "get-all":
appName := flag.Arg(2)
values, err := common.PropertyGetAll(pluginName, appName)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
for key, value := range values {
fmt.Fprintln(os.Stdout, key, strings.TrimSuffix(value, "\n"))
}
case "exists":
appName := flag.Arg(2)
property := flag.Arg(3)
exists := common.PropertyExists(pluginName, appName, property)
if !exists {
os.Exit(1)
}
case "del":
appName := flag.Arg(2)
property := flag.Arg(3)
err := common.PropertyDelete(pluginName, appName, property)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "setup":
err := common.PropertySetup(pluginName)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
case "destroy":
appName := flag.Arg(2)
err := common.PropertyDestroy(pluginName, appName)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
}