package common import ( "bufio" "errors" "fmt" "os" "path/filepath" "reflect" "sort" "strings" ) // CommandPropertySet is a generic function that will set a property for a given plugin/app combination func CommandPropertySet(pluginName, appName, property, value string, validProperties map[string]string, validGlobalProperties map[string]bool) { if appName != "--global" { if err := VerifyAppName(appName); err != nil { LogFailWithError(err) } } if appName == "--global" && !validGlobalProperties[property] { LogFail("Property cannot be specified globally") } if property == "" { LogFail("No property specified") } for k := range validGlobalProperties { if _, ok := validProperties[k]; !ok { validProperties[k] = "" } } if _, ok := validProperties[property]; !ok { properties := reflect.ValueOf(validProperties).MapKeys() validPropertyList := make([]string, len(properties)) for i := 0; i < len(properties); i++ { validPropertyList[i] = properties[i].String() } sort.Strings(validPropertyList) LogFail(fmt.Sprintf("Invalid property specified, valid properties include: %s", strings.Join(validPropertyList, ", "))) } if value != "" { LogInfo2Quiet(fmt.Sprintf("Setting %s to %s", property, value)) if err := PropertyWrite(pluginName, appName, property, value); err != nil { LogFailWithError(err) } } else { LogInfo2Quiet(fmt.Sprintf("Unsetting %s", property)) if err := PropertyDelete(pluginName, appName, property); err != nil { LogFailWithError(err) } } } // PropertyClone clones a set of properties from one app to another func PropertyClone(pluginName string, oldAppName string, newAppName string) error { properties, err := PropertyGetAll(pluginName, oldAppName) if err != nil { return nil } for property, value := range properties { if err := PropertyWrite(pluginName, newAppName, property, value); err != nil { return err } } return nil } // PropertyDelete deletes a property from the plugin properties for an app func PropertyDelete(pluginName string, appName string, property string) error { propertyPath := getPropertyPath(pluginName, appName, property) if err := os.Remove(propertyPath); err != nil { if !PropertyExists(pluginName, appName, property) { return nil } return fmt.Errorf("Unable to remove %s property %s.%s", pluginName, appName, property) } return nil } // PropertyDestroy destroys the plugin properties for an app func PropertyDestroy(pluginName string, appName string) error { if appName == "_all_" { pluginConfigPath := getPluginConfigPath(pluginName) return os.RemoveAll(pluginConfigPath) } pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) return os.RemoveAll(pluginAppConfigRoot) } // PropertyExists returns whether a property exists or not func PropertyExists(pluginName string, appName string, property string) bool { propertyPath := getPropertyPath(pluginName, appName, property) _, err := os.Stat(propertyPath) return !os.IsNotExist(err) } // PropertyGet returns the value for a given property func PropertyGet(pluginName string, appName string, property string) string { return PropertyGetDefault(pluginName, appName, property, "") } // PropertyGetAll returns a map of all properties for a given app func PropertyGetAll(pluginName string, appName string) (map[string]string, error) { properties := make(map[string]string) pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) fi, err := os.Stat(pluginAppConfigRoot) if err != nil { return properties, nil } if !fi.IsDir() { return properties, errors.New("Specified property path is not a directory") } files, err := os.ReadDir(pluginAppConfigRoot) if err != nil { return properties, err } for _, file := range files { if file.IsDir() { continue } property := file.Name() properties[property] = PropertyGet(pluginName, appName, property) } return properties, nil } // PropertyGetAllByPrefix returns a map of all properties for a given app with a specified prefix func PropertyGetAllByPrefix(pluginName string, appName string, prefix string) (map[string]string, error) { properties := make(map[string]string) pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) fi, err := os.Stat(pluginAppConfigRoot) if err != nil { return properties, nil } if !fi.IsDir() { return properties, errors.New("Specified property path is not a directory") } files, err := os.ReadDir(pluginAppConfigRoot) if err != nil { return properties, err } for _, file := range files { if file.IsDir() { continue } property := file.Name() if !strings.HasPrefix(property, prefix) { continue } properties[property] = PropertyGet(pluginName, appName, property) } return properties, nil } // PropertyGetDefault returns the value for a given property with a specified default value func PropertyGetDefault(pluginName, appName, property, defaultValue string) (val string) { if !PropertyExists(pluginName, appName, property) { val = defaultValue return } propertyPath := getPropertyPath(pluginName, appName, property) b, err := os.ReadFile(propertyPath) if err != nil { LogWarn(fmt.Sprintf("Unable to read %s property %s.%s", pluginName, appName, property)) return } val = string(b) return } // 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 { return err } scannedLines, err := PropertyListGet(pluginName, appName, property) if err != nil { return err } value = strings.TrimSpace(value) var lines []string for i, line := range scannedLines { if index != 0 && i == (index-1) { lines = append(lines, value) } lines = append(lines, line) } if index == 0 || index > len(scannedLines) { lines = append(lines, value) } return PropertyListWrite(pluginName, appName, property, lines) } // PropertyListWrite completely rewrites a list property func PropertyListWrite(pluginName string, appName string, property string, values []string) error { if err := propertyTouch(pluginName, appName, property); 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 } w := bufio.NewWriter(file) for _, line := range values { 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()) } if err := file.Close(); err != nil { return fmt.Errorf("Unable to close %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } if err := SetPermissions(SetPermissionInput{ Filename: propertyPath, Mode: os.FileMode(0600), }); err != nil { return fmt.Errorf("Unable to set permissions for %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } return nil } // PropertyListGet returns a property list func PropertyListGet(pluginName string, appName string, property string) (lines []string, err error) { if !PropertyExists(pluginName, appName, property) { return lines, nil } propertyPath := getPropertyPath(pluginName, appName, property) file, err := os.Open(propertyPath) if err != nil { return lines, err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } if err = scanner.Err(); err != nil { return lines, fmt.Errorf("Unable to read %s config value for %s.%s: %s", pluginName, appName, property, err.Error()) } return lines, nil } // PropertyListLength returns the length of a property list func PropertyListLength(pluginName string, appName string, property string) (length int, err error) { if !PropertyExists(pluginName, appName, property) { return length, nil } propertyPath := getPropertyPath(pluginName, appName, property) file, err := os.Open(propertyPath) if err != nil { return length, err } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } if err = scanner.Err(); err != nil { return length, fmt.Errorf("Unable to read %s config value for %s.%s: %s", pluginName, appName, property, err.Error()) } length = len(lines) return length, nil } // PropertyListGetByIndex returns an entry within property list by index func PropertyListGetByIndex(pluginName string, appName string, property string, index int) (propertyValue string, err error) { lines, err := PropertyListGet(pluginName, appName, property) if err != nil { return } found := false for i, line := range lines { if i == index { propertyValue = line found = true } } if !found { err = errors.New("Index not found") } return } // PropertyListGetByValue returns an entry within property list by value func PropertyListGetByValue(pluginName string, appName string, property string, value string) (propertyValue string, err error) { lines, err := PropertyListGet(pluginName, appName, property) if err != nil { return } found := false for _, line := range lines { if line == value { propertyValue = line found = true } } if !found { err = errors.New("Value not found") } return } // PropertyListRemove removes a value from a property list func PropertyListRemove(pluginName string, appName string, property string, value 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 line == value { 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()) } if err := file.Close(); err != nil { return fmt.Errorf("Unable to close %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } if err := SetPermissions(SetPermissionInput{ Filename: propertyPath, Mode: os.FileMode(0600), }); err != nil { return fmt.Errorf("Unable to set permissions for %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } if !found { return errors.New("Property not found, nothing was removed") } 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()) } if err := file.Close(); err != nil { return fmt.Errorf("Unable to close %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } if err := SetPermissions(SetPermissionInput{ Filename: propertyPath, Mode: os.FileMode(0600), }); err != nil { return fmt.Errorf("Unable to set permissions for %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } 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 { return err } scannedLines, err := PropertyListGet(pluginName, appName, property) if err != nil { return err } value = strings.TrimSpace(value) var lines []string if index >= len(scannedLines) { lines = append(lines, scannedLines...) lines = append(lines, value) } else { for i, line := range scannedLines { if i == index { lines = append(lines, value) } else { lines = append(lines, line) } } } return PropertyListWrite(pluginName, appName, property, lines) } // 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()) } propertyPath := getPropertyPath(pluginName, appName, property) if PropertyExists(pluginName, appName, property) { return nil } file, err := os.Create(propertyPath) if err != nil { return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } defer file.Close() return nil } // 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 { return err } propertyPath := getPropertyPath(pluginName, appName, property) file, err := os.Create(propertyPath) if err != nil { return fmt.Errorf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } defer file.Close() fmt.Fprint(file, value) if err := file.Close(); err != nil { return fmt.Errorf("Unable to close %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } if err := SetPermissions(SetPermissionInput{ Filename: propertyPath, Mode: os.FileMode(0600), }); err != nil { return fmt.Errorf("Unable to set permissions for %s config value %s.%s: %s", pluginName, appName, property, err.Error()) } return nil } // PropertySetup creates the plugin config root func PropertySetup(pluginName string) error { configRoot := filepath.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config") pluginConfigRoot := getPluginConfigPath(pluginName) if err := os.MkdirAll(pluginConfigRoot, 0755); err != nil { return err } // check if configRoot is a symlink if !IsSymlink(configRoot) { input := SetPermissionInput{ Filename: configRoot, Mode: os.FileMode(0755), } if err := SetPermissions(input); err != nil { return err } } return SetPermissions(SetPermissionInput{ Filename: pluginConfigRoot, Mode: os.FileMode(0755), }) } func PropertySetupApp(pluginName string, appName 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()) } return nil } func getPropertyPath(pluginName string, appName string, property string) string { pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) return filepath.Join(pluginAppConfigRoot, property) } // getPluginAppPropertyPath returns the plugin property path for a given plugin/app combination func getPluginAppPropertyPath(pluginName string, appName string) string { return filepath.Join(getPluginConfigPath(pluginName), appName) } // getPluginConfigPath returns the plugin property path for a given plugin func getPluginConfigPath(pluginName string) string { return filepath.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config", pluginName) } // makePluginAppPropertyPath ensures that a property path exists func makePluginAppPropertyPath(pluginName string, appName string) error { pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) if err := os.MkdirAll(pluginAppConfigRoot, 0755); err != nil { return err } return SetPermissions(SetPermissionInput{ Filename: pluginAppConfigRoot, Mode: os.FileMode(0755), }) }