-
Notifications
You must be signed in to change notification settings - Fork 44
/
Copy pathconfig.go
359 lines (304 loc) · 12.5 KB
/
config.go
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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package scw
import (
"bytes"
goerrors "errors"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/scaleway/scaleway-sdk-go/errors"
"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/logger"
"gopkg.in/yaml.v2"
)
const (
documentationLink = "https://github.com/scaleway/scaleway-sdk-go/blob/master/scw/README.md"
defaultConfigPermission = 0o600
// Reserved name for the default profile.
DefaultProfileName = "default"
)
const configFileTemplate = `# Scaleway configuration file
# https://github.com/scaleway/scaleway-sdk-go/tree/master/scw#scaleway-config
# This configuration file can be used with:
# - Scaleway SDK Go (https://github.com/scaleway/scaleway-sdk-go)
# - Scaleway CLI (>2.0.0) (https://github.com/scaleway/scaleway-cli)
# - Scaleway Terraform Provider (https://www.terraform.io/docs/providers/scaleway/index.html)
# You need an access key and a secret key to connect to Scaleway API.
# Generate your token at the following address: https://console.scaleway.com/iam/api-keys
# An access key is a secret key identifier.
{{ if .AccessKey }}access_key: {{.AccessKey}}{{ else }}# access_key: SCW11111111111111111{{ end }}
# The secret key is the value that can be used to authenticate against the API (the value used in X-Auth-Token HTTP-header).
# The secret key MUST remain secret and not given to anyone or published online.
{{ if .SecretKey }}secret_key: {{ .SecretKey }}{{ else }}# secret_key: 11111111-1111-1111-1111-111111111111{{ end }}
# Your organization ID is the identifier of your account inside Scaleway infrastructure.
{{ if .DefaultOrganizationID }}default_organization_id: {{ .DefaultOrganizationID }}{{ else }}# default_organization_id: 11111111-1111-1111-1111-111111111111{{ end }}
# Your project ID is the identifier of the project your resources are attached to (beta).
{{ if .DefaultProjectID }}default_project_id: {{ .DefaultProjectID }}{{ else }}# default_project_id: 11111111-1111-1111-1111-111111111111{{ end }}
# A region is represented as a geographical area such as France (Paris) or the Netherlands (Amsterdam).
# It can contain multiple availability zones.
# Example of region: fr-par, nl-ams
{{ if .DefaultRegion }}default_region: {{ .DefaultRegion }}{{ else }}# default_region: fr-par{{ end }}
# A region can be split into many availability zones (AZ).
# Latency between multiple AZ of the same region are low as they have a common network layer.
# Example of zones: fr-par-1, nl-ams-1
{{ if .DefaultZone }}default_zone: {{.DefaultZone}}{{ else }}# default_zone: fr-par-1{{ end }}
# APIURL overrides the API URL of the Scaleway API to the given URL.
# Change that if you want to direct requests to a different endpoint.
{{ if .APIURL }}apiurl: {{ .APIURL }}{{ else }}# api_url: https://api.scaleway.com{{ end }}
# Insecure enables insecure transport on the client.
# Default to false
{{ if .Insecure }}insecure: {{ .Insecure }}{{ else }}# insecure: false{{ end }}
# A configuration is a named set of Scaleway properties.
# Starting off with a Scaleway SDK or Scaleway CLI, you’ll work with a single configuration named default.
# You can set properties of the default profile by running either scw init or scw config set.
# This single default configuration is suitable for most use cases.
{{ if .ActiveProfile }}active_profile: {{ .ActiveProfile }}{{ else }}# active_profile: myProfile{{ end }}
# To improve the Scaleway CLI we rely on diagnostic and usage data.
# Sending such data is optional and can be disable at any time by setting send_telemetry variable to false.
{{ if .SendTelemetry }}send_telemetry: {{ .SendTelemetry }}{{ else }}# send_telemetry: false{{ end }}
# To work with multiple projects or authorization accounts, you can set up multiple configurations with scw config configurations create and switch among them accordingly.
# You can use a profile by either:
# - Define the profile you want to use as the SCW_PROFILE environment variable
# - Use the GetActiveProfile() function in the SDK
# - Use the --profile flag with the CLI
# You can define a profile using the following syntax:
{{ if gt (len .Profiles) 0 }}
profiles:
{{- range $k,$v := .Profiles }}
{{ $k }}:
{{ if $v.AccessKey }}access_key: {{ $v.AccessKey }}{{ else }}# access_key: SCW11111111111111111{{ end }}
{{ if $v.SecretKey }}secret_key: {{ $v.SecretKey }}{{ else }}# secret_key: 11111111-1111-1111-1111-111111111111{{ end }}
{{ if $v.DefaultOrganizationID }}default_organization_id: {{ $v.DefaultOrganizationID }}{{ else }}# default_organization_id: 11111111-1111-1111-1111-111111111111{{ end }}
{{ if $v.DefaultProjectID }}default_project_id: {{ $v.DefaultProjectID }}{{ else }}# default_project_id: 11111111-1111-1111-1111-111111111111{{ end }}
{{ if $v.DefaultZone }}default_zone: {{ $v.DefaultZone }}{{ else }}# default_zone: fr-par-1{{ end }}
{{ if $v.DefaultRegion }}default_region: {{ $v.DefaultRegion }}{{ else }}# default_region: fr-par{{ end }}
{{ if $v.APIURL }}api_url: {{ $v.APIURL }}{{ else }}# api_url: https://api.scaleway.com{{ end }}
{{ if $v.Insecure }}insecure: {{ $v.Insecure }}{{ else }}# insecure: false{{ end }}
{{ end }}
{{- else }}
# profiles:
# myProfile:
# access_key: 11111111-1111-1111-1111-111111111111
# secret_key: 11111111-1111-1111-1111-111111111111
# default_organization_id: 11111111-1111-1111-1111-111111111111
# default_project_id: 11111111-1111-1111-1111-111111111111
# default_zone: fr-par-1
# default_region: fr-par
# api_url: https://api.scaleway.com
# insecure: false
{{ end -}}
`
type Config struct {
Profile `yaml:",inline"`
ActiveProfile *string `yaml:"active_profile,omitempty" json:"active_profile,omitempty"`
Profiles map[string]*Profile `yaml:"profiles,omitempty" json:"profiles,omitempty"`
}
type Profile struct {
AccessKey *string `yaml:"access_key,omitempty" json:"access_key,omitempty"`
SecretKey *string `yaml:"secret_key,omitempty" json:"secret_key,omitempty"`
APIURL *string `yaml:"api_url,omitempty" json:"api_url,omitempty"`
Insecure *bool `yaml:"insecure,omitempty" json:"insecure,omitempty"`
DefaultOrganizationID *string `yaml:"default_organization_id,omitempty" json:"default_organization_id,omitempty"`
DefaultProjectID *string `yaml:"default_project_id,omitempty" json:"default_project_id,omitempty"`
DefaultRegion *string `yaml:"default_region,omitempty" json:"default_region,omitempty"`
DefaultZone *string `yaml:"default_zone,omitempty" json:"default_zone,omitempty"`
SendTelemetry *bool `yaml:"send_telemetry,omitempty" json:"send_telemetry,omitempty"`
}
func (p *Profile) String() string {
p2 := *p
p2.SecretKey = hideSecretKey(p2.SecretKey)
configRaw, _ := yaml.Marshal(p2)
return string(configRaw)
}
// clone deep copy config object
func (c *Config) clone() *Config {
c2 := &Config{}
configRaw, _ := yaml.Marshal(c)
_ = yaml.Unmarshal(configRaw, c2)
return c2
}
func (c *Config) String() string {
c2 := c.clone()
c2.SecretKey = hideSecretKey(c2.SecretKey)
for _, p := range c2.Profiles {
p.SecretKey = hideSecretKey(p.SecretKey)
}
configRaw, _ := yaml.Marshal(c2)
return string(configRaw)
}
func (c *Config) IsEmpty() bool {
return c.String() == "{}\n"
}
func hideSecretKey(key *string) *string {
if key == nil {
return nil
}
newKey := auth.HideSecretKey(*key)
return &newKey
}
func unmarshalConfV2(content []byte) (*Config, error) {
var config Config
err := yaml.Unmarshal(content, &config)
if err != nil {
return nil, err
}
return &config, nil
}
// MustLoadConfig is like LoadConfig but panic instead of returning an error.
func MustLoadConfig() *Config {
c, err := LoadConfigFromPath(GetConfigPath())
if err != nil {
panic(err)
}
return c
}
// LoadConfig read the config from the default path.
func LoadConfig() (*Config, error) {
configPath := GetConfigPath()
cfg, err := LoadConfigFromPath(configPath)
// Special case if using default config path
// if config.yaml does not exist, we should try to read config.yml
if os.Getenv(ScwConfigPathEnv) == "" {
var configNotFoundError *ConfigFileNotFoundError
if err != nil && goerrors.As(err, &configNotFoundError) && strings.HasSuffix(configPath, ".yaml") {
configPath = strings.TrimSuffix(configPath, ".yaml") + ".yml"
cfgYml, errYml := LoadConfigFromPath(configPath)
// If .yml config is not found, return first error when reading .yaml
if errYml == nil || (errYml != nil && !goerrors.As(errYml, &configNotFoundError)) {
return cfgYml, errYml
}
}
}
return cfg, err
}
// LoadConfigFromPath read the config from the given path.
func LoadConfigFromPath(path string) (*Config, error) {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return nil, configFileNotFound(path)
}
if err != nil {
return nil, err
}
file, err := os.ReadFile(path)
if err != nil {
return nil, errors.Wrap(err, "cannot read config file")
}
confV2, err := unmarshalConfV2(file)
if err != nil {
return nil, errors.Wrap(err, "content of config file %s is invalid", path)
}
return confV2, nil
}
// GetProfile returns the profile corresponding to the given profile name.
func (c *Config) GetProfile(profileName string) (*Profile, error) {
if profileName == "" {
return nil, errors.New("profileName cannot be empty")
}
if profileName == DefaultProfileName {
return &c.Profile, nil
}
p, exist := c.Profiles[profileName]
if !exist {
return nil, errors.New("given profile %s does not exist", profileName)
}
// Merge selected profile on top of default profile
return MergeProfiles(&c.Profile, p), nil
}
// GetActiveProfile returns the active profile of the config based on the following order:
// env SCW_PROFILE > config active_profile > config root profile
func (c *Config) GetActiveProfile() (*Profile, error) {
switch {
case os.Getenv(ScwActiveProfileEnv) != "":
logger.Debugf("using active profile from env: %s=%s\n", ScwActiveProfileEnv, os.Getenv(ScwActiveProfileEnv))
return c.GetProfile(os.Getenv(ScwActiveProfileEnv))
case c.ActiveProfile != nil:
logger.Debugf("using active profile from config: active_profile=%s\n", *c.ActiveProfile)
return c.GetProfile(*c.ActiveProfile)
default:
return &c.Profile, nil
}
}
// SaveTo will save the config to the default config path. This
// action will overwrite the previous file when it exists.
func (c *Config) Save() error {
return c.SaveTo(GetConfigPath())
}
// HumanConfig will generate a config file with documented arguments.
func (c *Config) HumanConfig() (string, error) {
tmpl, err := template.New("configuration").Parse(configFileTemplate)
if err != nil {
return "", err
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, c)
if err != nil {
return "", err
}
return buf.String(), nil
}
// SaveTo will save the config to the given path. This action will
// overwrite the previous file when it exists.
func (c *Config) SaveTo(path string) error {
path = filepath.Clean(path)
// STEP 1: Render the configuration file as a file
file, err := c.HumanConfig()
if err != nil {
return err
}
// STEP 2: create config path dir in cases it didn't exist before
err = os.MkdirAll(filepath.Dir(path), 0o700)
if err != nil {
return err
}
// STEP 3: write new config file
err = os.WriteFile(path, []byte(file), defaultConfigPermission)
if err != nil {
return err
}
return nil
}
// MergeProfiles merges profiles in a new one. The last profile has priority.
func MergeProfiles(original *Profile, others ...*Profile) *Profile {
np := &Profile{
AccessKey: original.AccessKey,
SecretKey: original.SecretKey,
APIURL: original.APIURL,
Insecure: original.Insecure,
DefaultOrganizationID: original.DefaultOrganizationID,
DefaultProjectID: original.DefaultProjectID,
DefaultRegion: original.DefaultRegion,
DefaultZone: original.DefaultZone,
SendTelemetry: original.SendTelemetry,
}
for _, other := range others {
if other.AccessKey != nil {
np.AccessKey = other.AccessKey
}
if other.SecretKey != nil {
np.SecretKey = other.SecretKey
}
if other.APIURL != nil {
np.APIURL = other.APIURL
}
if other.Insecure != nil {
np.Insecure = other.Insecure
}
if other.DefaultOrganizationID != nil {
np.DefaultOrganizationID = other.DefaultOrganizationID
}
if other.DefaultProjectID != nil {
np.DefaultProjectID = other.DefaultProjectID
}
if other.DefaultRegion != nil {
np.DefaultRegion = other.DefaultRegion
}
if other.DefaultZone != nil {
np.DefaultZone = other.DefaultZone
}
if other.SendTelemetry != nil {
np.SendTelemetry = other.SendTelemetry
}
}
return np
}