package scw import ( "bytes" "io/ioutil" "os" "path/filepath" "text/template" "github.com/scaleway/scaleway-sdk-go/internal/auth" "github.com/scaleway/scaleway-sdk-go/internal/errors" "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 = 0600 // 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/project/credentials # 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) { return LoadConfigFromPath(GetConfigPath()) } // 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 := ioutil.ReadFile(path) if err != nil { return nil, errors.Wrap(err, "cannot read config file") } _, err = unmarshalConfV1(file) if err == nil { // reject V1 config 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) } 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", ScwActiveProfileEnv, os.Getenv(ScwActiveProfileEnv)) return c.GetProfile(os.Getenv(ScwActiveProfileEnv)) case c.ActiveProfile != nil: logger.Debugf("using active profile from config: active_profile=%s", ScwActiveProfileEnv, *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), 0700) if err != nil { return err } // STEP 3: write new config file err = ioutil.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, } 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 } } return np }