Skip to content

Commit

Permalink
Merge pull request #324 from roots/cli-config-refactor
Browse files Browse the repository at this point in the history
CLI config refactor and improvements
  • Loading branch information
swalkinshaw authored Oct 10, 2022
2 parents 561fdee + c2611b7 commit e4bd4a9
Show file tree
Hide file tree
Showing 17 changed files with 477 additions and 140 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,54 @@ Supported commands so far:
| `valet` | Commands for Laravel Valet |
| `vault` | Commands for Ansible Vault |
## Configuration
There are three ways to set configuration settings for trellis-cli and they are
loaded in this order of precedence:
1. global config
2. project config
3. env variables
The global CLI config (defaults to `$HOME/.config/trellis/cli.yml`)
and will be loaded first (if it exists).
Next, if a project is detected, the project CLI config will be loaded if it
exists at `.trellis/cli.yml`.
Finally, env variables prefixed with `TRELLIS_` will be used as
overrides if they match a supported configuration setting. The prefix will be
stripped and the rest is lowercased to determine the setting key.
Note: only string, numeric, and boolean values are supported when using environment
variables.
Current supported settings:
| Setting | Description | Type | Default |
| --- | --- | -- | -- |
| `ask_vault_pass` | Set Ansible to always ask for the vault pass | boolean | false |
| `check_for_updates` | Whether to check for new versions of trellis-cli | boolean | true |
| `load_plugins` | Load external CLI plugins | boolean | true |
| `open` | List of name -> URL shortcuts | map[string]string | none |
| `virtualenv_integration` | Enable automated virtualenv integration | boolean | true |
Example config:
```yaml
ask_vault_pass: false
check_for_updates: true
load_plugins: true
open:
site: "https://mysite.com"
admin: "https://mysite.com/wp/wp-admin"
virtualenv_integration: true
```
Example env var usage:
```bash
TRELLIS_ASK_VAULT_PASS=true trellis provision production
```
## Development
trellis-cli requires Go >= 1.18 (`brew install go` on macOS)
Expand Down
52 changes: 52 additions & 0 deletions app_paths/app_paths.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package app_paths

import (
"os"
"path/filepath"
"runtime"
)

const (
appData = "AppData"
trellisConfigDir = "TRELLIS_CONFIG_DIR"
localAppData = "LocalAppData"
xdgCacheHome = "XDG_CACHE_HOME"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgDataHome = "XDG_DATA_HOME"
)

// Config path precedence: TRELLIS_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME.
func ConfigDir() string {
var path string

if a := os.Getenv(trellisConfigDir); a != "" {
path = a
} else if b := os.Getenv(xdgConfigHome); b != "" {
path = filepath.Join(b, "trellis")
} else if c := os.Getenv(appData); runtime.GOOS == "windows" && c != "" {
path = filepath.Join(c, "Trellis CLI")
} else {
d, _ := os.UserHomeDir()
path = filepath.Join(d, ".config", "trellis")
}

return path
}

func ConfigPath(path string) string {
return filepath.Join(ConfigDir(), path)
}

// Cache path precedence: XDG_CACHE_HOME, LocalAppData (windows only), HOME.
func CacheDir() string {
var path string
if a := os.Getenv(xdgCacheHome); a != "" {
path = filepath.Join(a, "trellis")
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
path = filepath.Join(b, "Trellis CLI")
} else {
c, _ := os.UserHomeDir()
path = filepath.Join(c, ".local", "state", "trellis")
}
return path
}
101 changes: 101 additions & 0 deletions cli_config/cli_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package cli_config

import (
"errors"
"fmt"
"os"
"reflect"
"strconv"
"strings"

"gopkg.in/yaml.v2"
)

type Config struct {
AskVaultPass bool `yaml:"ask_vault_pass"`
CheckForUpdates bool `yaml:"check_for_updates"`
LoadPlugins bool `yaml:"load_plugins"`
Open map[string]string `yaml:"open"`
VirtualenvIntegration bool `yaml:"virtualenv_integration"`
}

var (
ErrUnsupportedType = errors.New("Invalid env var config setting: value is an unsupported type.")
ErrCouldNotParse = errors.New("Invalid env var config setting: failed to parse value")
)

func NewConfig(defaultConfig Config) Config {
return defaultConfig
}

func (c *Config) LoadFile(path string) error {
configYaml, err := os.ReadFile(path)

if err != nil && !os.IsNotExist(err) {
return err
}

if err := yaml.Unmarshal(configYaml, &c); err != nil {
return err
}

return nil
}

