Merge pull request #14322 from Mia-Cross/scw_nodeup

Scaleway init and nodeup
This commit is contained in:
Kubernetes Prow Robot 2022-09-26 06:03:44 -07:00 committed by GitHub
commit db3ec0c72f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 3525 additions and 2 deletions

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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
}

View File

@ -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

View File

@ -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 ""
}

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

191
vendor/github.com/scaleway/scaleway-sdk-go/LICENSE generated vendored Normal file
View File

@ -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.

View File

@ -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
}

View 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{}
}

View File

@ -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"
}
}

View 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...),
}
}

View 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}
}

View File

@ -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)
}

View File

@ -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 | - |

View File

@ -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
}

View File

@ -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 = &region
}
}
// 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
}

View File

@ -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, youll 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

144
vendor/github.com/scaleway/scaleway-sdk-go/scw/env.go generated vendored Normal file
View File

@ -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 = &region
}
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
}
}

View File

@ -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() {}

View File

@ -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)
}

98
vendor/github.com/scaleway/scaleway-sdk-go/scw/path.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}

7
vendor/modules.txt generated vendored
View File

@ -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