-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathconfig.go
More file actions
288 lines (242 loc) · 8.32 KB
/
config.go
File metadata and controls
288 lines (242 loc) · 8.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
package config
import (
"context"
"fmt"
"path/filepath"
"regexp"
"runtime/debug"
"strings"
"time"
"github.com/UpCloudLtd/upcloud-cli/v3/internal/clierrors"
internal "github.com/UpCloudLtd/upcloud-cli/v3/internal/service"
"github.com/zalando/go-keyring"
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/client"
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/service"
"github.com/adrg/xdg"
"github.com/gemalto/flume"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const (
// KeyClientTimeout defines the viper configuration key used to define client timeout
KeyClientTimeout = "client-timeout"
// KeyOutput defines the viper configuration key used to define the output
KeyOutput = "output"
// ValueOutputHuman defines the viper configuration value used to define human-readable output
ValueOutputHuman = "human"
// ValueOutputYAML defines the viper configuration value used to define YAML output
ValueOutputYAML = "yaml"
// ValueOutputJSON defines the viper configuration value used to define JSON output
ValueOutputJSON = "json"
// env vars custom prefix
envPrefix = "UPCLOUD"
// keyringServiceName is the name of the service to use when using the system keyring
keyringServiceName = "UpCloud"
)
var (
// Version contains the current version.
Version = "dev"
// BuildDate contains a string with the build date.
BuildDate = "unknown"
// flume logger for config, that will be passed to log pckg
logger = flume.New("config")
)
// New returns a new instance of Config bound to the given viper instance
func New() *Config {
ctx, cancel := context.WithCancel(context.Background())
return &Config{viper: viper.New(), context: ctx, cancel: cancel}
}
// GlobalFlags holds information on the flags shared among all commands
type GlobalFlags struct {
ConfigFile string `valid:"-"`
ClientTimeout time.Duration `valid:"-"`
Debug bool `valid:"-"`
OutputFormat string `valid:"in(human|json|yaml)"`
NoColours OptionalBoolean
ForceColours OptionalBoolean
}
// Config holds the configuration for running upctl
type Config struct {
viper *viper.Viper
flagSet *pflag.FlagSet
cancel context.CancelFunc
context context.Context //nolint: containedctx // This is where the top-level context is stored
GlobalFlags GlobalFlags
}
// Load loads config and sets up service
func (s *Config) Load() error {
v := s.Viper()
v.SetEnvPrefix(envPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
v.AutomaticEnv()
v.SetConfigName("upctl")
v.SetConfigType("yaml")
configFile := s.GlobalFlags.ConfigFile
if configFile != "" {
v.SetConfigFile(configFile)
} else {
// Support XDG default config home dir and common config dirs
v.AddConfigPath(xdg.ConfigHome)
v.AddConfigPath("$HOME/.config") // for MacOS as XDG config is not common
}
// Attempt to read the config file, ignoring only config file not found errors
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return fmt.Errorf("unable to parse config from file '%v': %w", v.ConfigFileUsed(), err)
}
}
// If no credentials are provided, check if token is stored in keyring
if v.GetString("token") == "" && v.GetString("username") == "" && v.GetString("password") == "" {
token, err := keyring.Get(keyringServiceName, "")
if err == nil {
if err := v.MergeConfigMap(map[string]interface{}{"token": token}); err != nil {
return fmt.Errorf("unable to merge token from keyring: %w", err)
}
}
}
// If only username is provided, check if password is stored in keyring
if v.GetString("username") != "" && v.GetString("token") == "" && v.GetString("password") == "" {
password, err := keyring.Get(keyringServiceName, v.GetString("username"))
if err == nil {
if err := v.MergeConfigMap(map[string]interface{}{"password": password}); err != nil {
return fmt.Errorf("unable to merge password from keyring: %w", err)
}
}
}
v.Set("config", v.ConfigFileUsed())
settings := v.AllSettings()
// sanitize password before logging settings
if _, ok := settings["password"]; ok {
settings["password"] = "..."
}
logger.Debug("viper initialized", "settings", settings)
return nil
}
// Viper returns a reference to the viper instance
func (s *Config) Viper() *viper.Viper {
return s.viper
}
// IsSet return true if the key is set in the current namespace
func (s *Config) IsSet(key string) bool {
return s.viper.IsSet(key)
}
// Get return the value of the key in the current namespace
func (s *Config) Get(key string) interface{} {
return s.viper.Get(key)
}
// GetString is a convenience method of getting a configuration value in the current namespace as a string
func (s *Config) GetString(key string) string {
return s.viper.GetString(key)
}
// FlagByKey returns pflag.Flag associated with a key in config
func (s *Config) FlagByKey(key string) *pflag.Flag {
if s.flagSet == nil {
s.flagSet = &pflag.FlagSet{}
}
return s.flagSet.Lookup(key)
}
// BoundFlags returns the list of all the flags given to the config
func (s *Config) BoundFlags() []*pflag.Flag {
if s.flagSet == nil {
s.flagSet = &pflag.FlagSet{}
}
var r []*pflag.Flag
s.flagSet.VisitAll(func(flag *pflag.Flag) {
r = append(r, flag)
})
return r
}
// ConfigBindFlagSet sets the config flag set and binds them to the viper instance
func (s *Config) ConfigBindFlagSet(flags *pflag.FlagSet) {
if flags == nil {
panic("Nil flagset")
}
flags.VisitAll(func(flag *pflag.Flag) {
_ = s.viper.BindPFlag(flag.Name, flag)
// s.flagSet.AddFlag(flag)
})
}
// Commonly used keys as accessors
// Output is a convenience method for getting the user specified output
func (s *Config) Output() string {
return s.viper.GetString(KeyOutput)
}
// OutputHuman is a convenience method that returns true if the user specified human-readable output
func (s *Config) OutputHuman() bool {
return s.Output() == ValueOutputHuman
}
// ClientTimeout is a convenience method that returns the user specified client timeout
func (s *Config) ClientTimeout() time.Duration {
return s.viper.GetDuration(KeyClientTimeout)
}
func (s *Config) Cancel() {
s.cancel()
}
func (s *Config) Context() context.Context {
return s.context
}
// CreateService creates a new service instance and puts in the conf struct
func (s *Config) CreateService() (internal.AllServices, error) {
username := s.GetString("username")
password := s.GetString("password")
token := s.GetString("token")
if token == "" && (username == "" || password == "") {
// This might give silghtly unexpected results on OS X, as xdg.ConfigHome points to ~/Library/Application Support
// while we really use/prefer/document ~/.config - which does work on osx as well but won't be displayed here.
configDetails := fmt.Sprintf("default location %s", filepath.Join(xdg.ConfigHome, "upctl.yaml"))
if s.GetString("config") != "" {
configDetails = fmt.Sprintf("used %s", s.GetString("config"))
}
return nil, clierrors.MissingCredentialsError{ConfigFile: configDetails, ServiceName: keyringServiceName}
}
configs := []client.ConfigFn{
client.WithTimeout(s.ClientTimeout()),
}
if token != "" {
configs = append(configs, client.WithBearerAuth(token))
} else {
configs = append(configs, client.WithBasicAuth(username, password))
}
client := client.New("", "", configs...)
client.UserAgent = fmt.Sprintf("upctl/%s", GetVersion())
svc := service.New(client)
return svc, nil
}
func GetVersion() string {
version := getVersion()
re := regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+.*`)
if re.MatchString(version) {
return version[1:]
}
return version
}
func SaveTokenToKeyring(token string) error {
return keyring.Set(keyringServiceName, "", token)
}
func getVersion() string {
// Version was overridden during the build
if Version != "dev" {
return Version
}
// Try to read version from build info
if buildInfo, ok := debug.ReadBuildInfo(); ok {
version := buildInfo.Main.Version
if version != "(devel)" && version != "" {
return version
}
settingsMap := make(map[string]string)
for _, setting := range buildInfo.Settings {
settingsMap[setting.Key] = setting.Value
}
version = "dev"
if rev, ok := settingsMap["vcs.revision"]; ok {
version = fmt.Sprintf("%s-%s", version, rev[:8])
}
if dirty, ok := settingsMap["vcs.modified"]; ok && dirty == "true" {
return version + "-dirty"
}
return version
}
// Fallback to the default value
return Version
}