func (c *Config) LoadEnv(prefix string) error {
structType := reflect.ValueOf(c).Elem()
fields := reflect.VisibleFields(structType.Type())

for _, env := range os.Environ() {
parts := strings.Split(env, "=")
originalKey := parts[0]
value := parts[1]

key := strings.TrimPrefix(originalKey, prefix)

if originalKey == key {
// key is unchanged and didn't start with prefix
continue
}

for _, field := range fields {
if strings.ToLower(key) == field.Tag.Get("yaml") {
structValue := structType.FieldByName(field.Name)

if !structValue.CanSet() {
continue
}

switch field.Type.Kind() {
case reflect.Bool:
val, err := strconv.ParseBool(value)

if err != nil {
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as a boolean", ErrCouldNotParse, env, value)
}

structValue.SetBool(val)
case reflect.Int:
val, err := strconv.ParseInt(value, 10, 32)

if err != nil {
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as an integer", ErrCouldNotParse, env, value)
}

structValue.SetInt(val)
case reflect.Float32:
val, err := strconv.ParseFloat(value, 32)
if err != nil {
return fmt.Errorf("%w '%s'\n'%s' can't be parsed as a float", ErrCouldNotParse, env, value)
}

structValue.SetFloat(val)
default:
return fmt.Errorf("%w\n%s setting of type %s is unsupported.", ErrUnsupportedType, env, field.Type.String())
}
}
}
}

return nil
}
107 changes: 107 additions & 0 deletions cli_config/cli_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package cli_config

import (
_ "fmt"
"os"
"path/filepath"
"strings"
"testing"
)

func TestLoadFile(t *testing.T) {
conf := Config{
AskVaultPass: false,
LoadPlugins: true,
}

dir := t.TempDir()
path := filepath.Join(dir, "cli.yml")
content := `
ask_vault_pass: true
open:
roots: https://roots.io
`

if err := os.WriteFile(path, []byte(content), os.ModePerm); err != nil {
t.Fatal(err)
}

conf.LoadFile(path)

if conf.LoadPlugins != true {
t.Errorf("expected LoadPlugins to be true (default value)")
}

if conf.AskVaultPass != true {
t.Errorf("expected AskVaultPass to be true")
}

open := conf.Open["roots"]
expected := "https://roots.io"

if open != expected {
t.Errorf("expected open to be %s, got %s", expected, open)
}
}

func TestLoadEnv(t *testing.T) {
t.Setenv("TRELLIS_ASK_VAULT_PASS", "true")
t.Setenv("TRELLIS_NOPE", "foo")
t.Setenv("ASK_VAULT_PASS", "false")

conf := Config{
AskVaultPass: false,
}

conf.LoadEnv("TRELLIS_")

if conf.AskVaultPass != true {
t.Errorf("expected AskVaultPass to be true")
}
}

func TestLoadBoolParseError(t *testing.T) {
t.Setenv("TRELLIS_ASK_VAULT_PASS", "foo")

conf := Config{}

err := conf.LoadEnv("TRELLIS_")

if err == nil {
t.Errorf("expected LoadEnv to return an error")
}

msg := err.Error()

expected := `
Invalid env var config setting: failed to parse value 'TRELLIS_ASK_VAULT_PASS=foo'
'foo' can't be parsed as a boolean
`

if msg != strings.TrimSpace(expected) {
t.Errorf("expected error %s got %s", expected, msg)
}
}

func TestLoadEnvUnsupportedType(t *testing.T) {
t.Setenv("TRELLIS_OPEN", "foo")

conf := Config{}

err := conf.LoadEnv("TRELLIS_")

if err == nil {
t.Errorf("expected LoadEnv to return an error")
}

msg := err.Error()

expected := `
Invalid env var config setting: value is an unsupported type.
TRELLIS_OPEN=foo setting of type map[string]string is unsupported.
`

if msg != strings.TrimSpace(expected) {
t.Errorf("expected error %s got %s", expected, msg)
}
}
3 changes: 1 addition & 2 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,5 @@ func virtualenvError(ui cli.Ui) {
ui.Error(" 1. Ensure Python 3 is installed and the `python3` command works. trellis-cli will use python3's built-in venv feature.")
ui.Error(" Ubuntu/Debian users (including Windows WSL): venv is not built-in, to install it run `sudo apt-get install python3-pip python3-venv`")
ui.Error("")
ui.Error(" 2. Disable trellis-cli's virtual env feature, and manage dependencies manually, by setting this env variable:")
ui.Error(fmt.Sprintf(" export %s=false", trellis.TrellisVenvEnvName))
ui.Error(" 2. Disable trellis-cli's virtualenv feature, and manage dependencies manually, by changing the 'virtualenv_integration' configuration setting to 'false'.")
}
7 changes: 0 additions & 7 deletions config/config.go

This file was deleted.

1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ require (
github.com/mholt/archiver v3.1.1+incompatible
github.com/mitchellh/cli v1.1.4
github.com/mitchellh/go-homedir v1.1.0
github.com/muesli/go-app-paths v0.2.2
github.com/posener/complete v1.2.3
github.com/theckman/yacspin v0.13.12
github.com/weppos/publicsuffix-go v0.20.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,6 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI=
github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
Expand Down
Loading

0 comments on commit e4bd4a9

Please sign in to comment.