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, you’ll 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