file.go raw

   1  package credentials
   2  
   3  import (
   4  	"fmt"
   5  	"os"
   6  	"os/user"
   7  	"path"
   8  
   9  	"github.com/spf13/viper"
  10  )
  11  
  12  type FileOpt func(*FileProvider)
  13  
  14  // FileOptWithFilename returns a FileOpt overriding the default filename.
  15  func FileOptWithFilename(filename string) FileOpt {
  16  	return func(f *FileProvider) {
  17  		f.filename = filename
  18  	}
  19  }
  20  
  21  // FileOptWithAccount returns a FileOpt overriding the default account.
  22  func FileOptWithAccount(account string) FileOpt {
  23  	return func(f *FileProvider) {
  24  		f.account = account
  25  	}
  26  }
  27  
  28  type FileProvider struct {
  29  	filename  string
  30  	account   string
  31  	retrieved bool
  32  
  33  	// TODO: export some fields from the config like: default zone...etc.
  34  }
  35  
  36  func NewFileCredentials(opts ...FileOpt) *Credentials {
  37  	fp := &FileProvider{}
  38  	for _, opt := range opts {
  39  		opt(fp)
  40  	}
  41  	return NewCredentials(fp)
  42  }
  43  
  44  func (f *FileProvider) Retrieve() (Value, error) {
  45  	f.retrieved = false
  46  
  47  	viperConf, err := f.retrieveViperConfig()
  48  	if err != nil {
  49  		return Value{}, err
  50  	}
  51  
  52  	if err := viperConf.ReadInConfig(); err != nil {
  53  		return Value{}, err
  54  	}
  55  
  56  	config := Config{}
  57  	if err := viperConf.Unmarshal(&config); err != nil {
  58  		return Value{}, fmt.Errorf("file provider: couldn't read config: %w", err)
  59  	}
  60  
  61  	if len(config.Accounts) == 0 {
  62  		return Value{}, fmt.Errorf("file provider: no accounts were found into %q", viper.ConfigFileUsed())
  63  	}
  64  
  65  	if f.account == "" && config.DefaultAccount == "" {
  66  		return Value{}, fmt.Errorf("file provider: no account defined")
  67  	}
  68  
  69  	accountName := config.DefaultAccount
  70  	if f.account != "" {
  71  		accountName = f.account
  72  	}
  73  
  74  	account := Account{}
  75  	for i, a := range config.Accounts {
  76  		if a.Name == accountName {
  77  			account = config.Accounts[i]
  78  			break
  79  		}
  80  	}
  81  
  82  	v := Value{
  83  		APIKey:    account.Key,
  84  		APISecret: account.Secret,
  85  	}
  86  
  87  	if !v.IsSet() {
  88  		return Value{}, fmt.Errorf("file provider: account %q: %w", accountName, ErrMissingIncomplete)
  89  	}
  90  
  91  	f.retrieved = true
  92  
  93  	return v, nil
  94  }
  95  
  96  // IsExpired returns if the shared credentials have expired.
  97  func (f *FileProvider) IsExpired() bool {
  98  	return !f.retrieved
  99  }
 100  
 101  type Account struct {
 102  	Name                string
 103  	Account             string
 104  	SosEndpoint         string
 105  	Environment         string
 106  	Key                 string
 107  	Secret              string
 108  	SecretCommand       []string
 109  	DefaultZone         string
 110  	DefaultSSHKey       string
 111  	DefaultTemplate     string
 112  	DefaultOutputFormat string
 113  	ClientTimeout       int
 114  	CustomHeaders       map[string]string
 115  }
 116  
 117  type Config struct {
 118  	DefaultAccount      string
 119  	DefaultOutputFormat string
 120  	Accounts            []Account
 121  }
 122  
 123  func (f *FileProvider) retrieveViperConfig() (*viper.Viper, error) {
 124  	config := viper.New()
 125  
 126  	if f.filename != "" {
 127  		config.SetConfigFile(f.filename)
 128  		return config, nil
 129  	}
 130  
 131  	cfgdir, err := os.UserConfigDir()
 132  	if err != nil {
 133  		return nil, fmt.Errorf("could not find configuration directory: %s", err)
 134  	}
 135  
 136  	usr, err := user.Current()
 137  	if err != nil {
 138  		return nil, err
 139  	}
 140  
 141  	config.SetConfigName("exoscale")
 142  	config.SetConfigType("toml")
 143  	config.AddConfigPath(path.Join(cfgdir, "exoscale"))
 144  	config.AddConfigPath(path.Join(usr.HomeDir, ".exoscale"))
 145  	config.AddConfigPath(usr.HomeDir)
 146  	config.AddConfigPath(".")
 147  
 148  	return config, nil
 149  }
 150