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