344 lines
12 KiB
Go
344 lines
12 KiB
Go
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
|
||
}
|