All checks were successful
continuous-integration/drone/push Build is passing
This implements proper modifier support in the env file using this new fork of the godotenv library. The modifier implementation is quite basic for but can be improved later if needed. See this commit for the actual implementation. Because we are now using proper modifer parsing, it does not affect the parsing of value, so this is possible again: ``` MY_VAR="#foo" ``` Closes coop-cloud/organising#535
277 lines
6.4 KiB
Go
277 lines
6.4 KiB
Go
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io/fs"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.coopcloud.tech/coop-cloud/godotenv"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// getBaseDir retrieves the Abra base directory.
|
|
func getBaseDir() string {
|
|
home := os.ExpandEnv("$HOME/.abra")
|
|
if customAbraDir, exists := os.LookupEnv("ABRA_DIR"); exists && customAbraDir != "" {
|
|
home = customAbraDir
|
|
}
|
|
return home
|
|
}
|
|
|
|
var ABRA_DIR = getBaseDir()
|
|
var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
|
|
var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
|
|
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
|
|
var BACKUP_DIR = path.Join(ABRA_DIR, "backups")
|
|
var CATALOGUE_DIR = path.Join(ABRA_DIR, "catalogue")
|
|
var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json")
|
|
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
|
|
var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
|
|
var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
|
|
|
|
// envVarModifiers is a list of env var modifier strings. These are added to
|
|
// env vars as comments and modify their processing by Abra, e.g. determining
|
|
// how long secrets should be.
|
|
var envVarModifiers = []string{"length"}
|
|
|
|
// GetServers retrieves all servers.
|
|
func GetServers() ([]string, error) {
|
|
var servers []string
|
|
|
|
servers, err := GetAllFoldersInDirectory(SERVERS_DIR)
|
|
if err != nil {
|
|
return servers, err
|
|
}
|
|
|
|
logrus.Debugf("retrieved %v servers: %s", len(servers), servers)
|
|
|
|
return servers, nil
|
|
}
|
|
|
|
// ReadEnv loads an app envivornment into a map.
|
|
func ReadEnv(filePath string) (AppEnv, error) {
|
|
var envVars AppEnv
|
|
|
|
envVars, _, err := godotenv.Read(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logrus.Debugf("read %s from %s", envVars, filePath)
|
|
|
|
return envVars, nil
|
|
}
|
|
|
|
// ReadEnv loads an app envivornment and their modifiers in two different maps.
|
|
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
|
|
var envVars AppEnv
|
|
|
|
envVars, mods, err := godotenv.Read(filePath)
|
|
if err != nil {
|
|
return nil, mods, err
|
|
}
|
|
|
|
logrus.Debugf("read %s from %s", envVars, filePath)
|
|
|
|
return envVars, mods, nil
|
|
}
|
|
|
|
// ReadServerNames retrieves all server names.
|
|
func ReadServerNames() ([]string, error) {
|
|
serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logrus.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR)
|
|
|
|
return serverNames, nil
|
|
}
|
|
|
|
// GetAllFilesInDirectory returns filenames of all files in directory
|
|
func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
|
|
var realFiles []fs.FileInfo
|
|
|
|
files, err := ioutil.ReadDir(directory)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, file := range files {
|
|
// Follow any symlinks
|
|
filePath := path.Join(directory, file.Name())
|
|
|
|
if filepath.Ext(strings.TrimSpace(filePath)) != ".env" {
|
|
continue
|
|
}
|
|
|
|
realPath, err := filepath.EvalSymlinks(filePath)
|
|
if err != nil {
|
|
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
|
|
} else {
|
|
realFile, err := os.Stat(realPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !realFile.IsDir() {
|
|
realFiles = append(realFiles, file)
|
|
}
|
|
}
|
|
}
|
|
|
|
return realFiles, nil
|
|
}
|
|
|
|
// GetAllFoldersInDirectory returns both folder and symlink paths
|
|
func GetAllFoldersInDirectory(directory string) ([]string, error) {
|
|
var folders []string
|
|
|
|
files, err := ioutil.ReadDir(directory)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(files) == 0 {
|
|
return nil, fmt.Errorf("directory is empty: %s", directory)
|
|
}
|
|
|
|
for _, file := range files {
|
|
// Check if file is directory or symlink
|
|
if file.IsDir() || file.Mode()&fs.ModeSymlink != 0 {
|
|
filePath := path.Join(directory, file.Name())
|
|
realDir, err := filepath.EvalSymlinks(filePath)
|
|
if err != nil {
|
|
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
|
|
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
|
|
// path is a directory
|
|
folders = append(folders, file.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
return folders, nil
|
|
}
|
|
|
|
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
|
|
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
|
|
envVars := make(map[string]string)
|
|
|
|
file, err := os.Open(abraSh)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return envVars, nil
|
|
}
|
|
return envVars, err
|
|
}
|
|
defer file.Close()
|
|
|
|
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
|
|
if err != nil {
|
|
return envVars, err
|
|
}
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
txt := scanner.Text()
|
|
if exportRegex.MatchString(txt) {
|
|
splitVals := strings.Split(txt, "export ")
|
|
envVarDef := splitVals[len(splitVals)-1]
|
|
keyVal := strings.Split(envVarDef, "=")
|
|
if len(keyVal) != 2 {
|
|
return envVars, fmt.Errorf("couldn't parse %s", txt)
|
|
}
|
|
envVars[keyVal[0]] = keyVal[1]
|
|
}
|
|
}
|
|
|
|
if len(envVars) > 0 {
|
|
logrus.Debugf("read %s from %s", envVars, abraSh)
|
|
} else {
|
|
logrus.Debugf("read 0 env var exports from %s", abraSh)
|
|
}
|
|
|
|
return envVars, nil
|
|
}
|
|
|
|
type EnvVar struct {
|
|
Name string
|
|
Present bool
|
|
}
|
|
|
|
func CheckEnv(app App) ([]EnvVar, error) {
|
|
var envVars []EnvVar
|
|
|
|
envSamplePath := path.Join(RECIPES_DIR, app.Recipe, ".env.sample")
|
|
if _, err := os.Stat(envSamplePath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return envVars, fmt.Errorf("%s does not exist?", envSamplePath)
|
|
}
|
|
return envVars, err
|
|
}
|
|
|
|
envSample, err := ReadEnv(envSamplePath)
|
|
if err != nil {
|
|
return envVars, err
|
|
}
|
|
|
|
var keys []string
|
|
for key := range envSample {
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
for _, key := range keys {
|
|
if _, ok := app.Env[key]; ok {
|
|
envVars = append(envVars, EnvVar{Name: key, Present: true})
|
|
} else {
|
|
envVars = append(envVars, EnvVar{Name: key, Present: false})
|
|
}
|
|
}
|
|
|
|
return envVars, nil
|
|
}
|
|
|
|
// ReadAbraShCmdNames reads the names of commands.
|
|
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
|
|
var cmdNames []string
|
|
|
|
file, err := os.Open(abraSh)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return cmdNames, nil
|
|
}
|
|
return cmdNames, err
|
|
}
|
|
defer file.Close()
|
|
|
|
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
|
|
if err != nil {
|
|
return cmdNames, err
|
|
}
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
matches := cmdNameRegex.FindStringSubmatch(line)
|
|
if len(matches) > 0 {
|
|
cmdNames = append(cmdNames, matches[1])
|
|
}
|
|
}
|
|
|
|
if len(cmdNames) > 0 {
|
|
logrus.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
|
|
} else {
|
|
logrus.Debugf("read 0 command names from %s", abraSh)
|
|
}
|
|
|
|
return cmdNames, nil
|
|
}
|