config.go raw
1 package scw
2
3 import (
4 "bytes"
5 goerrors "errors"
6 "os"
7 "path/filepath"
8 "strings"
9 "text/template"
10
11 "github.com/scaleway/scaleway-sdk-go/errors"
12 "github.com/scaleway/scaleway-sdk-go/internal/auth"
13 "github.com/scaleway/scaleway-sdk-go/logger"
14 "gopkg.in/yaml.v2"
15 )
16
17 const (
18 documentationLink = "https://github.com/scaleway/scaleway-sdk-go/blob/master/scw/README.md"
19 defaultConfigPermission = 0o600
20
21 // Reserved name for the default profile.
22 DefaultProfileName = "default"
23 )
24
25 const configFileTemplate = `# Scaleway configuration file
26 # https://github.com/scaleway/scaleway-sdk-go/tree/master/scw#scaleway-config
27
28 # This configuration file can be used with:
29 # - Scaleway SDK Go (https://github.com/scaleway/scaleway-sdk-go)
30 # - Scaleway CLI (>2.0.0) (https://github.com/scaleway/scaleway-cli)
31 # - Scaleway Terraform Provider (https://www.terraform.io/docs/providers/scaleway/index.html)
32
33 # You need an access key and a secret key to connect to Scaleway API.
34 # Generate your token at the following address: https://console.scaleway.com/iam/api-keys
35
36 # An access key is a secret key identifier.
37 {{ if .AccessKey }}access_key: {{.AccessKey}}{{ else }}# access_key: SCW11111111111111111{{ end }}
38
39 # The secret key is the value that can be used to authenticate against the API (the value used in X-Auth-Token HTTP-header).
40 # The secret key MUST remain secret and not given to anyone or published online.
41 {{ if .SecretKey }}secret_key: {{ .SecretKey }}{{ else }}# secret_key: 11111111-1111-1111-1111-111111111111{{ end }}
42
43 # Your organization ID is the identifier of your account inside Scaleway infrastructure.
44 {{ if .DefaultOrganizationID }}default_organization_id: {{ .DefaultOrganizationID }}{{ else }}# default_organization_id: 11111111-1111-1111-1111-111111111111{{ end }}
45
46 # Your project ID is the identifier of the project your resources are attached to (beta).
47 {{ if .DefaultProjectID }}default_project_id: {{ .DefaultProjectID }}{{ else }}# default_project_id: 11111111-1111-1111-1111-111111111111{{ end }}
48
49 # A region is represented as a geographical area such as France (Paris) or the Netherlands (Amsterdam).
50 # It can contain multiple availability zones.
51 # Example of region: fr-par, nl-ams
52 {{ if .DefaultRegion }}default_region: {{ .DefaultRegion }}{{ else }}# default_region: fr-par{{ end }}
53
54 # A region can be split into many availability zones (AZ).
55 # Latency between multiple AZ of the same region are low as they have a common network layer.
56 # Example of zones: fr-par-1, nl-ams-1
57 {{ if .DefaultZone }}default_zone: {{.DefaultZone}}{{ else }}# default_zone: fr-par-1{{ end }}
58
59 # APIURL overrides the API URL of the Scaleway API to the given URL.
60 # Change that if you want to direct requests to a different endpoint.
61 {{ if .APIURL }}apiurl: {{ .APIURL }}{{ else }}# api_url: https://api.scaleway.com{{ end }}
62
63 # Insecure enables insecure transport on the client.
64 # Default to false
65 {{ if .Insecure }}insecure: {{ .Insecure }}{{ else }}# insecure: false{{ end }}
66
67 # A configuration is a named set of Scaleway properties.
68 # Starting off with a Scaleway SDK or Scaleway CLI, you’ll work with a single configuration named default.
69 # You can set properties of the default profile by running either scw init or scw config set.
70 # This single default configuration is suitable for most use cases.
71 {{ if .ActiveProfile }}active_profile: {{ .ActiveProfile }}{{ else }}# active_profile: myProfile{{ end }}
72
73 # To improve the Scaleway CLI we rely on diagnostic and usage data.
74 # Sending such data is optional and can be disable at any time by setting send_telemetry variable to false.
75 {{ if .SendTelemetry }}send_telemetry: {{ .SendTelemetry }}{{ else }}# send_telemetry: false{{ end }}
76
77 # To work with multiple projects or authorization accounts, you can set up multiple configurations with scw config configurations create and switch among them accordingly.
78 # You can use a profile by either:
79 # - Define the profile you want to use as the SCW_PROFILE environment variable
80 # - Use the GetActiveProfile() function in the SDK
81 # - Use the --profile flag with the CLI
82
83 # You can define a profile using the following syntax:
84 {{ if gt (len .Profiles) 0 }}
85 profiles:
86 {{- range $k,$v := .Profiles }}
87 {{ $k }}:
88 {{ if $v.AccessKey }}access_key: {{ $v.AccessKey }}{{ else }}# access_key: SCW11111111111111111{{ end }}
89 {{ if $v.SecretKey }}secret_key: {{ $v.SecretKey }}{{ else }}# secret_key: 11111111-1111-1111-1111-111111111111{{ end }}
90 {{ if $v.DefaultOrganizationID }}default_organization_id: {{ $v.DefaultOrganizationID }}{{ else }}# default_organization_id: 11111111-1111-1111-1111-111111111111{{ end }}
91 {{ if $v.DefaultProjectID }}default_project_id: {{ $v.DefaultProjectID }}{{ else }}# default_project_id: 11111111-1111-1111-1111-111111111111{{ end }}
92 {{ if $v.DefaultZone }}default_zone: {{ $v.DefaultZone }}{{ else }}# default_zone: fr-par-1{{ end }}
93 {{ if $v.DefaultRegion }}default_region: {{ $v.DefaultRegion }}{{ else }}# default_region: fr-par{{ end }}
94 {{ if $v.APIURL }}api_url: {{ $v.APIURL }}{{ else }}# api_url: https://api.scaleway.com{{ end }}
95 {{ if $v.Insecure }}insecure: {{ $v.Insecure }}{{ else }}# insecure: false{{ end }}
96 {{ end }}
97 {{- else }}
98 # profiles:
99 # myProfile:
100 # access_key: 11111111-1111-1111-1111-111111111111
101 # secret_key: 11111111-1111-1111-1111-111111111111
102 # default_organization_id: 11111111-1111-1111-1111-111111111111
103 # default_project_id: 11111111-1111-1111-1111-111111111111
104 # default_zone: fr-par-1
105 # default_region: fr-par
106 # api_url: https://api.scaleway.com
107 # insecure: false
108 {{ end -}}
109 `
110
111 type Config struct {
112 Profile `yaml:",inline"`
113 ActiveProfile *string `yaml:"active_profile,omitempty" json:"active_profile,omitempty"`
114 Profiles map[string]*Profile `yaml:"profiles,omitempty" json:"profiles,omitempty"`
115 }
116
117 type Profile struct {
118 AccessKey *string `yaml:"access_key,omitempty" json:"access_key,omitempty"`
119 SecretKey *string `yaml:"secret_key,omitempty" json:"secret_key,omitempty"`
120 APIURL *string `yaml:"api_url,omitempty" json:"api_url,omitempty"`
121 Insecure *bool `yaml:"insecure,omitempty" json:"insecure,omitempty"`
122 DefaultOrganizationID *string `yaml:"default_organization_id,omitempty" json:"default_organization_id,omitempty"`
123 DefaultProjectID *string `yaml:"default_project_id,omitempty" json:"default_project_id,omitempty"`
124 DefaultRegion *string `yaml:"default_region,omitempty" json:"default_region,omitempty"`
125 DefaultZone *string `yaml:"default_zone,omitempty" json:"default_zone,omitempty"`
126 SendTelemetry *bool `yaml:"send_telemetry,omitempty" json:"send_telemetry,omitempty"`
127 }
128
129 func (p *Profile) String() string {
130 p2 := *p
131 p2.SecretKey = hideSecretKey(p2.SecretKey)
132 configRaw, _ := yaml.Marshal(p2)
133 return string(configRaw)
134 }
135
136 // clone deep copy config object
137 func (c *Config) clone() *Config {
138 c2 := &Config{}
139 configRaw, _ := yaml.Marshal(c)
140 _ = yaml.Unmarshal(configRaw, c2)
141 return c2
142 }
143
144 func (c *Config) String() string {
145 c2 := c.clone()
146 c2.SecretKey = hideSecretKey(c2.SecretKey)
147 for _, p := range c2.Profiles {
148 if p == nil {
149 continue
150 }
151 p.SecretKey = hideSecretKey(p.SecretKey)
152 }
153
154 configRaw, _ := yaml.Marshal(c2)
155 return string(configRaw)
156 }
157
158 func (c *Config) IsEmpty() bool {
159 return c.String() == "{}\n"
160 }
161
162 func hideSecretKey(key *string) *string {
163 if key == nil {
164 return nil
165 }
166
167 newKey := auth.HideSecretKey(*key)
168 return &newKey
169 }
170
171 func unmarshalConfV2(content []byte) (*Config, error) {
172 var config Config
173
174 err := yaml.Unmarshal(content, &config)
175 if err != nil {
176 return nil, err
177 }
178 return &config, nil
179 }
180
181 // MustLoadConfig is like LoadConfig but panic instead of returning an error.
182 func MustLoadConfig() *Config {
183 c, err := LoadConfigFromPath(GetConfigPath())
184 if err != nil {
185 panic(err)
186 }
187 return c
188 }
189
190 // LoadConfig read the config from the default path.
191 func LoadConfig() (*Config, error) {
192 configPath := GetConfigPath()
193 cfg, err := LoadConfigFromPath(configPath)
194
195 // Special case if using default config path
196 // if config.yaml does not exist, we should try to read config.yml
197 if os.Getenv(ScwConfigPathEnv) == "" {
198 var configNotFoundError *ConfigFileNotFoundError
199 if err != nil && goerrors.As(err, &configNotFoundError) && strings.HasSuffix(configPath, ".yaml") {
200 configPath = strings.TrimSuffix(configPath, ".yaml") + ".yml"
201 cfgYml, errYml := LoadConfigFromPath(configPath)
202 // If .yml config is not found, return first error when reading .yaml
203 if errYml == nil || (errYml != nil && !goerrors.As(errYml, &configNotFoundError)) {
204 return cfgYml, errYml
205 }
206 }
207 }
208
209 return cfg, err
210 }
211
212 // LoadConfigFromPath read the config from the given path.
213 func LoadConfigFromPath(path string) (*Config, error) {
214 _, err := os.Stat(path)
215 if os.IsNotExist(err) {
216 return nil, configFileNotFound(path)
217 }
218 if err != nil {
219 return nil, err
220 }
221
222 file, err := os.ReadFile(path)
223 if err != nil {
224 return nil, errors.Wrap(err, "cannot read config file")
225 }
226
227 confV2, err := unmarshalConfV2(file)
228 if err != nil {
229 return nil, errors.Wrap(err, "content of config file %s is invalid", path)
230 }
231
232 return confV2, nil
233 }
234
235 // GetProfile returns the profile corresponding to the given profile name.
236 func (c *Config) GetProfile(profileName string) (*Profile, error) {
237 if profileName == "" {
238 return nil, errors.New("profileName cannot be empty")
239 }
240
241 if profileName == DefaultProfileName {
242 return &c.Profile, nil
243 }
244
245 p, exist := c.Profiles[profileName]
246 if !exist {
247 return nil, errors.New("given profile %s does not exist", profileName)
248 }
249
250 // Merge selected profile on top of default profile
251 return MergeProfiles(&c.Profile, p), nil
252 }
253
254 // GetActiveProfile returns the active profile of the config based on the following order:
255 // env SCW_PROFILE > config active_profile > config root profile
256 func (c *Config) GetActiveProfile() (*Profile, error) {
257 switch {
258 case os.Getenv(ScwActiveProfileEnv) != "":
259 logger.Debugf("using active profile from env: %s=%s\n", ScwActiveProfileEnv, os.Getenv(ScwActiveProfileEnv))
260 return c.GetProfile(os.Getenv(ScwActiveProfileEnv))
261 case c.ActiveProfile != nil:
262 logger.Debugf("using active profile from config: active_profile=%s\n", *c.ActiveProfile)
263 return c.GetProfile(*c.ActiveProfile)
264 default:
265 return &c.Profile, nil
266 }
267 }
268
269 // SaveTo will save the config to the default config path. This
270 // action will overwrite the previous file when it exists.
271 func (c *Config) Save() error {
272 return c.SaveTo(GetConfigPath())
273 }
274
275 // HumanConfig will generate a config file with documented arguments.
276 func (c *Config) HumanConfig() (string, error) {
277 tmpl, err := template.New("configuration").Parse(configFileTemplate)
278 if err != nil {
279 return "", err
280 }
281
282 var buf bytes.Buffer
283 err = tmpl.Execute(&buf, c)
284 if err != nil {
285 return "", err
286 }
287
288 return buf.String(), nil
289 }
290
291 // SaveTo will save the config to the given path. This action will
292 // overwrite the previous file when it exists.
293 func (c *Config) SaveTo(path string) error {
294 path = filepath.Clean(path)
295
296 // STEP 1: Render the configuration file as a file
297 file, err := c.HumanConfig()
298 if err != nil {
299 return err
300 }
301
302 // STEP 2: create config path dir in cases it didn't exist before
303 err = os.MkdirAll(filepath.Dir(path), 0o700)
304 if err != nil {
305 return err
306 }
307
308 // STEP 3: write new config file
309 err = os.WriteFile(path, []byte(file), defaultConfigPermission)
310 if err != nil {
311 return err
312 }
313
314 return nil
315 }
316
317 // MergeProfiles merges profiles in a new one. The last profile has priority.
318 func MergeProfiles(original *Profile, others ...*Profile) *Profile {
319 np := &Profile{
320 AccessKey: original.AccessKey,
321 SecretKey: original.SecretKey,
322 APIURL: original.APIURL,
323 Insecure: original.Insecure,
324 DefaultOrganizationID: original.DefaultOrganizationID,
325 DefaultProjectID: original.DefaultProjectID,
326 DefaultRegion: original.DefaultRegion,
327 DefaultZone: original.DefaultZone,
328 SendTelemetry: original.SendTelemetry,
329 }
330
331 for _, other := range others {
332 if other.AccessKey != nil {
333 np.AccessKey = other.AccessKey
334 }
335 if other.SecretKey != nil {
336 np.SecretKey = other.SecretKey
337 }
338 if other.APIURL != nil {
339 np.APIURL = other.APIURL
340 }
341 if other.Insecure != nil {
342 np.Insecure = other.Insecure
343 }
344 if other.DefaultOrganizationID != nil {
345 np.DefaultOrganizationID = other.DefaultOrganizationID
346 }
347 if other.DefaultProjectID != nil {
348 np.DefaultProjectID = other.DefaultProjectID
349 }
350 if other.DefaultRegion != nil {
351 np.DefaultRegion = other.DefaultRegion
352 }
353 if other.DefaultZone != nil {
354 np.DefaultZone = other.DefaultZone
355 }
356 if other.SendTelemetry != nil {
357 np.SendTelemetry = other.SendTelemetry
358 }
359 }
360
361 return np
362 }
363