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