mirror of https://github.com/kubernetes/kops.git
Merge pull request #14322 from Mia-Cross/scw_nodeup
Scaleway init and nodeup
This commit is contained in:
commit
db3ec0c72f
2
Makefile
2
Makefile
|
|
@ -48,7 +48,7 @@ UPLOAD_CMD=$(KOPS_ROOT)/hack/upload ${UPLOAD_ARGS}
|
|||
# Unexport environment variables that can affect tests and are not used in builds
|
||||
unexport AWS_ACCESS_KEY_ID AWS_REGION AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN CNI_VERSION_URL DNS_IGNORE_NS_CHECK DNSCONTROLLER_IMAGE DO_ACCESS_TOKEN GOOGLE_APPLICATION_CREDENTIALS
|
||||
unexport KOPS_BASE_URL KOPS_CLUSTER_NAME KOPS_RUN_OBSOLETE_VERSION KOPS_STATE_STORE KOPS_STATE_S3_ACL KUBE_API_VERSIONS NODEUP_URL OPENSTACK_CREDENTIAL_FILE SKIP_PACKAGE_UPDATE
|
||||
unexport SKIP_REGION_CHECK S3_ACCESS_KEY_ID S3_ENDPOINT S3_REGION S3_SECRET_ACCESS_KEY HCLOUD_TOKEN
|
||||
unexport SKIP_REGION_CHECK S3_ACCESS_KEY_ID S3_ENDPOINT S3_REGION S3_SECRET_ACCESS_KEY HCLOUD_TOKEN SCW_ACCESS_KEY SCW_SECRET_KEY SCW_DEFAULT_PROJECT_ID SCW_DEFAULT_REGION SCW_DEFAULT_ZONE
|
||||
|
||||
|
||||
VERSION=$(shell tools/get_version.sh | grep VERSION | awk '{print $$2}')
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -35,6 +35,7 @@ require (
|
|||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pkg/sftp v1.13.5
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9
|
||||
github.com/sergi/go-diff v1.2.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -1049,6 +1049,8 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI
|
|||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 h1:0roa6gXKgyta64uqh52AQG3wzZXH21unn+ltzQSXML0=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc=
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ unset KOPS_BASE_URL DNSCONTROLLER_IMAGE KOPSCONTROLLER_IMAGE KUBE_APISERVER_HEAL
|
|||
unset AWS_ACCESS_KEY_ID AWS_REGION AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN CNI_VERSION_URL DNS_IGNORE_NS_CHECK DO_ACCESS_TOKEN GOOGLE_APPLICATION_CREDENTIALS HCLOUD_TOKEN
|
||||
unset KOPS_CLUSTER_NAME KOPS_RUN_OBSOLETE_VERSION KOPS_STATE_STORE KOPS_STATE_S3_ACL KUBE_API_VERSIONS NODEUP_URL OPENSTACK_CREDENTIAL_FILE PROTOKUBE_IMAGE SKIP_PACKAGE_UPDATE
|
||||
unset SKIP_REGION_CHECK S3_ACCESS_KEY_ID S3_ENDPOINT S3_REGION S3_SECRET_ACCESS_KEY
|
||||
|
||||
unset SCW_ACCESS_KEY SCW_SECRET_KEY SCW_DEFAULT_PROJECT_ID SCW_DEFAULT_REGION SCW_DEFAULT_ZONE
|
||||
|
||||
# Run the tests in "autofix mode"
|
||||
HACK_UPDATE_EXPECTED_IN_PLACE=1 go test "${PKG}" -count=1
|
||||
|
|
|
|||
|
|
@ -48,5 +48,12 @@ if [[ "${DEST:0:5}" == "do://" ]]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${DEST:0:6}" == "scw://" ]]; then
|
||||
SCW_BUCKET=$(echo "${DEST}" | cut -c 7-)
|
||||
echo "--> s3cmd put ${SRC} s3://$SCW_BUCKET --recursive ${PUBLIC:+--acl-public} --progress"
|
||||
s3cmd put ${SRC} s3://$SCW_BUCKET --recursive ${PUBLIC:+--acl-public} --progress
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Unsupported destination - supports s3://, gs:// and oss:// urls: ${DEST}"
|
||||
exit 1
|
||||
|
|
|
|||
|
|
@ -153,6 +153,14 @@ func (i *Installation) buildEnvFile() *nodetasks.File {
|
|||
envVars["AZURE_STORAGE_ACCOUNT"] = os.Getenv("AZURE_STORAGE_ACCOUNT")
|
||||
}
|
||||
|
||||
if os.Getenv("SCW_SECRET_KEY") != "" {
|
||||
envVars["SCW_ACCESS_KEY"] = os.Getenv("SCW_ACCESS_KEY")
|
||||
envVars["SCW_SECRET_KEY"] = os.Getenv("SCW_SECRET_KEY")
|
||||
envVars["SCW_DEFAULT_PROJECT_ID"] = os.Getenv("SCW_DEFAULT_PROJECT_ID")
|
||||
envVars["SCW_DEFAULT_REGION"] = os.Getenv("SCW_DEFAULT_REGION")
|
||||
envVars["SCW_DEFAULT_ZONE"] = os.Getenv("SCW_DEFAULT_ZONE")
|
||||
}
|
||||
|
||||
sysconfig := ""
|
||||
for key, value := range envVars {
|
||||
sysconfig += key + "=" + value + "\n"
|
||||
|
|
|
|||
|
|
@ -310,6 +310,14 @@ func (t *ProtokubeBuilder) buildEnvFile() (*nodetasks.File, error) {
|
|||
envVars["AZURE_STORAGE_ACCOUNT"] = os.Getenv("AZURE_STORAGE_ACCOUNT")
|
||||
}
|
||||
|
||||
if t.CloudProvider == kops.CloudProviderScaleway {
|
||||
envVars["SCW_ACCESS_KEY"] = os.Getenv("SCW_ACCESS_KEY")
|
||||
envVars["SCW_SECRET_KEY"] = os.Getenv("SCW_SECRET_KEY")
|
||||
envVars["SCW_DEFAULT_PROJECT_ID"] = os.Getenv("SCW_DEFAULT_PROJECT_ID")
|
||||
envVars["SCW_DEFAULT_REGION"] = os.Getenv("SCW_DEFAULT_REGION")
|
||||
envVars["SCW_DEFAULT_ZONE"] = os.Getenv("SCW_DEFAULT_ZONE")
|
||||
}
|
||||
|
||||
for _, envVar := range proxy.GetProxyEnvVars(t.Cluster.Spec.EgressProxy) {
|
||||
envVars[envVar.Name] = envVar.Value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ const (
|
|||
CloudProviderHetzner CloudProviderID = "hetzner"
|
||||
CloudProviderOpenstack CloudProviderID = "openstack"
|
||||
CloudProviderAzure CloudProviderID = "azure"
|
||||
CloudProviderScaleway CloudProviderID = "scaleway"
|
||||
)
|
||||
|
||||
// FindImage returns the image for the cloudprovider, or nil if none found
|
||||
|
|
|
|||
|
|
@ -241,6 +241,8 @@ type CloudProviderSpec struct {
|
|||
Hetzner *HetznerSpec `json:"hetzner,omitempty"`
|
||||
// Openstack configures the Openstack cloud provider.
|
||||
Openstack *OpenstackSpec `json:"openstack,omitempty"`
|
||||
// Scaleway configures the Scaleway cloud provider.
|
||||
Scaleway *ScalewaySpec `json:"scaleway,omitempty"`
|
||||
}
|
||||
|
||||
// AWSSpec configures the AWS cloud provider.
|
||||
|
|
@ -259,6 +261,10 @@ type GCESpec struct {
|
|||
type HetznerSpec struct {
|
||||
}
|
||||
|
||||
// ScalewaySpec configures the Scaleway cloud provider
|
||||
type ScalewaySpec struct {
|
||||
}
|
||||
|
||||
type KarpenterConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
|
@ -911,6 +917,8 @@ func (c *ClusterSpec) GetCloudProvider() CloudProviderID {
|
|||
return CloudProviderHetzner
|
||||
} else if c.CloudProvider.Openstack != nil {
|
||||
return CloudProviderOpenstack
|
||||
} else if c.CloudProvider.Scaleway != nil {
|
||||
return CloudProviderScaleway
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ func Convert_v1alpha2_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out *kops
|
|||
return err
|
||||
}
|
||||
}
|
||||
case kops.CloudProviderScaleway:
|
||||
out.CloudProvider.Scaleway = &kops.ScalewaySpec{}
|
||||
case "":
|
||||
default:
|
||||
return field.NotSupported(field.NewPath("spec").Child("cloudProvider"), in.LegacyCloudProvider, []string{
|
||||
|
|
@ -107,6 +109,7 @@ func Convert_v1alpha2_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out *kops
|
|||
string(kops.CloudProviderAWS),
|
||||
string(kops.CloudProviderHetzner),
|
||||
string(kops.CloudProviderOpenstack),
|
||||
string(kops.CloudProviderScaleway),
|
||||
})
|
||||
}
|
||||
if in.TagSubnets != nil {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,8 @@ type CloudProviderSpec struct {
|
|||
Hetzner *HetznerSpec `json:"hetzner,omitempty"`
|
||||
// Openstack configures the Openstack cloud provider.
|
||||
Openstack *OpenstackSpec `json:"openstack,omitempty"`
|
||||
// Scaleway configures the Scaleway cloud provider.
|
||||
Scaleway *ScalewaySpec `json:"scaleway,omitempty"`
|
||||
}
|
||||
|
||||
// AWSSpec configures the AWS cloud provider.
|
||||
|
|
@ -256,6 +258,10 @@ type GCESpec struct {
|
|||
type HetznerSpec struct {
|
||||
}
|
||||
|
||||
// ScalewaySpec configures the Scaleway cloud provider
|
||||
type ScalewaySpec struct {
|
||||
}
|
||||
|
||||
type KarpenterConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1134,6 +1134,16 @@ func RegisterConversions(s *runtime.Scheme) error {
|
|||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*ScalewaySpec)(nil), (*kops.ScalewaySpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha3_ScalewaySpec_To_kops_ScalewaySpec(a.(*ScalewaySpec), b.(*kops.ScalewaySpec), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*kops.ScalewaySpec)(nil), (*ScalewaySpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_kops_ScalewaySpec_To_v1alpha3_ScalewaySpec(a.(*kops.ScalewaySpec), b.(*ScalewaySpec), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*ServiceAccountExternalPermission)(nil), (*kops.ServiceAccountExternalPermission)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha3_ServiceAccountExternalPermission_To_kops_ServiceAccountExternalPermission(a.(*ServiceAccountExternalPermission), b.(*kops.ServiceAccountExternalPermission), scope)
|
||||
}); err != nil {
|
||||
|
|
@ -2310,6 +2320,15 @@ func autoConvert_v1alpha3_CloudProviderSpec_To_kops_CloudProviderSpec(in *CloudP
|
|||
} else {
|
||||
out.Openstack = nil
|
||||
}
|
||||
if in.Scaleway != nil {
|
||||
in, out := &in.Scaleway, &out.Scaleway
|
||||
*out = new(kops.ScalewaySpec)
|
||||
if err := Convert_v1alpha3_ScalewaySpec_To_kops_ScalewaySpec(*in, *out, s); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
out.Scaleway = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -2373,6 +2392,15 @@ func autoConvert_kops_CloudProviderSpec_To_v1alpha3_CloudProviderSpec(in *kops.C
|
|||
} else {
|
||||
out.Openstack = nil
|
||||
}
|
||||
if in.Scaleway != nil {
|
||||
in, out := &in.Scaleway, &out.Scaleway
|
||||
*out = new(ScalewaySpec)
|
||||
if err := Convert_kops_ScalewaySpec_To_v1alpha3_ScalewaySpec(*in, *out, s); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
out.Scaleway = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -7121,6 +7149,24 @@ func Convert_kops_SSHCredentialSpec_To_v1alpha3_SSHCredentialSpec(in *kops.SSHCr
|
|||
return autoConvert_kops_SSHCredentialSpec_To_v1alpha3_SSHCredentialSpec(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha3_ScalewaySpec_To_kops_ScalewaySpec(in *ScalewaySpec, out *kops.ScalewaySpec, s conversion.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha3_ScalewaySpec_To_kops_ScalewaySpec is an autogenerated conversion function.
|
||||
func Convert_v1alpha3_ScalewaySpec_To_kops_ScalewaySpec(in *ScalewaySpec, out *kops.ScalewaySpec, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha3_ScalewaySpec_To_kops_ScalewaySpec(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_kops_ScalewaySpec_To_v1alpha3_ScalewaySpec(in *kops.ScalewaySpec, out *ScalewaySpec, s conversion.Scope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_kops_ScalewaySpec_To_v1alpha3_ScalewaySpec is an autogenerated conversion function.
|
||||
func Convert_kops_ScalewaySpec_To_v1alpha3_ScalewaySpec(in *kops.ScalewaySpec, out *ScalewaySpec, s conversion.Scope) error {
|
||||
return autoConvert_kops_ScalewaySpec_To_v1alpha3_ScalewaySpec(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha3_ServiceAccountExternalPermission_To_kops_ServiceAccountExternalPermission(in *ServiceAccountExternalPermission, out *kops.ServiceAccountExternalPermission, s conversion.Scope) error {
|
||||
out.Name = in.Name
|
||||
out.Namespace = in.Namespace
|
||||
|
|
|
|||
|
|
@ -833,6 +833,11 @@ func (in *CloudProviderSpec) DeepCopyInto(out *CloudProviderSpec) {
|
|||
*out = new(OpenstackSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Scaleway != nil {
|
||||
in, out := &in.Scaleway, &out.Scaleway
|
||||
*out = new(ScalewaySpec)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -5060,6 +5065,22 @@ func (in *SSHCredentialSpec) DeepCopy() *SSHCredentialSpec {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScalewaySpec) DeepCopyInto(out *ScalewaySpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalewaySpec.
|
||||
func (in *ScalewaySpec) DeepCopy() *ScalewaySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScalewaySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceAccountExternalPermission) DeepCopyInto(out *ServiceAccountExternalPermission) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -930,6 +930,11 @@ func (in *CloudProviderSpec) DeepCopyInto(out *CloudProviderSpec) {
|
|||
*out = new(OpenstackSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Scaleway != nil {
|
||||
in, out := &in.Scaleway, &out.Scaleway
|
||||
*out = new(ScalewaySpec)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -5367,6 +5372,22 @@ func (in *SSHCredentialSpec) DeepCopy() *SSHCredentialSpec {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScalewaySpec) DeepCopyInto(out *ScalewaySpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalewaySpec.
|
||||
func (in *ScalewaySpec) DeepCopy() *ScalewaySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScalewaySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceAccountExternalPermission) DeepCopyInto(out *ServiceAccountExternalPermission) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ func SupportedClouds() []kops.CloudProviderID {
|
|||
if featureflag.Azure.Enabled() {
|
||||
clouds = append(clouds, kops.CloudProviderAzure)
|
||||
}
|
||||
if featureflag.Scaleway.Enabled() {
|
||||
clouds = append(clouds, kops.CloudProviderScaleway)
|
||||
}
|
||||
|
||||
return clouds
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ var (
|
|||
Karpenter = new("Karpenter", Bool(false))
|
||||
// ImageDigest remaps all manifests with image digests
|
||||
ImageDigest = new("ImageDigest", Bool(true))
|
||||
// Scaleway toggles the Scaleway Cloud support.
|
||||
Scaleway = new("Scaleway", Bool(false))
|
||||
)
|
||||
|
||||
// FeatureFlag defines a feature flag
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
"k8s.io/kops/upup/pkg/fi/utils"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
"k8s.io/kops/pkg/apis/kops"
|
||||
"k8s.io/kops/pkg/apis/nodeup"
|
||||
"k8s.io/kops/pkg/model/resources"
|
||||
|
|
@ -213,6 +214,39 @@ func (b *BootstrapScript) buildEnvironmentVariables(cluster *kops.Cluster) (map[
|
|||
}
|
||||
}
|
||||
|
||||
if cluster.Spec.GetCloudProvider() == kops.CloudProviderScaleway {
|
||||
|
||||
region, err := scw.ParseRegion(os.Getenv("SCW_DEFAULT_REGION"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing SCW_DEFAULT_REGION: %w", err)
|
||||
}
|
||||
env["SCW_DEFAULT_REGION"] = string(region)
|
||||
|
||||
zone, err := scw.ParseZone(os.Getenv("SCW_DEFAULT_ZONE"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing SCW_DEFAULT_ZONE: %w", err)
|
||||
}
|
||||
env["SCW_DEFAULT_ZONE"] = string(zone)
|
||||
|
||||
scwAccessKey := os.Getenv("SCW_ACCESS_KEY")
|
||||
if scwAccessKey == "" {
|
||||
return nil, fmt.Errorf("SCW_ACCESS_KEY has to be set as an environment variable")
|
||||
}
|
||||
env["SCW_ACCESS_KEY"] = scwAccessKey
|
||||
|
||||
scwSecretKey := os.Getenv("SCW_SECRET_KEY")
|
||||
if scwSecretKey != "" {
|
||||
return nil, fmt.Errorf("SCW_SECRET_KEY has to be set as an environment variable")
|
||||
}
|
||||
env["SCW_SECRET_KEY"] = scwSecretKey
|
||||
|
||||
scwProjectID := os.Getenv("SCW_DEFAULT_PROJECT_ID")
|
||||
if scwProjectID != "" {
|
||||
return nil, fmt.Errorf("SCW_DEFAULT_PROJECT_ID has to be set as an environment variable")
|
||||
}
|
||||
env["SCW_DEFAULT_PROJECT_ID"] = scwProjectID
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ func BuildSystemComponentEnvVars(spec *kops.ClusterSpec) EnvVars {
|
|||
// Azure related values.
|
||||
vars.addEnvVariableIfExist("AZURE_STORAGE_ACCOUNT")
|
||||
|
||||
// Scaleway related values.
|
||||
vars.addEnvVariableIfExist("SCW_ACCESS_KEY")
|
||||
vars.addEnvVariableIfExist("SCW_SECRET_KEY")
|
||||
vars.addEnvVariableIfExist("SCW_DEFAULT_PROJECT_ID")
|
||||
vars.addEnvVariableIfExist("SCW_DEFAULT_REGION")
|
||||
vars.addEnvVariableIfExist("SCW_DEFAULT_ZONE")
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2019 Scaleway.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package auth
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Auth implement methods required for authentication.
|
||||
// Valid authentication are currently a token or no auth.
|
||||
type Auth interface {
|
||||
// Headers returns headers that must be add to the http request
|
||||
Headers() http.Header
|
||||
|
||||
// AnonymizedHeaders returns an anonymised version of Headers()
|
||||
// This method could be use for logging purpose.
|
||||
AnonymizedHeaders() http.Header
|
||||
}
|
||||
19
vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/no_auth.go
generated
vendored
Normal file
19
vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/no_auth.go
generated
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package auth
|
||||
|
||||
import "net/http"
|
||||
|
||||
type NoAuth struct {
|
||||
}
|
||||
|
||||
// NewNoAuth return an auth with no authentication method
|
||||
func NewNoAuth() *NoAuth {
|
||||
return &NoAuth{}
|
||||
}
|
||||
|
||||
func (t *NoAuth) Headers() http.Header {
|
||||
return http.Header{}
|
||||
}
|
||||
|
||||
func (t *NoAuth) AnonymizedHeaders() http.Header {
|
||||
return http.Header{}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package auth
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Token is the pair accessKey + secretKey.
|
||||
// This type is public because it's an internal package.
|
||||
type Token struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
// XAuthTokenHeader is Scaleway standard auth header
|
||||
const XAuthTokenHeader = "X-Auth-Token" // #nosec G101
|
||||
|
||||
// NewToken create a token authentication from an
|
||||
// access key and a secret key
|
||||
func NewToken(accessKey, secretKey string) *Token {
|
||||
return &Token{AccessKey: accessKey, SecretKey: secretKey}
|
||||
}
|
||||
|
||||
// Headers returns headers that must be add to the http request
|
||||
func (t *Token) Headers() http.Header {
|
||||
headers := http.Header{}
|
||||
headers.Set(XAuthTokenHeader, t.SecretKey)
|
||||
return headers
|
||||
}
|
||||
|
||||
// AnonymizedHeaders returns an anonymized version of Headers()
|
||||
// This method could be use for logging purpose.
|
||||
func (t *Token) AnonymizedHeaders() http.Header {
|
||||
headers := http.Header{}
|
||||
headers.Set(XAuthTokenHeader, HideSecretKey(t.SecretKey))
|
||||
return headers
|
||||
}
|
||||
|
||||
func HideSecretKey(k string) string {
|
||||
switch {
|
||||
case len(k) == 0:
|
||||
return ""
|
||||
case len(k) > 8:
|
||||
return k[0:8] + "-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
default:
|
||||
return "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
}
|
||||
41
vendor/github.com/scaleway/scaleway-sdk-go/internal/errors/error.go
generated
vendored
Normal file
41
vendor/github.com/scaleway/scaleway-sdk-go/internal/errors/error.go
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Error is a base error that implement scw.SdkError
|
||||
type Error struct {
|
||||
Str string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error implement standard xerror.Wrapper interface
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Error implement standard error interface
|
||||
func (e *Error) Error() string {
|
||||
str := "scaleway-sdk-go: " + e.Str
|
||||
if e.Err != nil {
|
||||
str += ": " + e.Err.Error()
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// IsScwSdkError implement SdkError interface
|
||||
func (e *Error) IsScwSdkError() {}
|
||||
|
||||
// New creates a new error with that same interface as fmt.Errorf
|
||||
func New(format string, args ...interface{}) *Error {
|
||||
return &Error{
|
||||
Str: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap an error with additional information
|
||||
func Wrap(err error, format string, args ...interface{}) *Error {
|
||||
return &Error{
|
||||
Err: err,
|
||||
Str: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
111
vendor/github.com/scaleway/scaleway-sdk-go/logger/default_logger.go
generated
vendored
Normal file
111
vendor/github.com/scaleway/scaleway-sdk-go/logger/default_logger.go
generated
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var DefaultLogger = newLogger(os.Stderr, LogLevelWarning)
|
||||
var logger Logger = DefaultLogger
|
||||
|
||||
// loggerT is the default logger used by scaleway-sdk-go.
|
||||
type loggerT struct {
|
||||
m [4]*log.Logger
|
||||
v LogLevel
|
||||
}
|
||||
|
||||
// Init create a new default logger.
|
||||
// Not mutex-protected, should be called before any scaleway-sdk-go functions.
|
||||
func (g *loggerT) Init(w io.Writer, level LogLevel) {
|
||||
g.m = newLogger(w, level).m
|
||||
}
|
||||
|
||||
// Debugf logs to the DEBUG log. Arguments are handled in the manner of fmt.Printf.
|
||||
func Debugf(format string, args ...interface{}) { logger.Debugf(format, args...) }
|
||||
func (g *loggerT) Debugf(format string, args ...interface{}) {
|
||||
g.m[LogLevelDebug].Printf(format, args...)
|
||||
}
|
||||
|
||||
// Infof logs to the INFO log. Arguments are handled in the manner of fmt.Printf.
|
||||
func Infof(format string, args ...interface{}) { logger.Infof(format, args...) }
|
||||
func (g *loggerT) Infof(format string, args ...interface{}) {
|
||||
g.m[LogLevelInfo].Printf(format, args...)
|
||||
}
|
||||
|
||||
// Warningf logs to the WARNING log. Arguments are handled in the manner of fmt.Printf.
|
||||
func Warningf(format string, args ...interface{}) { logger.Warningf(format, args...) }
|
||||
func (g *loggerT) Warningf(format string, args ...interface{}) {
|
||||
g.m[LogLevelWarning].Printf(format, args...)
|
||||
}
|
||||
|
||||
// Errorf logs to the ERROR log. Arguments are handled in the manner of fmt.Printf.
|
||||
func Errorf(format string, args ...interface{}) { logger.Errorf(format, args...) }
|
||||
func (g *loggerT) Errorf(format string, args ...interface{}) {
|
||||
g.m[LogLevelError].Printf(format, args...)
|
||||
}
|
||||
|
||||
// ShouldLog reports whether verbosity level l is at least the requested verbose level.
|
||||
func ShouldLog(level LogLevel) bool { return logger.ShouldLog(level) }
|
||||
func (g *loggerT) ShouldLog(level LogLevel) bool {
|
||||
return level <= g.v
|
||||
}
|
||||
|
||||
func isEnabled(envKey string) bool {
|
||||
env, exist := os.LookupEnv(envKey)
|
||||
if !exist {
|
||||
return false
|
||||
}
|
||||
|
||||
value, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: environment variable %s has invalid boolean value\n", envKey)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// newLogger creates a logger to be used as default logger.
|
||||
// All logs are written to w.
|
||||
func newLogger(w io.Writer, level LogLevel) *loggerT {
|
||||
errorW := ioutil.Discard
|
||||
warningW := ioutil.Discard
|
||||
infoW := ioutil.Discard
|
||||
debugW := ioutil.Discard
|
||||
if isEnabled(DebugEnv) {
|
||||
level = LogLevelDebug
|
||||
}
|
||||
switch level {
|
||||
case LogLevelDebug:
|
||||
debugW = w
|
||||
case LogLevelInfo:
|
||||
infoW = w
|
||||
case LogLevelWarning:
|
||||
warningW = w
|
||||
case LogLevelError:
|
||||
errorW = w
|
||||
}
|
||||
|
||||
// Error logs will be written to errorW, warningW, infoW and debugW.
|
||||
// Warning logs will be written to warningW, infoW and debugW.
|
||||
// Info logs will be written to infoW and debugW.
|
||||
// Debug logs will be written to debugW.
|
||||
var m [4]*log.Logger
|
||||
|
||||
m[LogLevelError] = log.New(io.MultiWriter(debugW, infoW, warningW, errorW),
|
||||
severityName[LogLevelError]+": ", log.LstdFlags)
|
||||
|
||||
m[LogLevelWarning] = log.New(io.MultiWriter(debugW, infoW, warningW),
|
||||
severityName[LogLevelWarning]+": ", log.LstdFlags)
|
||||
|
||||
m[LogLevelInfo] = log.New(io.MultiWriter(debugW, infoW),
|
||||
severityName[LogLevelInfo]+": ", log.LstdFlags)
|
||||
|
||||
m[LogLevelDebug] = log.New(debugW,
|
||||
severityName[LogLevelDebug]+": ", log.LstdFlags)
|
||||
|
||||
return &loggerT{m: m, v: level}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package logger
|
||||
|
||||
import "os"
|
||||
|
||||
type LogLevel int
|
||||
|
||||
const DebugEnv = "SCW_DEBUG"
|
||||
|
||||
const (
|
||||
// LogLevelDebug indicates Debug severity.
|
||||
LogLevelDebug LogLevel = iota
|
||||
// LogLevelInfo indicates Info severity.
|
||||
LogLevelInfo
|
||||
// LogLevelWarning indicates Warning severity.
|
||||
LogLevelWarning
|
||||
// LogLevelError indicates Error severity.
|
||||
LogLevelError
|
||||
)
|
||||
|
||||
// severityName contains the string representation of each severity.
|
||||
var severityName = []string{
|
||||
LogLevelDebug: "DEBUG",
|
||||
LogLevelInfo: "INFO",
|
||||
LogLevelWarning: "WARNING",
|
||||
LogLevelError: "ERROR",
|
||||
}
|
||||
|
||||
// Logger does underlying logging work for scaleway-sdk-go.
|
||||
type Logger interface {
|
||||
// Debugf logs to DEBUG log. Arguments are handled in the manner of fmt.Printf.
|
||||
Debugf(format string, args ...interface{})
|
||||
// Infof logs to INFO log. Arguments are handled in the manner of fmt.Printf.
|
||||
Infof(format string, args ...interface{})
|
||||
// Warningf logs to WARNING log. Arguments are handled in the manner of fmt.Printf.
|
||||
Warningf(format string, args ...interface{})
|
||||
// Errorf logs to ERROR log. Arguments are handled in the manner of fmt.Printf.
|
||||
Errorf(format string, args ...interface{})
|
||||
// ShouldLog reports whether verbosity level l is at least the requested verbose level.
|
||||
ShouldLog(level LogLevel) bool
|
||||
}
|
||||
|
||||
// SetLogger sets logger that is used in by the SDK.
|
||||
// Not mutex-protected, should be called before any scaleway-sdk-go functions.
|
||||
func SetLogger(l Logger) {
|
||||
logger = l
|
||||
}
|
||||
|
||||
// EnableDebugMode enable LogLevelDebug on the default logger.
|
||||
// If a custom logger was provided with SetLogger this method has no effect.
|
||||
func EnableDebugMode() {
|
||||
DefaultLogger.Init(os.Stderr, LogLevelDebug)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Scaleway config
|
||||
|
||||
## TL;DR
|
||||
|
||||
Recommended config file:
|
||||
|
||||
```yaml
|
||||
# Get your credentials on https://console.scaleway.com/project/credentials
|
||||
access_key: SCWXXXXXXXXXXXXXXXXX
|
||||
secret_key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
default_organization_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
default_project_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
default_region: fr-par
|
||||
default_zone: fr-par-1
|
||||
```
|
||||
|
||||
## Config file path
|
||||
|
||||
The function [`GetConfigPath`](https://godoc.org/github.com/scaleway/scaleway-sdk-go/scw#GetConfigPath) will try to locate the config file in the following ways:
|
||||
|
||||
1. Custom directory: `$SCW_CONFIG_PATH`
|
||||
2. [XDG base directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html): `$XDG_CONFIG_HOME/scw/config.yaml`
|
||||
3. Unix home directory: `$HOME/.config/scw/config.yaml`
|
||||
4. Windows home directory: `%USERPROFILE%/.config/scw/config.yaml`
|
||||
|
||||
## V1 config (DEPRECATED)
|
||||
|
||||
The V1 config (AKA legacy config) `.scwrc` is deprecated.
|
||||
To migrate the V1 config to the new format use the function [`MigrateLegacyConfig`](https://godoc.org/github.com/scaleway/scaleway-sdk-go/scw#MigrateLegacyConfig), this will create a [proper config file](#tl-dr) the new [config file path](#config-file-path).
|
||||
|
||||
## Reading config order
|
||||
|
||||
[ClientOption](https://godoc.org/github.com/scaleway/scaleway-sdk-go/scw#ClientOption) ordering will decide the order in which the config should apply:
|
||||
|
||||
```go
|
||||
p, _ := scw.MustLoadConfig().GetActiveProfile()
|
||||
|
||||
scw.NewClient(
|
||||
scw.WithProfile(p), // active profile applies first
|
||||
scw.WithEnv(), // existing env variables may overwrite active profile
|
||||
scw.WithDefaultRegion(scw.RegionFrPar) // any prior region set will be discarded to usr the new one
|
||||
)
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description | Legacy variables |
|
||||
| :----------------------------- | :----------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ |
|
||||
| `$SCW_ACCESS_KEY` | Access key of a token ([get yours](https://console.scaleway.com/project/credentials)) | `$SCALEWAY_ACCESS_KEY` (used by terraform) |
|
||||
| `$SCW_SECRET_KEY` | Secret key of a token ([get yours](https://console.scaleway.com/project/credentials)) | `$SCW_TOKEN` (used by cli), `$SCALEWAY_TOKEN` (used by terraform), `$SCALEWAY_ACCESS_KEY` (used by terraform) |
|
||||
| `$SCW_DEFAULT_ORGANIZATION_ID` | Your default organization ID ([get yours](https://console.scaleway.com/project/credentials)) | `$SCW_ORGANIZATION` (used by cli),`$SCALEWAY_ORGANIZATION` (used by terraform) |
|
||||
| `$SCW_DEFAULT_PROJECT_ID` | Your default project ID ([get yours](https://console.scaleway.com/project/credentials)) | |
|
||||
| `$SCW_DEFAULT_REGION` | Your default [region](https://developers.scaleway.com/en/quickstart/#region-and-zone) | `$SCW_REGION` (used by cli),`$SCALEWAY_REGION` (used by terraform) |
|
||||
| `$SCW_DEFAULT_ZONE` | Your default [availability zone](https://developers.scaleway.com/en/quickstart/#region-and-zone) | `$SCW_ZONE` (used by cli),`$SCALEWAY_ZONE` (used by terraform) |
|
||||
| `$SCW_API_URL` | Url of the API | - |
|
||||
| `$SCW_INSECURE` | Set this to `true` to enable the insecure mode | `$SCW_TLSVERIFY` (inverse flag used by the cli) |
|
||||
| `$SCW_PROFILE` | Set the config profile to use | - |
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/auth"
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||||
"github.com/scaleway/scaleway-sdk-go/logger"
|
||||
)
|
||||
|
||||
// Client is the Scaleway client which performs API requests.
|
||||
//
|
||||
// This client should be passed in the `NewApi` functions whenever an API instance is created.
|
||||
// Creating a Client is done with the `NewClient` function.
|
||||
type Client struct {
|
||||
httpClient httpClient
|
||||
auth auth.Auth
|
||||
apiURL string
|
||||
userAgent string
|
||||
defaultOrganizationID *string
|
||||
defaultProjectID *string
|
||||
defaultRegion *Region
|
||||
defaultZone *Zone
|
||||
defaultPageSize *uint32
|
||||
}
|
||||
|
||||
func defaultOptions() []ClientOption {
|
||||
return []ClientOption{
|
||||
WithoutAuth(),
|
||||
WithAPIURL("https://api.scaleway.com"),
|
||||
withDefaultUserAgent(userAgent),
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient instantiate a new Client object.
|
||||
//
|
||||
// Zero or more ClientOption object can be passed as a parameter.
|
||||
// These options will then be applied to the client.
|
||||
func NewClient(opts ...ClientOption) (*Client, error) {
|
||||
s := newSettings()
|
||||
|
||||
// apply options
|
||||
s.apply(append(defaultOptions(), opts...))
|
||||
|
||||
// validate settings
|
||||
err := s.validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// dial the API
|
||||
if s.httpClient == nil {
|
||||
s.httpClient = newHTTPClient()
|
||||
}
|
||||
|
||||
// insecure mode
|
||||
if s.insecure {
|
||||
logger.Debugf("client: using insecure mode")
|
||||
setInsecureMode(s.httpClient)
|
||||
}
|
||||
|
||||
logger.Debugf("client: using sdk version " + version)
|
||||
|
||||
return &Client{
|
||||
auth: s.token,
|
||||
httpClient: s.httpClient,
|
||||
apiURL: s.apiURL,
|
||||
userAgent: s.userAgent,
|
||||
defaultOrganizationID: s.defaultOrganizationID,
|
||||
defaultProjectID: s.defaultProjectID,
|
||||
defaultRegion: s.defaultRegion,
|
||||
defaultZone: s.defaultZone,
|
||||
defaultPageSize: s.defaultPageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDefaultOrganizationID returns the default organization ID
|
||||
// of the client. This value can be set in the client option
|
||||
// WithDefaultOrganizationID(). Be aware this value can be empty.
|
||||
func (c *Client) GetDefaultOrganizationID() (organizationID string, exists bool) {
|
||||
if c.defaultOrganizationID != nil {
|
||||
return *c.defaultOrganizationID, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetDefaultProjectID returns the default project ID
|
||||
// of the client. This value can be set in the client option
|
||||
// WithDefaultProjectID(). Be aware this value can be empty.
|
||||
func (c *Client) GetDefaultProjectID() (projectID string, exists bool) {
|
||||
if c.defaultProjectID != nil {
|
||||
return *c.defaultProjectID, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetDefaultRegion returns the default region of the client.
|
||||
// This value can be set in the client option
|
||||
// WithDefaultRegion(). Be aware this value can be empty.
|
||||
func (c *Client) GetDefaultRegion() (region Region, exists bool) {
|
||||
if c.defaultRegion != nil {
|
||||
return *c.defaultRegion, true
|
||||
}
|
||||
return Region(""), false
|
||||
}
|
||||
|
||||
// GetDefaultZone returns the default zone of the client.
|
||||
// This value can be set in the client option
|
||||
// WithDefaultZone(). Be aware this value can be empty.
|
||||
func (c *Client) GetDefaultZone() (zone Zone, exists bool) {
|
||||
if c.defaultZone != nil {
|
||||
return *c.defaultZone, true
|
||||
}
|
||||
return Zone(""), false
|
||||
}
|
||||
|
||||
func (c *Client) GetSecretKey() (secretKey string, exists bool) {
|
||||
if token, isToken := c.auth.(*auth.Token); isToken {
|
||||
return token.SecretKey, isToken
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessKey() (accessKey string, exists bool) {
|
||||
if token, isToken := c.auth.(*auth.Token); isToken {
|
||||
return token.AccessKey, isToken
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetDefaultPageSize returns the default page size of the client.
|
||||
// This value can be set in the client option
|
||||
// WithDefaultPageSize(). Be aware this value can be empty.
|
||||
func (c *Client) GetDefaultPageSize() (pageSize uint32, exists bool) {
|
||||
if c.defaultPageSize != nil {
|
||||
return *c.defaultPageSize, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Do performs HTTP request(s) based on the ScalewayRequest object.
|
||||
// RequestOptions are applied prior to doing the request.
|
||||
func (c *Client) Do(req *ScalewayRequest, res interface{}, opts ...RequestOption) (err error) {
|
||||
// apply request options
|
||||
req.apply(opts)
|
||||
|
||||
// validate request options
|
||||
err = req.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.auth == nil {
|
||||
req.auth = c.auth
|
||||
}
|
||||
|
||||
if req.allPages {
|
||||
return c.doListAll(req, res)
|
||||
}
|
||||
|
||||
return c.do(req, res)
|
||||
}
|
||||
|
||||
// requestNumber auto increments on each do().
|
||||
// This allows easy distinguishing of concurrently performed requests in log.
|
||||
var requestNumber uint32
|
||||
|
||||
// do performs a single HTTP request based on the ScalewayRequest object.
|
||||
func (c *Client) do(req *ScalewayRequest, res interface{}) (sdkErr error) {
|
||||
currentRequestNumber := atomic.AddUint32(&requestNumber, 1)
|
||||
|
||||
if req == nil {
|
||||
return errors.New("request must be non-nil")
|
||||
}
|
||||
|
||||
// build url
|
||||
url, sdkErr := req.getURL(c.apiURL)
|
||||
if sdkErr != nil {
|
||||
return sdkErr
|
||||
}
|
||||
logger.Debugf("creating %s request on %s", req.Method, url.String())
|
||||
|
||||
// build request
|
||||
httpRequest, err := http.NewRequest(req.Method, url.String(), req.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create request")
|
||||
}
|
||||
|
||||
httpRequest.Header = req.getAllHeaders(req.auth, c.userAgent, false)
|
||||
|
||||
if req.ctx != nil {
|
||||
httpRequest = httpRequest.WithContext(req.ctx)
|
||||
}
|
||||
|
||||
if logger.ShouldLog(logger.LogLevelDebug) {
|
||||
// Keep original headers (before anonymization)
|
||||
originalHeaders := httpRequest.Header
|
||||
|
||||
// Get anonymized headers
|
||||
httpRequest.Header = req.getAllHeaders(req.auth, c.userAgent, true)
|
||||
|
||||
dump, err := httputil.DumpRequestOut(httpRequest, true)
|
||||
if err != nil {
|
||||
logger.Warningf("cannot dump outgoing request: %s", err)
|
||||
} else {
|
||||
var logString string
|
||||
logString += "\n--------------- Scaleway SDK REQUEST %d : ---------------\n"
|
||||
logString += "%s\n"
|
||||
logString += "---------------------------------------------------------"
|
||||
|
||||
logger.Debugf(logString, currentRequestNumber, dump)
|
||||
}
|
||||
|
||||
// Restore original headers before sending the request
|
||||
httpRequest.Header = originalHeaders
|
||||
}
|
||||
|
||||
// execute request
|
||||
httpResponse, err := c.httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error executing request")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
closeErr := httpResponse.Body.Close()
|
||||
if sdkErr == nil && closeErr != nil {
|
||||
sdkErr = errors.Wrap(closeErr, "could not close http response")
|
||||
}
|
||||
}()
|
||||
if logger.ShouldLog(logger.LogLevelDebug) {
|
||||
dump, err := httputil.DumpResponse(httpResponse, true)
|
||||
if err != nil {
|
||||
logger.Warningf("cannot dump ingoing response: %s", err)
|
||||
} else {
|
||||
var logString string
|
||||
logString += "\n--------------- Scaleway SDK RESPONSE %d : ---------------\n"
|
||||
logString += "%s\n"
|
||||
logString += "----------------------------------------------------------"
|
||||
|
||||
logger.Debugf(logString, currentRequestNumber, dump)
|
||||
}
|
||||
}
|
||||
|
||||
sdkErr = hasResponseError(httpResponse)
|
||||
if sdkErr != nil {
|
||||
return sdkErr
|
||||
}
|
||||
|
||||
if res != nil {
|
||||
contentType := httpResponse.Header.Get("Content-Type")
|
||||
|
||||
switch contentType {
|
||||
case "application/json":
|
||||
err = json.NewDecoder(httpResponse.Body).Decode(&res)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse %s response body", contentType)
|
||||
}
|
||||
default:
|
||||
buffer, isBuffer := res.(io.Writer)
|
||||
if !isBuffer {
|
||||
return errors.Wrap(err, "could not handle %s response body with %T result type", contentType, buffer)
|
||||
}
|
||||
|
||||
_, err := io.Copy(buffer, httpResponse.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not copy %s response body", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle instance API X-Total-Count header
|
||||
xTotalCountStr := httpResponse.Header.Get("X-Total-Count")
|
||||
if legacyLister, isLegacyLister := res.(legacyLister); isLegacyLister && xTotalCountStr != "" {
|
||||
xTotalCount, err := strconv.Atoi(xTotalCountStr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse X-Total-Count header")
|
||||
}
|
||||
legacyLister.UnsafeSetTotalCount(xTotalCount)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type lister interface {
|
||||
UnsafeGetTotalCount() uint32
|
||||
UnsafeAppend(interface{}) (uint32, error)
|
||||
}
|
||||
|
||||
type legacyLister interface {
|
||||
UnsafeSetTotalCount(totalCount int)
|
||||
}
|
||||
|
||||
const maxPageCount uint32 = math.MaxUint32
|
||||
|
||||
// doListAll collects all pages of a List request and aggregate all results on a single response.
|
||||
func (c *Client) doListAll(req *ScalewayRequest, res interface{}) (err error) {
|
||||
// check for lister interface
|
||||
if response, isLister := res.(lister); isLister {
|
||||
pageCount := maxPageCount
|
||||
for page := uint32(1); page <= pageCount; page++ {
|
||||
// set current page
|
||||
req.Query.Set("page", strconv.FormatUint(uint64(page), 10))
|
||||
|
||||
// request the next page
|
||||
nextPage := newVariableFromType(response)
|
||||
err := c.do(req, nextPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// append results
|
||||
pageSize, err := response.UnsafeAppend(nextPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pageSize == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// set total count on first request
|
||||
if pageCount == maxPageCount {
|
||||
totalCount := nextPage.(lister).UnsafeGetTotalCount()
|
||||
pageCount = (totalCount + pageSize - 1) / pageSize
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("%T does not support pagination", res)
|
||||
}
|
||||
|
||||
// newVariableFromType returns a variable set to the zero value of the given type
|
||||
func newVariableFromType(t interface{}) interface{} {
|
||||
// reflect.New always create a pointer, that's why we use reflect.Indirect before
|
||||
return reflect.New(reflect.Indirect(reflect.ValueOf(t)).Type()).Interface()
|
||||
}
|
||||
|
||||
func newHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setInsecureMode(c httpClient) {
|
||||
standardHTTPClient, ok := c.(*http.Client)
|
||||
if !ok {
|
||||
logger.Warningf("client: cannot use insecure mode with HTTP client of type %T", c)
|
||||
return
|
||||
}
|
||||
transportClient, ok := standardHTTPClient.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
logger.Warningf("client: cannot use insecure mode with Transport client of type %T", standardHTTPClient.Transport)
|
||||
return
|
||||
}
|
||||
if transportClient.TLSClientConfig == nil {
|
||||
transportClient.TLSClientConfig = &tls.Config{}
|
||||
}
|
||||
transportClient.TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/auth"
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||||
"github.com/scaleway/scaleway-sdk-go/validation"
|
||||
)
|
||||
|
||||
// ClientOption is a function which applies options to a settings object.
|
||||
type ClientOption func(*settings)
|
||||
|
||||
// httpClient wraps the net/http Client Do method
|
||||
type httpClient interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// WithHTTPClient client option allows passing a custom http.Client which will be used for all requests.
|
||||
func WithHTTPClient(httpClient httpClient) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.httpClient = httpClient
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutAuth client option sets the client token to an empty token.
|
||||
func WithoutAuth() ClientOption {
|
||||
return func(s *settings) {
|
||||
s.token = auth.NewNoAuth()
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuth client option sets the client access key and secret key.
|
||||
func WithAuth(accessKey, secretKey string) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.token = auth.NewToken(accessKey, secretKey)
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPIURL client option overrides the API URL of the Scaleway API to the given URL.
|
||||
func WithAPIURL(apiURL string) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.apiURL = apiURL
|
||||
}
|
||||
}
|
||||
|
||||
// WithInsecure client option enables insecure transport on the client.
|
||||
func WithInsecure() ClientOption {
|
||||
return func(s *settings) {
|
||||
s.insecure = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserAgent client option append a user agent to the default user agent of the SDK.
|
||||
func WithUserAgent(ua string) ClientOption {
|
||||
return func(s *settings) {
|
||||
if s.userAgent != "" && ua != "" {
|
||||
s.userAgent += " "
|
||||
}
|
||||
s.userAgent += ua
|
||||
}
|
||||
}
|
||||
|
||||
// withDefaultUserAgent client option overrides the default user agent of the SDK.
|
||||
func withDefaultUserAgent(ua string) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.userAgent = ua
|
||||
}
|
||||
}
|
||||
|
||||
// WithProfile client option configures a client from the given profile.
|
||||
func WithProfile(p *Profile) ClientOption {
|
||||
return func(s *settings) {
|
||||
accessKey := ""
|
||||
if p.AccessKey != nil {
|
||||
accessKey = *p.AccessKey
|
||||
}
|
||||
|
||||
if p.SecretKey != nil {
|
||||
s.token = auth.NewToken(accessKey, *p.SecretKey)
|
||||
}
|
||||
|
||||
if p.APIURL != nil {
|
||||
s.apiURL = *p.APIURL
|
||||
}
|
||||
|
||||
if p.Insecure != nil {
|
||||
s.insecure = *p.Insecure
|
||||
}
|
||||
|
||||
if p.DefaultOrganizationID != nil {
|
||||
organizationID := *p.DefaultOrganizationID
|
||||
s.defaultOrganizationID = &organizationID
|
||||
}
|
||||
|
||||
if p.DefaultProjectID != nil {
|
||||
projectID := *p.DefaultProjectID
|
||||
s.defaultProjectID = &projectID
|
||||
}
|
||||
|
||||
if p.DefaultRegion != nil {
|
||||
defaultRegion := Region(*p.DefaultRegion)
|
||||
s.defaultRegion = &defaultRegion
|
||||
}
|
||||
|
||||
if p.DefaultZone != nil {
|
||||
defaultZone := Zone(*p.DefaultZone)
|
||||
s.defaultZone = &defaultZone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithProfile client option configures a client from the environment variables.
|
||||
func WithEnv() ClientOption {
|
||||
return WithProfile(LoadEnvProfile())
|
||||
}
|
||||
|
||||
// WithDefaultOrganizationID client option sets the client default organization ID.
|
||||
//
|
||||
// It will be used as the default value of the organization_id field in all requests made with this client.
|
||||
func WithDefaultOrganizationID(organizationID string) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.defaultOrganizationID = &organizationID
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultProjectID client option sets the client default project ID.
|
||||
//
|
||||
// It will be used as the default value of the projectID field in all requests made with this client.
|
||||
func WithDefaultProjectID(projectID string) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.defaultProjectID = &projectID
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultRegion client option sets the client default region.
|
||||
//
|
||||
// It will be used as the default value of the region field in all requests made with this client.
|
||||
func WithDefaultRegion(region Region) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.defaultRegion = ®ion
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultZone client option sets the client default zone.
|
||||
//
|
||||
// It will be used as the default value of the zone field in all requests made with this client.
|
||||
func WithDefaultZone(zone Zone) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.defaultZone = &zone
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultPageSize client option overrides the default page size of the SDK.
|
||||
//
|
||||
// It will be used as the default value of the page_size field in all requests made with this client.
|
||||
func WithDefaultPageSize(pageSize uint32) ClientOption {
|
||||
return func(s *settings) {
|
||||
s.defaultPageSize = &pageSize
|
||||
}
|
||||
}
|
||||
|
||||
// settings hold the values of all client options
|
||||
type settings struct {
|
||||
apiURL string
|
||||
token auth.Auth
|
||||
userAgent string
|
||||
httpClient httpClient
|
||||
insecure bool
|
||||
defaultOrganizationID *string
|
||||
defaultProjectID *string
|
||||
defaultRegion *Region
|
||||
defaultZone *Zone
|
||||
defaultPageSize *uint32
|
||||
}
|
||||
|
||||
func newSettings() *settings {
|
||||
return &settings{}
|
||||
}
|
||||
|
||||
func (s *settings) apply(opts []ClientOption) {
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *settings) validate() error {
|
||||
// Auth.
|
||||
if s.token == nil {
|
||||
// It should not happen, WithoutAuth option is used by default.
|
||||
panic(errors.New("no credential option provided"))
|
||||
}
|
||||
if token, isToken := s.token.(*auth.Token); isToken {
|
||||
if token.AccessKey == "" {
|
||||
return NewInvalidClientOptionError("access key cannot be empty")
|
||||
}
|
||||
if !validation.IsAccessKey(token.AccessKey) {
|
||||
return NewInvalidClientOptionError("invalid access key format '%s', expected SCWXXXXXXXXXXXXXXXXX format", token.AccessKey)
|
||||
}
|
||||
if token.SecretKey == "" {
|
||||
return NewInvalidClientOptionError("secret key cannot be empty")
|
||||
}
|
||||
if !validation.IsSecretKey(token.SecretKey) {
|
||||
return NewInvalidClientOptionError("invalid secret key format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", token.SecretKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Default Organization ID.
|
||||
if s.defaultOrganizationID != nil {
|
||||
if *s.defaultOrganizationID == "" {
|
||||
return NewInvalidClientOptionError("default organization ID cannot be empty")
|
||||
}
|
||||
if !validation.IsOrganizationID(*s.defaultOrganizationID) {
|
||||
return NewInvalidClientOptionError("invalid organization ID format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", *s.defaultOrganizationID)
|
||||
}
|
||||
}
|
||||
|
||||
// Default Project ID.
|
||||
if s.defaultProjectID != nil {
|
||||
if *s.defaultProjectID == "" {
|
||||
return NewInvalidClientOptionError("default project ID cannot be empty")
|
||||
}
|
||||
if !validation.IsProjectID(*s.defaultProjectID) {
|
||||
return NewInvalidClientOptionError("invalid project ID format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", *s.defaultProjectID)
|
||||
}
|
||||
}
|
||||
|
||||
// Default Region.
|
||||
if s.defaultRegion != nil {
|
||||
if *s.defaultRegion == "" {
|
||||
return NewInvalidClientOptionError("default region cannot be empty")
|
||||
}
|
||||
if !validation.IsRegion(string(*s.defaultRegion)) {
|
||||
regions := []string(nil)
|
||||
for _, r := range AllRegions {
|
||||
regions = append(regions, string(r))
|
||||
}
|
||||
return NewInvalidClientOptionError("invalid default region format '%s', available regions are: %s", *s.defaultRegion, strings.Join(regions, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// Default Zone.
|
||||
if s.defaultZone != nil {
|
||||
if *s.defaultZone == "" {
|
||||
return NewInvalidClientOptionError("default zone cannot be empty")
|
||||
}
|
||||
if !validation.IsZone(string(*s.defaultZone)) {
|
||||
zones := []string(nil)
|
||||
for _, z := range AllZones {
|
||||
zones = append(zones, string(z))
|
||||
}
|
||||
return NewInvalidClientOptionError("invalid default zone format '%s', available zones are: %s", *s.defaultZone, strings.Join(zones, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// API URL.
|
||||
if !validation.IsURL(s.apiURL) {
|
||||
return NewInvalidClientOptionError("invalid url %s", s.apiURL)
|
||||
}
|
||||
if s.apiURL[len(s.apiURL)-1:] == "/" {
|
||||
return NewInvalidClientOptionError("invalid url %s it should not have a trailing slash", s.apiURL)
|
||||
}
|
||||
|
||||
// TODO: check for max s.defaultPageSize
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||||
"github.com/scaleway/scaleway-sdk-go/logger"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// configV1 is a Scaleway CLI configuration file
|
||||
type configV1 struct {
|
||||
// Organization is the identifier of the Scaleway organization
|
||||
Organization string `json:"organization"`
|
||||
|
||||
// Token is the authentication token for the Scaleway organization
|
||||
Token string `json:"token"`
|
||||
|
||||
// Version is the actual version of scw CLI
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func unmarshalConfV1(content []byte) (*configV1, error) {
|
||||
var config configV1
|
||||
err := json.Unmarshal(content, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, err
|
||||
}
|
||||
|
||||
func (v1 *configV1) toV2() *Config {
|
||||
return &Config{
|
||||
Profile: Profile{
|
||||
DefaultOrganizationID: &v1.Organization,
|
||||
DefaultProjectID: &v1.Organization, // v1 config is not aware of project, so default project is set to organization ID
|
||||
SecretKey: &v1.Token,
|
||||
// ignore v1 version
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MigrateLegacyConfig will migrate the legacy config to the V2 when none exist yet.
|
||||
// Returns a boolean set to true when the migration happened.
|
||||
// TODO: get accesskey from account?
|
||||
func MigrateLegacyConfig() (bool, error) {
|
||||
// STEP 1: try to load config file V2
|
||||
v2Path, v2PathOk := getConfigV2FilePath()
|
||||
if !v2PathOk || fileExist(v2Path) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// STEP 2: try to load config file V1
|
||||
v1Path, v1PathOk := getConfigV1FilePath()
|
||||
if !v1PathOk {
|
||||
return false, nil
|
||||
}
|
||||
file, err := ioutil.ReadFile(v1Path)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
confV1, err := unmarshalConfV1(file)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "content of config file %s is invalid json", v1Path)
|
||||
}
|
||||
|
||||
// STEP 3: create dir
|
||||
err = os.MkdirAll(filepath.Dir(v2Path), 0700)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "mkdir did not work on %s", filepath.Dir(v2Path))
|
||||
}
|
||||
|
||||
// STEP 4: marshal yaml config
|
||||
newConfig := confV1.toV2()
|
||||
file, err = yaml.Marshal(newConfig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// STEP 5: save config
|
||||
err = ioutil.WriteFile(v2Path, file, defaultConfigPermission)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "cannot write file %s", v2Path)
|
||||
}
|
||||
|
||||
// STEP 6: log success
|
||||
logger.Warningf("migrated existing config to %s", v2Path)
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StringPtr returns a pointer to the string value passed in.
|
||||
func StringPtr(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
// StringSlicePtr converts a slice of string values into a slice of
|
||||
// string pointers
|
||||
func StringSlicePtr(src []string) []*string {
|
||||
dst := make([]*string, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// StringsPtr returns a pointer to the []string value passed in.
|
||||
func StringsPtr(v []string) *[]string {
|
||||
return &v
|
||||
}
|
||||
|
||||
// StringsSlicePtr converts a slice of []string values into a slice of
|
||||
// []string pointers
|
||||
func StringsSlicePtr(src [][]string) []*[]string {
|
||||
dst := make([]*[]string, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// BytesPtr returns a pointer to the []byte value passed in.
|
||||
func BytesPtr(v []byte) *[]byte {
|
||||
return &v
|
||||
}
|
||||
|
||||
// BytesSlicePtr converts a slice of []byte values into a slice of
|
||||
// []byte pointers
|
||||
func BytesSlicePtr(src [][]byte) []*[]byte {
|
||||
dst := make([]*[]byte, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// BoolPtr returns a pointer to the bool value passed in.
|
||||
func BoolPtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
// BoolSlicePtr converts a slice of bool values into a slice of
|
||||
// bool pointers
|
||||
func BoolSlicePtr(src []bool) []*bool {
|
||||
dst := make([]*bool, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Int32Ptr returns a pointer to the int32 value passed in.
|
||||
func Int32Ptr(v int32) *int32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Int32SlicePtr converts a slice of int32 values into a slice of
|
||||
// int32 pointers
|
||||
func Int32SlicePtr(src []int32) []*int32 {
|
||||
dst := make([]*int32, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Int64Ptr returns a pointer to the int64 value passed in.
|
||||
func Int64Ptr(v int64) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Int64SlicePtr converts a slice of int64 values into a slice of
|
||||
// int64 pointers
|
||||
func Int64SlicePtr(src []int64) []*int64 {
|
||||
dst := make([]*int64, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Uint32Ptr returns a pointer to the uint32 value passed in.
|
||||
func Uint32Ptr(v uint32) *uint32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Uint32SlicePtr converts a slice of uint32 values into a slice of
|
||||
// uint32 pointers
|
||||
func Uint32SlicePtr(src []uint32) []*uint32 {
|
||||
dst := make([]*uint32, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Uint64Ptr returns a pointer to the uint64 value passed in.
|
||||
func Uint64Ptr(v uint64) *uint64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Uint64SlicePtr converts a slice of uint64 values into a slice of
|
||||
// uint64 pointers
|
||||
func Uint64SlicePtr(src []uint64) []*uint64 {
|
||||
dst := make([]*uint64, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Float32Ptr returns a pointer to the float32 value passed in.
|
||||
func Float32Ptr(v float32) *float32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Float32SlicePtr converts a slice of float32 values into a slice of
|
||||
// float32 pointers
|
||||
func Float32SlicePtr(src []float32) []*float32 {
|
||||
dst := make([]*float32, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Float64Ptr returns a pointer to the float64 value passed in.
|
||||
func Float64Ptr(v float64) *float64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Float64SlicePtr converts a slice of float64 values into a slice of
|
||||
// float64 pointers
|
||||
func Float64SlicePtr(src []float64) []*float64 {
|
||||
dst := make([]*float64, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// TimeDurationPtr returns a pointer to the Duration value passed in.
|
||||
func TimeDurationPtr(v time.Duration) *time.Duration {
|
||||
return &v
|
||||
}
|
||||
|
||||
// TimePtr returns a pointer to the Time value passed in.
|
||||
func TimePtr(v time.Time) *time.Time {
|
||||
return &v
|
||||
}
|
||||
|
||||
// SizePtr returns a pointer to the Size value passed in.
|
||||
func SizePtr(v Size) *Size {
|
||||
return &v
|
||||
}
|
||||
|
||||
// IPPtr returns a pointer to the net.IP value passed in.
|
||||
func IPPtr(v net.IP) *net.IP {
|
||||
return &v
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||||
"github.com/scaleway/scaleway-sdk-go/logger"
|
||||
)
|
||||
|
||||
// ServiceInfo contains API metadata
|
||||
// These metadata are only here for debugging. Do not rely on these values
|
||||
type ServiceInfo struct {
|
||||
// Name is the name of the API
|
||||
Name string `json:"name"`
|
||||
|
||||
// Description is a human readable description for the API
|
||||
Description string `json:"description"`
|
||||
|
||||
// Version is the version of the API
|
||||
Version string `json:"version"`
|
||||
|
||||
// DocumentationURL is the a web url where the documentation of the API can be found
|
||||
DocumentationURL *string `json:"documentation_url"`
|
||||
}
|
||||
|
||||
// File is the structure used to receive / send a file from / to the API
|
||||
type File struct {
|
||||
// Name of the file
|
||||
Name string `json:"name"`
|
||||
|
||||
// ContentType used in the HTTP header `Content-Type`
|
||||
ContentType string `json:"content_type"`
|
||||
|
||||
// Content of the file
|
||||
Content io.Reader `json:"content"`
|
||||
}
|
||||
|
||||
func (f *File) UnmarshalJSON(b []byte) error {
|
||||
type file File
|
||||
var tmpFile struct {
|
||||
file
|
||||
Content []byte `json:"content"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(b, &tmpFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpFile.file.Content = bytes.NewReader(tmpFile.Content)
|
||||
|
||||
*f = File(tmpFile.file)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Money represents an amount of money with its currency type.
|
||||
type Money struct {
|
||||
// CurrencyCode is the 3-letter currency code defined in ISO 4217.
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
|
||||
// Units is the whole units of the amount.
|
||||
// For example if `currencyCode` is `"USD"`, then 1 unit is one US dollar.
|
||||
Units int64 `json:"units"`
|
||||
|
||||
// Nanos is the number of nano (10^-9) units of the amount.
|
||||
// The value must be between -999,999,999 and +999,999,999 inclusive.
|
||||
// If `units` is positive, `nanos` must be positive or zero.
|
||||
// If `units` is zero, `nanos` can be positive, zero, or negative.
|
||||
// If `units` is negative, `nanos` must be negative or zero.
|
||||
// For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000.
|
||||
Nanos int32 `json:"nanos"`
|
||||
}
|
||||
|
||||
// NewMoneyFromFloat converts a float with currency to a Money.
|
||||
//
|
||||
// value: The float value.
|
||||
// currencyCode: The 3-letter currency code defined in ISO 4217.
|
||||
// precision: The number of digits after the decimal point used to parse the nanos part of the value.
|
||||
//
|
||||
// Examples:
|
||||
// - (value = 1.3333, precision = 2) => Money{Units = 1, Nanos = 330000000}
|
||||
// - (value = 1.123456789, precision = 9) => Money{Units = 1, Nanos = 123456789}
|
||||
func NewMoneyFromFloat(value float64, currencyCode string, precision int) *Money {
|
||||
if precision > 9 {
|
||||
panic(fmt.Errorf("max precision is 9"))
|
||||
}
|
||||
|
||||
strValue := strconv.FormatFloat(value, 'f', precision, 64)
|
||||
units, nanos, err := splitFloatString(strValue)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &Money{
|
||||
CurrencyCode: currencyCode,
|
||||
Units: units,
|
||||
Nanos: nanos,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of Money.
|
||||
func (m Money) String() string {
|
||||
currencySignsByCodes := map[string]string{
|
||||
"EUR": "€",
|
||||
"USD": "$",
|
||||
}
|
||||
|
||||
currencySign, currencySignFound := currencySignsByCodes[m.CurrencyCode]
|
||||
if !currencySignFound {
|
||||
logger.Debugf("%s currency code is not supported", m.CurrencyCode)
|
||||
currencySign = m.CurrencyCode
|
||||
}
|
||||
|
||||
cents := fmt.Sprintf("%09d", m.Nanos)
|
||||
cents = cents[:2] + strings.TrimRight(cents[2:], "0")
|
||||
|
||||
return fmt.Sprintf("%s %d.%s", currencySign, m.Units, cents)
|
||||
}
|
||||
|
||||
// ToFloat converts a Money object to a float.
|
||||
func (m Money) ToFloat() float64 {
|
||||
return float64(m.Units) + float64(m.Nanos)/1e9
|
||||
}
|
||||
|
||||
// Size represents a size in bytes.
|
||||
type Size uint64
|
||||
|
||||
const (
|
||||
B Size = 1
|
||||
KB = 1000 * B
|
||||
MB = 1000 * KB
|
||||
GB = 1000 * MB
|
||||
TB = 1000 * GB
|
||||
PB = 1000 * TB
|
||||
)
|
||||
|
||||
// String returns the string representation of a Size.
|
||||
func (s Size) String() string {
|
||||
return fmt.Sprintf("%d", s)
|
||||
}
|
||||
|
||||
// TimeSeries represents a time series that could be used for graph purposes.
|
||||
type TimeSeries struct {
|
||||
// Name of the metric.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Points contains all the points that composed the series.
|
||||
Points []*TimeSeriesPoint `json:"points"`
|
||||
|
||||
// Metadata contains some string metadata related to a metric.
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// TimeSeriesPoint represents a point of a time series.
|
||||
type TimeSeriesPoint struct {
|
||||
Timestamp time.Time
|
||||
Value float32
|
||||
}
|
||||
|
||||
func (tsp TimeSeriesPoint) MarshalJSON() ([]byte, error) {
|
||||
timestamp := tsp.Timestamp.Format(time.RFC3339)
|
||||
value, err := json.Marshal(tsp.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(`["` + timestamp + `",` + string(value) + "]"), nil
|
||||
}
|
||||
|
||||
func (tsp *TimeSeriesPoint) UnmarshalJSON(b []byte) error {
|
||||
point := [2]interface{}{}
|
||||
|
||||
err := json.Unmarshal(b, &point)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(point) != 2 {
|
||||
return fmt.Errorf("invalid point array")
|
||||
}
|
||||
|
||||
strTimestamp, isStrTimestamp := point[0].(string)
|
||||
if !isStrTimestamp {
|
||||
return fmt.Errorf("%s timestamp is not a string in RFC 3339 format", point[0])
|
||||
}
|
||||
timestamp, err := time.Parse(time.RFC3339, strTimestamp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s timestamp is not in RFC 3339 format", point[0])
|
||||
}
|
||||
tsp.Timestamp = timestamp
|
||||
|
||||
// By default, JSON unmarshal a float in float64 but the TimeSeriesPoint is a float32 value.
|
||||
value, isValue := point[1].(float64)
|
||||
if !isValue {
|
||||
return fmt.Errorf("%s is not a valid float32 value", point[1])
|
||||
}
|
||||
tsp.Value = float32(value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPNet inherits net.IPNet and represents an IP network.
|
||||
type IPNet struct {
|
||||
net.IPNet
|
||||
}
|
||||
|
||||
func (n IPNet) MarshalJSON() ([]byte, error) {
|
||||
value := n.String()
|
||||
if value == "<nil>" {
|
||||
value = ""
|
||||
}
|
||||
return []byte(`"` + value + `"`), nil
|
||||
}
|
||||
|
||||
func (n *IPNet) UnmarshalJSON(b []byte) error {
|
||||
var str string
|
||||
|
||||
err := json.Unmarshal(b, &str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if str == "" {
|
||||
*n = IPNet{}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ip := net.ParseIP(str); {
|
||||
case ip.To4() != nil:
|
||||
str += "/32"
|
||||
case ip.To16() != nil:
|
||||
str += "/128"
|
||||
}
|
||||
|
||||
ip, value, err := net.ParseCIDR(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value.IP = ip
|
||||
n.IPNet = *value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Duration represents a signed, fixed-length span of time represented as a
|
||||
// count of seconds and fractions of seconds at nanosecond resolution. It is
|
||||
// independent of any calendar and concepts like "day" or "month". It is related
|
||||
// to Timestamp in that the difference between two Timestamp values is a Duration
|
||||
// and it can be added or subtracted from a Timestamp.
|
||||
// Range is approximately +-10,000 years.
|
||||
type Duration struct {
|
||||
Seconds int64
|
||||
Nanos int32
|
||||
}
|
||||
|
||||
func (d *Duration) ToTimeDuration() *time.Duration {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
timeDuration := time.Duration(d.Nanos) + time.Duration(d.Seconds/1e9)
|
||||
return &timeDuration
|
||||
}
|
||||
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
nanos := d.Nanos
|
||||
if nanos < 0 {
|
||||
nanos = -nanos
|
||||
}
|
||||
|
||||
return []byte(`"` + fmt.Sprintf("%d.%09d", d.Seconds, nanos) + `s"`), nil
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
if string(b) == "null" {
|
||||
return nil
|
||||
}
|
||||
var str string
|
||||
|
||||
err := json.Unmarshal(b, &str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if str == "" {
|
||||
*d = Duration{}
|
||||
return nil
|
||||
}
|
||||
|
||||
seconds, nanos, err := splitFloatString(strings.TrimRight(str, "s"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = Duration{
|
||||
Seconds: seconds,
|
||||
Nanos: nanos,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitFloatString splits a float represented in a string, and returns its units (left-coma part) and nanos (right-coma part).
|
||||
// E.g.:
|
||||
// "3" ==> units = 3 | nanos = 0
|
||||
// "3.14" ==> units = 3 | nanos = 14*1e7
|
||||
// "-3.14" ==> units = -3 | nanos = -14*1e7
|
||||
func splitFloatString(input string) (units int64, nanos int32, err error) {
|
||||
parts := strings.SplitN(input, ".", 2)
|
||||
|
||||
// parse units as int64
|
||||
units, err = strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(err, "invalid units")
|
||||
}
|
||||
|
||||
// handle nanos
|
||||
if len(parts) == 2 {
|
||||
// add leading zeros
|
||||
strNanos := parts[1] + "000000000"[len(parts[1]):]
|
||||
|
||||
// parse nanos as int32
|
||||
n, err := strconv.ParseUint(strNanos, 10, 32)
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(err, "invalid nanos")
|
||||
}
|
||||
|
||||
nanos = int32(n)
|
||||
}
|
||||
|
||||
if units < 0 {
|
||||
nanos = -nanos
|
||||
}
|
||||
|
||||
return units, nanos, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/logger"
|
||||
)
|
||||
|
||||
// Environment variables
|
||||
const (
|
||||
// Up-to-date
|
||||
ScwCacheDirEnv = "SCW_CACHE_DIR"
|
||||
ScwConfigPathEnv = "SCW_CONFIG_PATH"
|
||||
ScwAccessKeyEnv = "SCW_ACCESS_KEY"
|
||||
ScwSecretKeyEnv = "SCW_SECRET_KEY" // #nosec G101
|
||||
ScwActiveProfileEnv = "SCW_PROFILE"
|
||||
ScwAPIURLEnv = "SCW_API_URL"
|
||||
ScwInsecureEnv = "SCW_INSECURE"
|
||||
ScwDefaultOrganizationIDEnv = "SCW_DEFAULT_ORGANIZATION_ID"
|
||||
ScwDefaultProjectIDEnv = "SCW_DEFAULT_PROJECT_ID"
|
||||
ScwDefaultRegionEnv = "SCW_DEFAULT_REGION"
|
||||
ScwDefaultZoneEnv = "SCW_DEFAULT_ZONE"
|
||||
DebugEnv = logger.DebugEnv
|
||||
|
||||
// All deprecated (cli&terraform)
|
||||
terraformAccessKeyEnv = "SCALEWAY_ACCESS_KEY" // used both as access key and secret key
|
||||
terraformSecretKeyEnv = "SCALEWAY_TOKEN"
|
||||
terraformOrganizationEnv = "SCALEWAY_ORGANIZATION"
|
||||
terraformRegionEnv = "SCALEWAY_REGION"
|
||||
cliTLSVerifyEnv = "SCW_TLSVERIFY"
|
||||
cliOrganizationEnv = "SCW_ORGANIZATION"
|
||||
cliRegionEnv = "SCW_REGION"
|
||||
cliSecretKeyEnv = "SCW_TOKEN"
|
||||
|
||||
// TBD
|
||||
//cliVerboseEnv = "SCW_VERBOSE_API"
|
||||
//cliDebugEnv = "DEBUG"
|
||||
//cliNoCheckVersionEnv = "SCW_NOCHECKVERSION"
|
||||
//cliTestWithRealAPIEnv = "TEST_WITH_REAL_API"
|
||||
//cliSecureExecEnv = "SCW_SECURE_EXEC"
|
||||
//cliGatewayEnv = "SCW_GATEWAY"
|
||||
//cliSensitiveEnv = "SCW_SENSITIVE"
|
||||
//cliAccountAPIEnv = "SCW_ACCOUNT_API"
|
||||
//cliMetadataAPIEnv = "SCW_METADATA_API"
|
||||
//cliMarketPlaceAPIEnv = "SCW_MARKETPLACE_API"
|
||||
//cliComputePar1APIEnv = "SCW_COMPUTE_PAR1_API"
|
||||
//cliComputeAms1APIEnv = "SCW_COMPUTE_AMS1_API"
|
||||
//cliCommercialTypeEnv = "SCW_COMMERCIAL_TYPE"
|
||||
//cliTargetArchEnv = "SCW_TARGET_ARCH"
|
||||
)
|
||||
|
||||
const (
|
||||
v1RegionFrPar = "par1"
|
||||
v1RegionNlAms = "ams1"
|
||||
)
|
||||
|
||||
func LoadEnvProfile() *Profile {
|
||||
p := &Profile{}
|
||||
|
||||
accessKey, _, envExist := getEnv(ScwAccessKeyEnv, terraformAccessKeyEnv)
|
||||
if envExist {
|
||||
p.AccessKey = &accessKey
|
||||
}
|
||||
|
||||
secretKey, _, envExist := getEnv(ScwSecretKeyEnv, cliSecretKeyEnv, terraformSecretKeyEnv, terraformAccessKeyEnv)
|
||||
if envExist {
|
||||
p.SecretKey = &secretKey
|
||||
}
|
||||
|
||||
apiURL, _, envExist := getEnv(ScwAPIURLEnv)
|
||||
if envExist {
|
||||
p.APIURL = &apiURL
|
||||
}
|
||||
|
||||
insecureValue, envKey, envExist := getEnv(ScwInsecureEnv, cliTLSVerifyEnv)
|
||||
if envExist {
|
||||
insecure, err := strconv.ParseBool(insecureValue)
|
||||
if err != nil {
|
||||
logger.Warningf("env variable %s cannot be parsed: %s is invalid boolean", envKey, insecureValue)
|
||||
}
|
||||
|
||||
if envKey == cliTLSVerifyEnv {
|
||||
insecure = !insecure // TLSVerify is the inverse of Insecure
|
||||
}
|
||||
|
||||
p.Insecure = &insecure
|
||||
}
|
||||
|
||||
organizationID, _, envExist := getEnv(ScwDefaultOrganizationIDEnv, cliOrganizationEnv, terraformOrganizationEnv)
|
||||
if envExist {
|
||||
p.DefaultOrganizationID = &organizationID
|
||||
}
|
||||
|
||||
projectID, _, envExist := getEnv(ScwDefaultProjectIDEnv)
|
||||
if envExist {
|
||||
p.DefaultProjectID = &projectID
|
||||
}
|
||||
|
||||
region, _, envExist := getEnv(ScwDefaultRegionEnv, cliRegionEnv, terraformRegionEnv)
|
||||
if envExist {
|
||||
region = v1RegionToV2(region)
|
||||
p.DefaultRegion = ®ion
|
||||
}
|
||||
|
||||
zone, _, envExist := getEnv(ScwDefaultZoneEnv)
|
||||
if envExist {
|
||||
p.DefaultZone = &zone
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func getEnv(upToDateKey string, deprecatedKeys ...string) (string, string, bool) {
|
||||
value, exist := os.LookupEnv(upToDateKey)
|
||||
if exist {
|
||||
logger.Debugf("reading value from %s", upToDateKey)
|
||||
return value, upToDateKey, true
|
||||
}
|
||||
|
||||
for _, key := range deprecatedKeys {
|
||||
value, exist := os.LookupEnv(key)
|
||||
if exist {
|
||||
logger.Debugf("reading value from %s", key)
|
||||
logger.Warningf("%s is deprecated, please use %s instead", key, upToDateKey)
|
||||
return value, key, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func v1RegionToV2(region string) string {
|
||||
switch region {
|
||||
case v1RegionFrPar:
|
||||
logger.Warningf("par1 is a deprecated name for region, use fr-par instead")
|
||||
return "fr-par"
|
||||
case v1RegionNlAms:
|
||||
logger.Warningf("ams1 is a deprecated name for region, use nl-ams instead")
|
||||
return "nl-ams"
|
||||
default:
|
||||
return region
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,551 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||||
"github.com/scaleway/scaleway-sdk-go/validation"
|
||||
)
|
||||
|
||||
// SdkError is a base interface for all Scaleway SDK errors.
|
||||
type SdkError interface {
|
||||
Error() string
|
||||
IsScwSdkError()
|
||||
}
|
||||
|
||||
// ResponseError is an error type for the Scaleway API
|
||||
type ResponseError struct {
|
||||
// Message is a human-friendly error message
|
||||
Message string `json:"message"`
|
||||
|
||||
// Type is a string code that defines the kind of error. This field is only used by instance API
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
// Resource is a string code that defines the resource concerned by the error. This field is only used by instance API
|
||||
Resource string `json:"resource,omitempty"`
|
||||
|
||||
// Fields contains detail about validation error. This field is only used by instance API
|
||||
Fields map[string][]string `json:"fields,omitempty"`
|
||||
|
||||
// StatusCode is the HTTP status code received
|
||||
StatusCode int `json:"-"`
|
||||
|
||||
// Status is the HTTP status received
|
||||
Status string `json:"-"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
func (e *ResponseError) UnmarshalJSON(b []byte) error {
|
||||
type tmpResponseError ResponseError
|
||||
tmp := tmpResponseError(*e)
|
||||
|
||||
err := json.Unmarshal(b, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp.Message = strings.ToLower(tmp.Message)
|
||||
|
||||
*e = ResponseError(tmp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsScwSdkError implement SdkError interface
|
||||
func (e *ResponseError) IsScwSdkError() {}
|
||||
func (e *ResponseError) Error() string {
|
||||
s := fmt.Sprintf("scaleway-sdk-go: http error %s", e.Status)
|
||||
|
||||
if e.Resource != "" {
|
||||
s = fmt.Sprintf("%s: resource %s", s, e.Resource)
|
||||
}
|
||||
|
||||
if e.Message != "" {
|
||||
s = fmt.Sprintf("%s: %s", s, e.Message)
|
||||
}
|
||||
|
||||
if len(e.Fields) > 0 {
|
||||
s = fmt.Sprintf("%s: %v", s, e.Fields)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
func (e *ResponseError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
// hasResponseError returns an SdkError when the HTTP status is not OK.
|
||||
func hasResponseError(res *http.Response) error {
|
||||
if res.StatusCode >= 200 && res.StatusCode <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newErr := &ResponseError{
|
||||
StatusCode: res.StatusCode,
|
||||
Status: res.Status,
|
||||
}
|
||||
|
||||
if res.Body == nil {
|
||||
return newErr
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot read error response body")
|
||||
}
|
||||
newErr.RawBody = body
|
||||
|
||||
// The error content is not encoded in JSON, only returns HTTP data.
|
||||
if res.Header.Get("Content-Type") != "application/json" {
|
||||
newErr.Message = res.Status
|
||||
return newErr
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, newErr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse error response body")
|
||||
}
|
||||
|
||||
err = unmarshalStandardError(newErr.Type, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = unmarshalNonStandardError(newErr.Type, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return newErr
|
||||
}
|
||||
|
||||
func unmarshalStandardError(errorType string, body []byte) error {
|
||||
var stdErr SdkError
|
||||
|
||||
switch errorType {
|
||||
case "invalid_arguments":
|
||||
stdErr = &InvalidArgumentsError{RawBody: body}
|
||||
case "quotas_exceeded":
|
||||
stdErr = &QuotasExceededError{RawBody: body}
|
||||
case "transient_state":
|
||||
stdErr = &TransientStateError{RawBody: body}
|
||||
case "not_found":
|
||||
stdErr = &ResourceNotFoundError{RawBody: body}
|
||||
case "locked":
|
||||
stdErr = &ResourceLockedError{RawBody: body}
|
||||
case "permissions_denied":
|
||||
stdErr = &PermissionsDeniedError{RawBody: body}
|
||||
case "out_of_stock":
|
||||
stdErr = &OutOfStockError{RawBody: body}
|
||||
case "resource_expired":
|
||||
stdErr = &ResourceExpiredError{RawBody: body}
|
||||
case "denied_authentication":
|
||||
stdErr = &DeniedAuthenticationError{RawBody: body}
|
||||
case "precondition_failed":
|
||||
stdErr = &PreconditionFailedError{RawBody: body}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal(body, stdErr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse error %s response body", errorType)
|
||||
}
|
||||
|
||||
return stdErr
|
||||
}
|
||||
|
||||
func unmarshalNonStandardError(errorType string, body []byte) error {
|
||||
switch errorType {
|
||||
// Only in instance API.
|
||||
|
||||
case "unknown_resource":
|
||||
unknownResourceError := &UnknownResource{RawBody: body}
|
||||
err := json.Unmarshal(body, unknownResourceError)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse error %s response body", errorType)
|
||||
}
|
||||
return unknownResourceError.ToResourceNotFoundError()
|
||||
|
||||
case "invalid_request_error":
|
||||
invalidRequestError := &InvalidRequestError{RawBody: body}
|
||||
err := json.Unmarshal(body, invalidRequestError)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse error %s response body", errorType)
|
||||
}
|
||||
|
||||
invalidArgumentsError := invalidRequestError.ToInvalidArgumentsError()
|
||||
if invalidArgumentsError != nil {
|
||||
return invalidArgumentsError
|
||||
}
|
||||
|
||||
quotasExceededError := invalidRequestError.ToQuotasExceededError()
|
||||
if quotasExceededError != nil {
|
||||
return quotasExceededError
|
||||
}
|
||||
|
||||
// At this point, the invalid_request_error is not an InvalidArgumentsError and
|
||||
// the default marshalling will be used.
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type InvalidArgumentsErrorDetail struct {
|
||||
ArgumentName string `json:"argument_name"`
|
||||
Reason string `json:"reason"`
|
||||
HelpMessage string `json:"help_message"`
|
||||
}
|
||||
|
||||
type InvalidArgumentsError struct {
|
||||
Details []InvalidArgumentsErrorDetail `json:"details"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e *InvalidArgumentsError) IsScwSdkError() {}
|
||||
func (e *InvalidArgumentsError) Error() string {
|
||||
invalidArgs := make([]string, len(e.Details))
|
||||
for i, d := range e.Details {
|
||||
invalidArgs[i] = d.ArgumentName
|
||||
switch d.Reason {
|
||||
case "unknown":
|
||||
invalidArgs[i] += " is invalid for unexpected reason"
|
||||
case "required":
|
||||
invalidArgs[i] += " is required"
|
||||
case "format":
|
||||
invalidArgs[i] += " is wrongly formatted"
|
||||
case "constraint":
|
||||
invalidArgs[i] += " does not respect constraint"
|
||||
}
|
||||
if d.HelpMessage != "" {
|
||||
invalidArgs[i] += ", " + d.HelpMessage
|
||||
}
|
||||
}
|
||||
|
||||
return "scaleway-sdk-go: invalid argument(s): " + strings.Join(invalidArgs, "; ")
|
||||
}
|
||||
func (e *InvalidArgumentsError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
// UnknownResource is only returned by the instance API.
|
||||
// Warning: this is not a standard error.
|
||||
type UnknownResource struct {
|
||||
Message string `json:"message"`
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// ToSdkError returns a standard error InvalidArgumentsError or nil Fields is nil.
|
||||
func (e *UnknownResource) ToResourceNotFoundError() SdkError {
|
||||
resourceNotFound := &ResourceNotFoundError{
|
||||
RawBody: e.RawBody,
|
||||
}
|
||||
|
||||
messageParts := strings.Split(e.Message, `"`)
|
||||
|
||||
// Some errors uses ' and not "
|
||||
if len(messageParts) == 1 {
|
||||
messageParts = strings.Split(e.Message, "'")
|
||||
}
|
||||
|
||||
switch len(messageParts) {
|
||||
case 2: // message like: `"111..." not found`
|
||||
resourceNotFound.ResourceID = messageParts[0]
|
||||
case 3: // message like: `Security Group "111..." not found`
|
||||
resourceNotFound.ResourceID = messageParts[1]
|
||||
// transform `Security group ` to `security_group`
|
||||
resourceNotFound.Resource = strings.ReplaceAll(strings.ToLower(strings.TrimSpace(messageParts[0])), " ", "_")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if !validation.IsUUID(resourceNotFound.ResourceID) {
|
||||
return nil
|
||||
}
|
||||
return resourceNotFound
|
||||
}
|
||||
|
||||
// InvalidRequestError is only returned by the instance API.
|
||||
// Warning: this is not a standard error.
|
||||
type InvalidRequestError struct {
|
||||
Message string `json:"message"`
|
||||
|
||||
Fields map[string][]string `json:"fields"`
|
||||
|
||||
Resource string `json:"resource"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// ToSdkError returns a standard error InvalidArgumentsError or nil Fields is nil.
|
||||
func (e *InvalidRequestError) ToInvalidArgumentsError() SdkError {
|
||||
// If error has no fields, it is not an InvalidArgumentsError.
|
||||
if e.Fields == nil || len(e.Fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
invalidArguments := &InvalidArgumentsError{
|
||||
RawBody: e.RawBody,
|
||||
}
|
||||
fieldNames := []string(nil)
|
||||
for fieldName := range e.Fields {
|
||||
fieldNames = append(fieldNames, fieldName)
|
||||
}
|
||||
sort.Strings(fieldNames)
|
||||
for _, fieldName := range fieldNames {
|
||||
for _, message := range e.Fields[fieldName] {
|
||||
invalidArguments.Details = append(invalidArguments.Details, InvalidArgumentsErrorDetail{
|
||||
ArgumentName: fieldName,
|
||||
Reason: "constraint",
|
||||
HelpMessage: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
return invalidArguments
|
||||
}
|
||||
|
||||
func (e *InvalidRequestError) ToQuotasExceededError() SdkError {
|
||||
if !strings.Contains(strings.ToLower(e.Message), "quota exceeded for this resource") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &QuotasExceededError{
|
||||
Details: []QuotasExceededErrorDetail{
|
||||
{
|
||||
Resource: e.Resource,
|
||||
Quota: 0,
|
||||
Current: 0,
|
||||
},
|
||||
},
|
||||
RawBody: e.RawBody,
|
||||
}
|
||||
}
|
||||
|
||||
type QuotasExceededErrorDetail struct {
|
||||
Resource string `json:"resource"`
|
||||
Quota uint32 `json:"quota"`
|
||||
Current uint32 `json:"current"`
|
||||
}
|
||||
|
||||
type QuotasExceededError struct {
|
||||
Details []QuotasExceededErrorDetail `json:"details"`
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e *QuotasExceededError) IsScwSdkError() {}
|
||||
func (e *QuotasExceededError) Error() string {
|
||||
invalidArgs := make([]string, len(e.Details))
|
||||
for i, d := range e.Details {
|
||||
invalidArgs[i] = fmt.Sprintf("%s has reached its quota (%d/%d)", d.Resource, d.Current, d.Current)
|
||||
}
|
||||
|
||||
return "scaleway-sdk-go: quota exceeded(s): " + strings.Join(invalidArgs, "; ")
|
||||
}
|
||||
func (e *QuotasExceededError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
type PermissionsDeniedError struct {
|
||||
Details []struct {
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
} `json:"details"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e *PermissionsDeniedError) IsScwSdkError() {}
|
||||
func (e *PermissionsDeniedError) Error() string {
|
||||
invalidArgs := make([]string, len(e.Details))
|
||||
for i, d := range e.Details {
|
||||
invalidArgs[i] = fmt.Sprintf("%s %s", d.Action, d.Resource)
|
||||
}
|
||||
|
||||
return "scaleway-sdk-go: insufficient permissions: " + strings.Join(invalidArgs, "; ")
|
||||
}
|
||||
func (e *PermissionsDeniedError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
type TransientStateError struct {
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
CurrentState string `json:"current_state"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e *TransientStateError) IsScwSdkError() {}
|
||||
func (e *TransientStateError) Error() string {
|
||||
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is in a transient state: %s", e.Resource, e.ResourceID, e.CurrentState)
|
||||
}
|
||||
func (e *TransientStateError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
type ResourceNotFoundError struct {
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e *ResourceNotFoundError) IsScwSdkError() {}
|
||||
func (e *ResourceNotFoundError) Error() string {
|
||||
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is not found", e.Resource, e.ResourceID)
|
||||
}
|
||||
func (e *ResourceNotFoundError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
type ResourceLockedError struct {
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e *ResourceLockedError) IsScwSdkError() {}
|
||||
func (e *ResourceLockedError) Error() string {
|
||||
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is locked", e.Resource, e.ResourceID)
|
||||
}
|
||||
func (e *ResourceLockedError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
type OutOfStockError struct {
|
||||
Resource string `json:"resource"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e *OutOfStockError) IsScwSdkError() {}
|
||||
func (e *OutOfStockError) Error() string {
|
||||
return fmt.Sprintf("scaleway-sdk-go: resource %s is out of stock", e.Resource)
|
||||
}
|
||||
func (e *OutOfStockError) GetRawBody() json.RawMessage {
|
||||
return e.RawBody
|
||||
}
|
||||
|
||||
// InvalidClientOptionError indicates that at least one of client data has been badly provided for the client creation.
|
||||
type InvalidClientOptionError struct {
|
||||
errorType string
|
||||
}
|
||||
|
||||
func NewInvalidClientOptionError(format string, a ...interface{}) *InvalidClientOptionError {
|
||||
return &InvalidClientOptionError{errorType: fmt.Sprintf(format, a...)}
|
||||
}
|
||||
|
||||
// IsScwSdkError implements the SdkError interface
|
||||
func (e InvalidClientOptionError) IsScwSdkError() {}
|
||||
func (e InvalidClientOptionError) Error() string {
|
||||
return fmt.Sprintf("scaleway-sdk-go: %s", e.errorType)
|
||||
}
|
||||
|
||||
// ConfigFileNotFound indicates that the config file could not be found
|
||||
type ConfigFileNotFoundError struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func configFileNotFound(path string) *ConfigFileNotFoundError {
|
||||
return &ConfigFileNotFoundError{path: path}
|
||||
}
|
||||
|
||||
// ConfigFileNotFoundError implements the SdkError interface
|
||||
func (e ConfigFileNotFoundError) IsScwSdkError() {}
|
||||
func (e ConfigFileNotFoundError) Error() string {
|
||||
return fmt.Sprintf("scaleway-sdk-go: cannot read config file %s: no such file or directory", e.path)
|
||||
}
|
||||
|
||||
// ResourceExpiredError implements the SdkError interface
|
||||
type ResourceExpiredError struct {
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
ExpiredSince time.Time `json:"expired_since"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
func (r ResourceExpiredError) Error() string {
|
||||
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s expired since %s", r.Resource, r.ResourceID, r.ExpiredSince.String())
|
||||
}
|
||||
|
||||
func (r ResourceExpiredError) IsScwSdkError() {}
|
||||
|
||||
// DeniedAuthenticationError implements the SdkError interface
|
||||
type DeniedAuthenticationError struct {
|
||||
Method string `json:"method"`
|
||||
Reason string `json:"reason"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
func (r DeniedAuthenticationError) Error() string {
|
||||
var reason string
|
||||
var method string
|
||||
|
||||
switch r.Method {
|
||||
case "unknown_method":
|
||||
method = "unknown method"
|
||||
case "jwt":
|
||||
method = "JWT"
|
||||
case "api_key":
|
||||
method = "API key"
|
||||
}
|
||||
|
||||
switch r.Reason {
|
||||
case "unknown_reason":
|
||||
reason = "unknown reason"
|
||||
case "invalid_argument":
|
||||
reason = "invalid " + method + " format or empty value"
|
||||
case "not_found":
|
||||
reason = method + " does not exist"
|
||||
case "expired":
|
||||
reason = method + " is expired"
|
||||
}
|
||||
return fmt.Sprintf("scaleway-sdk-go: denied authentication: %s", reason)
|
||||
}
|
||||
|
||||
func (r DeniedAuthenticationError) IsScwSdkError() {}
|
||||
|
||||
// PreconditionFailedError implements the SdkError interface
|
||||
type PreconditionFailedError struct {
|
||||
Precondition string `json:"method"`
|
||||
HelpMessage string `json:"help_message"`
|
||||
|
||||
RawBody json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
func (r PreconditionFailedError) Error() string {
|
||||
var msg string
|
||||
switch r.Precondition {
|
||||
case "unknown_precondition":
|
||||
msg = "unknown precondition"
|
||||
case "resource_still_in_use":
|
||||
msg = "resource is still in use"
|
||||
case "attribute_must_be_set":
|
||||
msg = "attribute must be set"
|
||||
}
|
||||
if r.HelpMessage != "" {
|
||||
msg += ", " + r.HelpMessage
|
||||
}
|
||||
|
||||
return fmt.Sprintf("scaleway-sdk-go: precondition failed: %s", msg)
|
||||
}
|
||||
|
||||
func (r PreconditionFailedError) IsScwSdkError() {}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||||
"github.com/scaleway/scaleway-sdk-go/logger"
|
||||
"github.com/scaleway/scaleway-sdk-go/validation"
|
||||
)
|
||||
|
||||
// localityPartsSeparator is the separator used in Zone and Region
|
||||
const localityPartsSeparator = "-"
|
||||
|
||||
// Zone is an availability zone
|
||||
type Zone string
|
||||
|
||||
const (
|
||||
// ZoneFrPar1 represents the fr-par-1 zone
|
||||
ZoneFrPar1 = Zone("fr-par-1")
|
||||
// ZoneFrPar2 represents the fr-par-2 zone
|
||||
ZoneFrPar2 = Zone("fr-par-2")
|
||||
// ZoneFrPar3 represents the fr-par-3 zone
|
||||
ZoneFrPar3 = Zone("fr-par-3")
|
||||
// ZoneNlAms1 represents the nl-ams-1 zone
|
||||
ZoneNlAms1 = Zone("nl-ams-1")
|
||||
// ZoneNlAms2 represents the nl-ams-2 zone
|
||||
ZoneNlAms2 = Zone("nl-ams-2")
|
||||
// ZonePlWaw1 represents the pl-waw-1 zone
|
||||
ZonePlWaw1 = Zone("pl-waw-1")
|
||||
)
|
||||
|
||||
var (
|
||||
// AllZones is an array that list all zones
|
||||
AllZones = []Zone{
|
||||
ZoneFrPar1,
|
||||
ZoneFrPar2,
|
||||
ZoneFrPar3,
|
||||
ZoneNlAms1,
|
||||
ZoneNlAms2,
|
||||
ZonePlWaw1,
|
||||
}
|
||||
)
|
||||
|
||||
// Exists checks whether a zone exists
|
||||
func (zone Zone) Exists() bool {
|
||||
for _, z := range AllZones {
|
||||
if z == zone {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// String returns a Zone as a string
|
||||
func (zone Zone) String() string {
|
||||
return string(zone)
|
||||
}
|
||||
|
||||
// Region returns the parent Region for the Zone.
|
||||
// Manipulates the string directly to allow unlisted zones formatted as xx-yyy-z.
|
||||
func (zone Zone) Region() (Region, error) {
|
||||
zoneStr := zone.String()
|
||||
if !validation.IsZone(zoneStr) {
|
||||
return "", fmt.Errorf("invalid zone '%v'", zoneStr)
|
||||
}
|
||||
zoneParts := strings.Split(zoneStr, localityPartsSeparator)
|
||||
return Region(strings.Join(zoneParts[:2], localityPartsSeparator)), nil
|
||||
}
|
||||
|
||||
// Region is a geographical location
|
||||
type Region string
|
||||
|
||||
const (
|
||||
// RegionFrPar represents the fr-par region
|
||||
RegionFrPar = Region("fr-par")
|
||||
// RegionNlAms represents the nl-ams region
|
||||
RegionNlAms = Region("nl-ams")
|
||||
// RegionPlWaw represents the pl-waw region
|
||||
RegionPlWaw = Region("pl-waw")
|
||||
)
|
||||
|
||||
var (
|
||||
// AllRegions is an array that list all regions
|
||||
AllRegions = []Region{
|
||||
RegionFrPar,
|
||||
RegionNlAms,
|
||||
RegionPlWaw,
|
||||
}
|
||||
)
|
||||
|
||||
// Exists checks whether a region exists
|
||||
func (region Region) Exists() bool {
|
||||
for _, r := range AllRegions {
|
||||
if r == region {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetZones is a function that returns the zones for the specified region
|
||||
func (region Region) GetZones() []Zone {
|
||||
switch region {
|
||||
case RegionFrPar:
|
||||
return []Zone{ZoneFrPar1, ZoneFrPar2, ZoneFrPar3}
|
||||
case RegionNlAms:
|
||||
return []Zone{ZoneNlAms1, ZoneNlAms2}
|
||||
case RegionPlWaw:
|
||||
return []Zone{ZonePlWaw1}
|
||||
default:
|
||||
return []Zone{}
|
||||
}
|
||||
}
|
||||
|
||||
// ParseZone parses a string value into a Zone and returns an error if it has a bad format.
|
||||
func ParseZone(zone string) (Zone, error) {
|
||||
switch zone {
|
||||
case "par1":
|
||||
// would be triggered by API market place
|
||||
// logger.Warningf("par1 is a deprecated name for zone, use fr-par-1 instead")
|
||||
return ZoneFrPar1, nil
|
||||
case "ams1":
|
||||
// would be triggered by API market place
|
||||
// logger.Warningf("ams1 is a deprecated name for zone, use nl-ams-1 instead")
|
||||
return ZoneNlAms1, nil
|
||||
default:
|
||||
if !validation.IsZone(zone) {
|
||||
zones := []string(nil)
|
||||
for _, z := range AllZones {
|
||||
zones = append(zones, string(z))
|
||||
}
|
||||
return "", errors.New("bad zone format, available zones are: %s", strings.Join(zones, ", "))
|
||||
}
|
||||
|
||||
newZone := Zone(zone)
|
||||
if !newZone.Exists() {
|
||||
logger.Infof("%s is an unknown zone\n", newZone)
|
||||
}
|
||||
return newZone, nil
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the Unmarshaler interface for a Zone.
|
||||
// this to call ParseZone on the string input and return the correct Zone object.
|
||||
func (zone *Zone) UnmarshalJSON(input []byte) error {
|
||||
// parse input value as string
|
||||
var stringValue string
|
||||
err := json.Unmarshal(input, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse string as Zone
|
||||
*zone, err = ParseZone(stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseRegion parses a string value into a Region and returns an error if it has a bad format.
|
||||
func ParseRegion(region string) (Region, error) {
|
||||
switch region {
|
||||
case "par1":
|
||||
// would be triggered by API market place
|
||||
// logger.Warningf("par1 is a deprecated name for region, use fr-par instead")
|
||||
return RegionFrPar, nil
|
||||
case "ams1":
|
||||
// would be triggered by API market place
|
||||
// logger.Warningf("ams1 is a deprecated name for region, use nl-ams instead")
|
||||
return RegionNlAms, nil
|
||||
default:
|
||||
if !validation.IsRegion(region) {
|
||||
regions := []string(nil)
|
||||
for _, r := range AllRegions {
|
||||
regions = append(regions, string(r))
|
||||
}
|
||||
return "", errors.New("bad region format, available regions are: %s", strings.Join(regions, ", "))
|
||||
}
|
||||
|
||||
newRegion := Region(region)
|
||||
if !newRegion.Exists() {
|
||||
logger.Infof("%s is an unknown region\n", newRegion)
|
||||
}
|
||||
return newRegion, nil
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the Unmarshaler interface for a Region.
|
||||
// this to call ParseRegion on the string input and return the correct Region object.
|
||||
func (region *Region) UnmarshalJSON(input []byte) error {
|
||||
// parse input value as string
|
||||
var stringValue string
|
||||
err := json.Unmarshal(input, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse string as Region
|
||||
*region, err = ParseRegion(stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a Region as a string
|
||||
func (region Region) String() string {
|
||||
return string(region)
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
// XDG wiki: https://wiki.archlinux.org/index.php/XDG_Base_Directory
|
||||
xdgConfigDirEnv = "XDG_CONFIG_HOME"
|
||||
xdgCacheDirEnv = "XDG_CACHE_HOME"
|
||||
|
||||
unixHomeDirEnv = "HOME"
|
||||
windowsHomeDirEnv = "USERPROFILE"
|
||||
|
||||
defaultConfigFileName = "config.yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoHomeDir errors when no user directory is found
|
||||
ErrNoHomeDir = errors.New("user home directory not found")
|
||||
)
|
||||
|
||||
// GetCacheDirectory returns the default cache directory.
|
||||
// Cache directory is based on the following priority order:
|
||||
// - $SCW_CACHE_DIR
|
||||
// - $XDG_CACHE_HOME/scw
|
||||
// - $HOME/.cache/scw
|
||||
// - $USERPROFILE/.cache/scw
|
||||
func GetCacheDirectory() string {
|
||||
cacheDir := ""
|
||||
switch {
|
||||
case os.Getenv(ScwCacheDirEnv) != "":
|
||||
cacheDir = os.Getenv(ScwCacheDirEnv)
|
||||
case os.Getenv(xdgCacheDirEnv) != "":
|
||||
cacheDir = filepath.Join(os.Getenv(xdgCacheDirEnv), "scw")
|
||||
case os.Getenv(unixHomeDirEnv) != "":
|
||||
cacheDir = filepath.Join(os.Getenv(unixHomeDirEnv), ".cache", "scw")
|
||||
case os.Getenv(windowsHomeDirEnv) != "":
|
||||
cacheDir = filepath.Join(os.Getenv(windowsHomeDirEnv), ".cache", "scw")
|
||||
default:
|
||||
// TODO: fallback on local folder?
|
||||
}
|
||||
|
||||
// Clean the cache directory path when exiting the function
|
||||
return filepath.Clean(cacheDir)
|
||||
}
|
||||
|
||||
// GetConfigPath returns the default path.
|
||||
// Default path is based on the following priority order:
|
||||
// - $SCW_CONFIG_PATH
|
||||
// - $XDG_CONFIG_HOME/scw/config.yaml
|
||||
// - $HOME/.config/scw/config.yaml
|
||||
// - $USERPROFILE/.config/scw/config.yaml
|
||||
func GetConfigPath() string {
|
||||
configPath := os.Getenv(ScwConfigPathEnv)
|
||||
if configPath == "" {
|
||||
configPath, _ = getConfigV2FilePath()
|
||||
}
|
||||
return filepath.Clean(configPath)
|
||||
}
|
||||
|
||||
// getConfigV2FilePath returns the path to the v2 config file
|
||||
func getConfigV2FilePath() (string, bool) {
|
||||
configDir, err := GetScwConfigDir()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Clean(filepath.Join(configDir, defaultConfigFileName)), true
|
||||
}
|
||||
|
||||
// getConfigV1FilePath returns the path to the v1 config file
|
||||
func getConfigV1FilePath() (string, bool) {
|
||||
path, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Clean(filepath.Join(path, ".scwrc")), true
|
||||
}
|
||||
|
||||
// GetScwConfigDir returns the path to scw config folder
|
||||
func GetScwConfigDir() (string, error) {
|
||||
if xdgPath := os.Getenv(xdgConfigDirEnv); xdgPath != "" {
|
||||
return filepath.Join(xdgPath, "scw"), nil
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(homeDir, ".config", "scw"), nil
|
||||
}
|
||||
|
||||
func fileExist(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
return err == nil
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/auth"
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||||
)
|
||||
|
||||
// ScalewayRequest contains all the contents related to performing a request on the Scaleway API.
|
||||
type ScalewayRequest struct {
|
||||
Method string
|
||||
Path string
|
||||
Headers http.Header
|
||||
Query url.Values
|
||||
Body io.Reader
|
||||
|
||||
// request options
|
||||
ctx context.Context
|
||||
auth auth.Auth
|
||||
allPages bool
|
||||
}
|
||||
|
||||
// getAllHeaders constructs a http.Header object and aggregates all headers into the object.
|
||||
func (req *ScalewayRequest) getAllHeaders(token auth.Auth, userAgent string, anonymized bool) http.Header {
|
||||
var allHeaders http.Header
|
||||
if anonymized {
|
||||
allHeaders = token.AnonymizedHeaders()
|
||||
} else {
|
||||
allHeaders = token.Headers()
|
||||
}
|
||||
|
||||
allHeaders.Set("User-Agent", userAgent)
|
||||
if req.Body != nil {
|
||||
allHeaders.Set("Content-Type", "application/json")
|
||||
}
|
||||
for key, value := range req.Headers {
|
||||
allHeaders.Del(key)
|
||||
for _, v := range value {
|
||||
allHeaders.Add(key, v)
|
||||
}
|
||||
}
|
||||
|
||||
return allHeaders
|
||||
}
|
||||
|
||||
// getURL constructs a URL based on the base url and the client.
|
||||
func (req *ScalewayRequest) getURL(baseURL string) (*url.URL, error) {
|
||||
url, err := url.Parse(baseURL + req.Path)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid url %s: %s", baseURL+req.Path, err)
|
||||
}
|
||||
url.RawQuery = req.Query.Encode()
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// SetBody json marshal the given body and write the json content type
|
||||
// to the request. It also catches when body is a file.
|
||||
func (req *ScalewayRequest) SetBody(body interface{}) error {
|
||||
var contentType string
|
||||
var content io.Reader
|
||||
|
||||
switch b := body.(type) {
|
||||
case *File:
|
||||
contentType = b.ContentType
|
||||
content = b.Content
|
||||
case io.Reader:
|
||||
contentType = "text/plain"
|
||||
content = b
|
||||
default:
|
||||
buf, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentType = "application/json"
|
||||
content = bytes.NewReader(buf)
|
||||
}
|
||||
|
||||
if req.Headers == nil {
|
||||
req.Headers = http.Header{}
|
||||
}
|
||||
|
||||
req.Headers.Set("Content-Type", contentType)
|
||||
req.Body = content
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *ScalewayRequest) apply(opts []RequestOption) {
|
||||
for _, opt := range opts {
|
||||
opt(req)
|
||||
}
|
||||
}
|
||||
|
||||
func (req *ScalewayRequest) validate() error {
|
||||
// nothing so far
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/scaleway/scaleway-sdk-go/internal/auth"
|
||||
)
|
||||
|
||||
// RequestOption is a function that applies options to a ScalewayRequest.
|
||||
type RequestOption func(*ScalewayRequest)
|
||||
|
||||
// WithContext request option sets the context of a ScalewayRequest
|
||||
func WithContext(ctx context.Context) RequestOption {
|
||||
return func(s *ScalewayRequest) {
|
||||
s.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllPages aggregate all pages in the response of a List request.
|
||||
// Will error when pagination is not supported on the request.
|
||||
func WithAllPages() RequestOption {
|
||||
return func(s *ScalewayRequest) {
|
||||
s.allPages = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuthRequest overwrites the client access key and secret key used in the request.
|
||||
func WithAuthRequest(accessKey, secretKey string) RequestOption {
|
||||
return func(s *ScalewayRequest) {
|
||||
s.auth = auth.NewToken(accessKey, secretKey)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package scw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// TODO: versioning process
|
||||
const version = "v1.0.0-beta.7+dev"
|
||||
|
||||
var userAgent = fmt.Sprintf("scaleway-sdk-go/%s (%s; %s; %s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// Package validation provides format validation functions.
|
||||
package validation
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
isUUIDRegexp = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||
isRegionRegex = regexp.MustCompile("^[a-z]{2}-[a-z]{3}$")
|
||||
isZoneRegex = regexp.MustCompile("^[a-z]{2}-[a-z]{3}-[1-9]$")
|
||||
isAccessKey = regexp.MustCompile("^SCW[A-Z0-9]{17}$")
|
||||
isEmailRegexp = regexp.MustCompile("^.+@.+$")
|
||||
)
|
||||
|
||||
// IsUUID returns true if the given string has a valid UUID format.
|
||||
func IsUUID(s string) bool {
|
||||
return isUUIDRegexp.MatchString(s)
|
||||
}
|
||||
|
||||
// IsAccessKey returns true if the given string has a valid Scaleway access key format.
|
||||
func IsAccessKey(s string) bool {
|
||||
return isAccessKey.MatchString(s)
|
||||
}
|
||||
|
||||
// IsSecretKey returns true if the given string has a valid Scaleway secret key format.
|
||||
func IsSecretKey(s string) bool {
|
||||
return IsUUID(s)
|
||||
}
|
||||
|
||||
// IsOrganizationID returns true if the given string has a valid Scaleway organization ID format.
|
||||
func IsOrganizationID(s string) bool {
|
||||
return IsUUID(s)
|
||||
}
|
||||
|
||||
// IsProjectID returns true if the given string has a valid Scaleway project ID format.
|
||||
func IsProjectID(s string) bool {
|
||||
return IsUUID(s)
|
||||
}
|
||||
|
||||
// IsRegion returns true if the given string has a valid region format.
|
||||
func IsRegion(s string) bool {
|
||||
return isRegionRegex.MatchString(s)
|
||||
}
|
||||
|
||||
// IsZone returns true if the given string has a valid zone format.
|
||||
func IsZone(s string) bool {
|
||||
return isZoneRegex.MatchString(s)
|
||||
}
|
||||
|
||||
// IsURL returns true if the given string has a valid URL format.
|
||||
func IsURL(s string) bool {
|
||||
_, err := url.Parse(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsEmail returns true if the given string has an email format.
|
||||
func IsEmail(v string) bool {
|
||||
return isEmailRegexp.MatchString(v)
|
||||
}
|
||||
|
|
@ -817,6 +817,13 @@ github.com/ryanuber/go-glob
|
|||
# github.com/sahilm/fuzzy v0.1.0
|
||||
## explicit
|
||||
github.com/sahilm/fuzzy
|
||||
# github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9
|
||||
## explicit; go 1.17
|
||||
github.com/scaleway/scaleway-sdk-go/internal/auth
|
||||
github.com/scaleway/scaleway-sdk-go/internal/errors
|
||||
github.com/scaleway/scaleway-sdk-go/logger
|
||||
github.com/scaleway/scaleway-sdk-go/scw
|
||||
github.com/scaleway/scaleway-sdk-go/validation
|
||||
# github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529
|
||||
## explicit
|
||||
github.com/sean-/seed
|
||||
|
|
|
|||
Loading…
Reference in New Issue