diff --git a/go.mod b/go.mod index 67fa110999..3f01c0aa98 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/pkg/sftp v1.13.5 github.com/prometheus/client_golang v1.13.0 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 github.com/sergi/go-diff v1.2.0 github.com/spf13/cobra v1.5.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index a669b90d27..abfbfcd103 100644 --- a/go.sum +++ b/go.sum @@ -1049,6 +1049,8 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 h1:0roa6gXKgyta64uqh52AQG3wzZXH21unn+ltzQSXML0= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc= diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/LICENSE b/vendor/github.com/scaleway/scaleway-sdk-go/LICENSE new file mode 100644 index 0000000000..2712cc51ad --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/LICENSE @@ -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. diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/auth.go b/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/auth.go new file mode 100644 index 0000000000..f7ee31fcb0 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/auth.go @@ -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 +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/no_auth.go b/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/no_auth.go new file mode 100644 index 0000000000..2181c57c25 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/no_auth.go @@ -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{} +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/token.go b/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/token.go new file mode 100644 index 0000000000..6a09027557 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/internal/auth/token.go @@ -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" + } +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/internal/errors/error.go b/vendor/github.com/scaleway/scaleway-sdk-go/internal/errors/error.go new file mode 100644 index 0000000000..351dd7084a --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/internal/errors/error.go @@ -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...), + } +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/logger/default_logger.go b/vendor/github.com/scaleway/scaleway-sdk-go/logger/default_logger.go new file mode 100644 index 0000000000..5b4f77e21b --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/logger/default_logger.go @@ -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} +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/logger/logger.go b/vendor/github.com/scaleway/scaleway-sdk-go/logger/logger.go new file mode 100644 index 0000000000..98006145eb --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/logger/logger.go @@ -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) +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/README.md b/vendor/github.com/scaleway/scaleway-sdk-go/scw/README.md new file mode 100644 index 0000000000..064989e37d --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/README.md @@ -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 | - | diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/client.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/client.go new file mode 100644 index 0000000000..0bdb13bca8 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/client.go @@ -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 +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/client_option.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/client_option.go new file mode 100644 index 0000000000..73b1df24d3 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/client_option.go @@ -0,0 +1,268 @@ +package scw + +import ( + "net/http" + "strings" + + "github.com/scaleway/scaleway-sdk-go/internal/auth" + "github.com/scaleway/scaleway-sdk-go/internal/errors" + "github.com/scaleway/scaleway-sdk-go/validation" +) + +// ClientOption is a function which applies options to a settings object. +type ClientOption func(*settings) + +// httpClient wraps the net/http Client Do method +type httpClient interface { + Do(*http.Request) (*http.Response, error) +} + +// WithHTTPClient client option allows passing a custom http.Client which will be used for all requests. +func WithHTTPClient(httpClient httpClient) ClientOption { + return func(s *settings) { + s.httpClient = httpClient + } +} + +// WithoutAuth client option sets the client token to an empty token. +func WithoutAuth() ClientOption { + return func(s *settings) { + s.token = auth.NewNoAuth() + } +} + +// WithAuth client option sets the client access key and secret key. +func WithAuth(accessKey, secretKey string) ClientOption { + return func(s *settings) { + s.token = auth.NewToken(accessKey, secretKey) + } +} + +// WithAPIURL client option overrides the API URL of the Scaleway API to the given URL. +func WithAPIURL(apiURL string) ClientOption { + return func(s *settings) { + s.apiURL = apiURL + } +} + +// WithInsecure client option enables insecure transport on the client. +func WithInsecure() ClientOption { + return func(s *settings) { + s.insecure = true + } +} + +// WithUserAgent client option append a user agent to the default user agent of the SDK. +func WithUserAgent(ua string) ClientOption { + return func(s *settings) { + if s.userAgent != "" && ua != "" { + s.userAgent += " " + } + s.userAgent += ua + } +} + +// withDefaultUserAgent client option overrides the default user agent of the SDK. +func withDefaultUserAgent(ua string) ClientOption { + return func(s *settings) { + s.userAgent = ua + } +} + +// WithProfile client option configures a client from the given profile. +func WithProfile(p *Profile) ClientOption { + return func(s *settings) { + accessKey := "" + if p.AccessKey != nil { + accessKey = *p.AccessKey + } + + if p.SecretKey != nil { + s.token = auth.NewToken(accessKey, *p.SecretKey) + } + + if p.APIURL != nil { + s.apiURL = *p.APIURL + } + + if p.Insecure != nil { + s.insecure = *p.Insecure + } + + if p.DefaultOrganizationID != nil { + organizationID := *p.DefaultOrganizationID + s.defaultOrganizationID = &organizationID + } + + if p.DefaultProjectID != nil { + projectID := *p.DefaultProjectID + s.defaultProjectID = &projectID + } + + if p.DefaultRegion != nil { + defaultRegion := Region(*p.DefaultRegion) + s.defaultRegion = &defaultRegion + } + + if p.DefaultZone != nil { + defaultZone := Zone(*p.DefaultZone) + s.defaultZone = &defaultZone + } + } +} + +// WithProfile client option configures a client from the environment variables. +func WithEnv() ClientOption { + return WithProfile(LoadEnvProfile()) +} + +// WithDefaultOrganizationID client option sets the client default organization ID. +// +// It will be used as the default value of the organization_id field in all requests made with this client. +func WithDefaultOrganizationID(organizationID string) ClientOption { + return func(s *settings) { + s.defaultOrganizationID = &organizationID + } +} + +// WithDefaultProjectID client option sets the client default project ID. +// +// It will be used as the default value of the projectID field in all requests made with this client. +func WithDefaultProjectID(projectID string) ClientOption { + return func(s *settings) { + s.defaultProjectID = &projectID + } +} + +// WithDefaultRegion client option sets the client default region. +// +// It will be used as the default value of the region field in all requests made with this client. +func WithDefaultRegion(region Region) ClientOption { + return func(s *settings) { + s.defaultRegion = ®ion + } +} + +// WithDefaultZone client option sets the client default zone. +// +// It will be used as the default value of the zone field in all requests made with this client. +func WithDefaultZone(zone Zone) ClientOption { + return func(s *settings) { + s.defaultZone = &zone + } +} + +// WithDefaultPageSize client option overrides the default page size of the SDK. +// +// It will be used as the default value of the page_size field in all requests made with this client. +func WithDefaultPageSize(pageSize uint32) ClientOption { + return func(s *settings) { + s.defaultPageSize = &pageSize + } +} + +// settings hold the values of all client options +type settings struct { + apiURL string + token auth.Auth + userAgent string + httpClient httpClient + insecure bool + defaultOrganizationID *string + defaultProjectID *string + defaultRegion *Region + defaultZone *Zone + defaultPageSize *uint32 +} + +func newSettings() *settings { + return &settings{} +} + +func (s *settings) apply(opts []ClientOption) { + for _, opt := range opts { + opt(s) + } +} + +func (s *settings) validate() error { + // Auth. + if s.token == nil { + // It should not happen, WithoutAuth option is used by default. + panic(errors.New("no credential option provided")) + } + if token, isToken := s.token.(*auth.Token); isToken { + if token.AccessKey == "" { + return NewInvalidClientOptionError("access key cannot be empty") + } + if !validation.IsAccessKey(token.AccessKey) { + return NewInvalidClientOptionError("invalid access key format '%s', expected SCWXXXXXXXXXXXXXXXXX format", token.AccessKey) + } + if token.SecretKey == "" { + return NewInvalidClientOptionError("secret key cannot be empty") + } + if !validation.IsSecretKey(token.SecretKey) { + return NewInvalidClientOptionError("invalid secret key format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", token.SecretKey) + } + } + + // Default Organization ID. + if s.defaultOrganizationID != nil { + if *s.defaultOrganizationID == "" { + return NewInvalidClientOptionError("default organization ID cannot be empty") + } + if !validation.IsOrganizationID(*s.defaultOrganizationID) { + return NewInvalidClientOptionError("invalid organization ID format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", *s.defaultOrganizationID) + } + } + + // Default Project ID. + if s.defaultProjectID != nil { + if *s.defaultProjectID == "" { + return NewInvalidClientOptionError("default project ID cannot be empty") + } + if !validation.IsProjectID(*s.defaultProjectID) { + return NewInvalidClientOptionError("invalid project ID format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", *s.defaultProjectID) + } + } + + // Default Region. + if s.defaultRegion != nil { + if *s.defaultRegion == "" { + return NewInvalidClientOptionError("default region cannot be empty") + } + if !validation.IsRegion(string(*s.defaultRegion)) { + regions := []string(nil) + for _, r := range AllRegions { + regions = append(regions, string(r)) + } + return NewInvalidClientOptionError("invalid default region format '%s', available regions are: %s", *s.defaultRegion, strings.Join(regions, ", ")) + } + } + + // Default Zone. + if s.defaultZone != nil { + if *s.defaultZone == "" { + return NewInvalidClientOptionError("default zone cannot be empty") + } + if !validation.IsZone(string(*s.defaultZone)) { + zones := []string(nil) + for _, z := range AllZones { + zones = append(zones, string(z)) + } + return NewInvalidClientOptionError("invalid default zone format '%s', available zones are: %s", *s.defaultZone, strings.Join(zones, ", ")) + } + } + + // API URL. + if !validation.IsURL(s.apiURL) { + return NewInvalidClientOptionError("invalid url %s", s.apiURL) + } + if s.apiURL[len(s.apiURL)-1:] == "/" { + return NewInvalidClientOptionError("invalid url %s it should not have a trailing slash", s.apiURL) + } + + // TODO: check for max s.defaultPageSize + + return nil +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/config.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/config.go new file mode 100644 index 0000000000..34b5769603 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/config.go @@ -0,0 +1,343 @@ +package scw + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "text/template" + + "github.com/scaleway/scaleway-sdk-go/internal/auth" + "github.com/scaleway/scaleway-sdk-go/internal/errors" + "github.com/scaleway/scaleway-sdk-go/logger" + "gopkg.in/yaml.v2" +) + +const ( + documentationLink = "https://github.com/scaleway/scaleway-sdk-go/blob/master/scw/README.md" + defaultConfigPermission = 0600 + + // Reserved name for the default profile. + DefaultProfileName = "default" +) + +const configFileTemplate = `# Scaleway configuration file +# https://github.com/scaleway/scaleway-sdk-go/tree/master/scw#scaleway-config + +# This configuration file can be used with: +# - Scaleway SDK Go (https://github.com/scaleway/scaleway-sdk-go) +# - Scaleway CLI (>2.0.0) (https://github.com/scaleway/scaleway-cli) +# - Scaleway Terraform Provider (https://www.terraform.io/docs/providers/scaleway/index.html) + +# You need an access key and a secret key to connect to Scaleway API. +# Generate your token at the following address: https://console.scaleway.com/project/credentials + +# An access key is a secret key identifier. +{{ if .AccessKey }}access_key: {{.AccessKey}}{{ else }}# access_key: SCW11111111111111111{{ end }} + +# The secret key is the value that can be used to authenticate against the API (the value used in X-Auth-Token HTTP-header). +# The secret key MUST remain secret and not given to anyone or published online. +{{ if .SecretKey }}secret_key: {{ .SecretKey }}{{ else }}# secret_key: 11111111-1111-1111-1111-111111111111{{ end }} + +# Your organization ID is the identifier of your account inside Scaleway infrastructure. +{{ if .DefaultOrganizationID }}default_organization_id: {{ .DefaultOrganizationID }}{{ else }}# default_organization_id: 11111111-1111-1111-1111-111111111111{{ end }} + +# Your project ID is the identifier of the project your resources are attached to (beta). +{{ if .DefaultProjectID }}default_project_id: {{ .DefaultProjectID }}{{ else }}# default_project_id: 11111111-1111-1111-1111-111111111111{{ end }} + +# A region is represented as a geographical area such as France (Paris) or the Netherlands (Amsterdam). +# It can contain multiple availability zones. +# Example of region: fr-par, nl-ams +{{ if .DefaultRegion }}default_region: {{ .DefaultRegion }}{{ else }}# default_region: fr-par{{ end }} + +# A region can be split into many availability zones (AZ). +# Latency between multiple AZ of the same region are low as they have a common network layer. +# Example of zones: fr-par-1, nl-ams-1 +{{ if .DefaultZone }}default_zone: {{.DefaultZone}}{{ else }}# default_zone: fr-par-1{{ end }} + +# APIURL overrides the API URL of the Scaleway API to the given URL. +# Change that if you want to direct requests to a different endpoint. +{{ if .APIURL }}apiurl: {{ .APIURL }}{{ else }}# api_url: https://api.scaleway.com{{ end }} + +# Insecure enables insecure transport on the client. +# Default to false +{{ if .Insecure }}insecure: {{ .Insecure }}{{ else }}# insecure: false{{ end }} + +# A configuration is a named set of Scaleway properties. +# Starting off with a Scaleway SDK or Scaleway CLI, you’ll work with a single configuration named default. +# You can set properties of the default profile by running either scw init or scw config set. +# This single default configuration is suitable for most use cases. +{{ if .ActiveProfile }}active_profile: {{ .ActiveProfile }}{{ else }}# active_profile: myProfile{{ end }} + +# To improve the Scaleway CLI we rely on diagnostic and usage data. +# Sending such data is optional and can be disable at any time by setting send_telemetry variable to false. +{{ if .SendTelemetry }}send_telemetry: {{ .SendTelemetry }}{{ else }}# send_telemetry: false{{ end }} + +# To work with multiple projects or authorization accounts, you can set up multiple configurations with scw config configurations create and switch among them accordingly. +# You can use a profile by either: +# - Define the profile you want to use as the SCW_PROFILE environment variable +# - Use the GetActiveProfile() function in the SDK +# - Use the --profile flag with the CLI + +# You can define a profile using the following syntax: +{{ if gt (len .Profiles) 0 }} +profiles: +{{- range $k,$v := .Profiles }} + {{ $k }}: + {{ if $v.AccessKey }}access_key: {{ $v.AccessKey }}{{ else }}# access_key: SCW11111111111111111{{ end }} + {{ if $v.SecretKey }}secret_key: {{ $v.SecretKey }}{{ else }}# secret_key: 11111111-1111-1111-1111-111111111111{{ end }} + {{ if $v.DefaultOrganizationID }}default_organization_id: {{ $v.DefaultOrganizationID }}{{ else }}# default_organization_id: 11111111-1111-1111-1111-111111111111{{ end }} + {{ if $v.DefaultProjectID }}default_project_id: {{ $v.DefaultProjectID }}{{ else }}# default_project_id: 11111111-1111-1111-1111-111111111111{{ end }} + {{ if $v.DefaultZone }}default_zone: {{ $v.DefaultZone }}{{ else }}# default_zone: fr-par-1{{ end }} + {{ if $v.DefaultRegion }}default_region: {{ $v.DefaultRegion }}{{ else }}# default_region: fr-par{{ end }} + {{ if $v.APIURL }}api_url: {{ $v.APIURL }}{{ else }}# api_url: https://api.scaleway.com{{ end }} + {{ if $v.Insecure }}insecure: {{ $v.Insecure }}{{ else }}# insecure: false{{ end }} +{{ end }} +{{- else }} +# profiles: +# myProfile: +# access_key: 11111111-1111-1111-1111-111111111111 +# secret_key: 11111111-1111-1111-1111-111111111111 +# default_organization_id: 11111111-1111-1111-1111-111111111111 +# default_project_id: 11111111-1111-1111-1111-111111111111 +# default_zone: fr-par-1 +# default_region: fr-par +# api_url: https://api.scaleway.com +# insecure: false +{{ end -}} +` + +type Config struct { + Profile `yaml:",inline"` + ActiveProfile *string `yaml:"active_profile,omitempty" json:"active_profile,omitempty"` + Profiles map[string]*Profile `yaml:"profiles,omitempty" json:"profiles,omitempty"` +} + +type Profile struct { + AccessKey *string `yaml:"access_key,omitempty" json:"access_key,omitempty"` + SecretKey *string `yaml:"secret_key,omitempty" json:"secret_key,omitempty"` + APIURL *string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + Insecure *bool `yaml:"insecure,omitempty" json:"insecure,omitempty"` + DefaultOrganizationID *string `yaml:"default_organization_id,omitempty" json:"default_organization_id,omitempty"` + DefaultProjectID *string `yaml:"default_project_id,omitempty" json:"default_project_id,omitempty"` + DefaultRegion *string `yaml:"default_region,omitempty" json:"default_region,omitempty"` + DefaultZone *string `yaml:"default_zone,omitempty" json:"default_zone,omitempty"` + SendTelemetry *bool `yaml:"send_telemetry,omitempty" json:"send_telemetry,omitempty"` +} + +func (p *Profile) String() string { + p2 := *p + p2.SecretKey = hideSecretKey(p2.SecretKey) + configRaw, _ := yaml.Marshal(p2) + return string(configRaw) +} + +// clone deep copy config object +func (c *Config) clone() *Config { + c2 := &Config{} + configRaw, _ := yaml.Marshal(c) + _ = yaml.Unmarshal(configRaw, c2) + return c2 +} + +func (c *Config) String() string { + c2 := c.clone() + c2.SecretKey = hideSecretKey(c2.SecretKey) + for _, p := range c2.Profiles { + p.SecretKey = hideSecretKey(p.SecretKey) + } + + configRaw, _ := yaml.Marshal(c2) + return string(configRaw) +} + +func (c *Config) IsEmpty() bool { + return c.String() == "{}\n" +} + +func hideSecretKey(key *string) *string { + if key == nil { + return nil + } + + newKey := auth.HideSecretKey(*key) + return &newKey +} + +func unmarshalConfV2(content []byte) (*Config, error) { + var config Config + + err := yaml.Unmarshal(content, &config) + if err != nil { + return nil, err + } + return &config, nil +} + +// MustLoadConfig is like LoadConfig but panic instead of returning an error. +func MustLoadConfig() *Config { + c, err := LoadConfigFromPath(GetConfigPath()) + if err != nil { + panic(err) + } + return c +} + +// LoadConfig read the config from the default path. +func LoadConfig() (*Config, error) { + return LoadConfigFromPath(GetConfigPath()) +} + +// LoadConfigFromPath read the config from the given path. +func LoadConfigFromPath(path string) (*Config, error) { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return nil, configFileNotFound(path) + } + if err != nil { + return nil, err + } + + file, err := ioutil.ReadFile(path) + if err != nil { + return nil, errors.Wrap(err, "cannot read config file") + } + + _, err = unmarshalConfV1(file) + if err == nil { + // reject V1 config + return nil, errors.New("found legacy config in %s: legacy config is not allowed, please switch to the new config file format: %s", path, documentationLink) + } + + confV2, err := unmarshalConfV2(file) + if err != nil { + return nil, errors.Wrap(err, "content of config file %s is invalid", path) + } + + return confV2, nil +} + +// GetProfile returns the profile corresponding to the given profile name. +func (c *Config) GetProfile(profileName string) (*Profile, error) { + if profileName == "" { + return nil, errors.New("profileName cannot be empty") + } + + if profileName == DefaultProfileName { + return &c.Profile, nil + } + + p, exist := c.Profiles[profileName] + if !exist { + return nil, errors.New("given profile %s does not exist", profileName) + } + + // Merge selected profile on top of default profile + return MergeProfiles(&c.Profile, p), nil +} + +// GetActiveProfile returns the active profile of the config based on the following order: +// env SCW_PROFILE > config active_profile > config root profile +func (c *Config) GetActiveProfile() (*Profile, error) { + switch { + case os.Getenv(ScwActiveProfileEnv) != "": + logger.Debugf("using active profile from env: %s=%s", ScwActiveProfileEnv, os.Getenv(ScwActiveProfileEnv)) + return c.GetProfile(os.Getenv(ScwActiveProfileEnv)) + case c.ActiveProfile != nil: + logger.Debugf("using active profile from config: active_profile=%s", ScwActiveProfileEnv, *c.ActiveProfile) + return c.GetProfile(*c.ActiveProfile) + default: + return &c.Profile, nil + } +} + +// SaveTo will save the config to the default config path. This +// action will overwrite the previous file when it exists. +func (c *Config) Save() error { + return c.SaveTo(GetConfigPath()) +} + +// HumanConfig will generate a config file with documented arguments. +func (c *Config) HumanConfig() (string, error) { + tmpl, err := template.New("configuration").Parse(configFileTemplate) + if err != nil { + return "", err + } + + var buf bytes.Buffer + err = tmpl.Execute(&buf, c) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// SaveTo will save the config to the given path. This action will +// overwrite the previous file when it exists. +func (c *Config) SaveTo(path string) error { + path = filepath.Clean(path) + + // STEP 1: Render the configuration file as a file + file, err := c.HumanConfig() + if err != nil { + return err + } + + // STEP 2: create config path dir in cases it didn't exist before + err = os.MkdirAll(filepath.Dir(path), 0700) + if err != nil { + return err + } + + // STEP 3: write new config file + err = ioutil.WriteFile(path, []byte(file), defaultConfigPermission) + if err != nil { + return err + } + + return nil +} + +// MergeProfiles merges profiles in a new one. The last profile has priority. +func MergeProfiles(original *Profile, others ...*Profile) *Profile { + np := &Profile{ + AccessKey: original.AccessKey, + SecretKey: original.SecretKey, + APIURL: original.APIURL, + Insecure: original.Insecure, + DefaultOrganizationID: original.DefaultOrganizationID, + DefaultProjectID: original.DefaultProjectID, + DefaultRegion: original.DefaultRegion, + DefaultZone: original.DefaultZone, + } + + for _, other := range others { + if other.AccessKey != nil { + np.AccessKey = other.AccessKey + } + if other.SecretKey != nil { + np.SecretKey = other.SecretKey + } + if other.APIURL != nil { + np.APIURL = other.APIURL + } + if other.Insecure != nil { + np.Insecure = other.Insecure + } + if other.DefaultOrganizationID != nil { + np.DefaultOrganizationID = other.DefaultOrganizationID + } + if other.DefaultProjectID != nil { + np.DefaultProjectID = other.DefaultProjectID + } + if other.DefaultRegion != nil { + np.DefaultRegion = other.DefaultRegion + } + if other.DefaultZone != nil { + np.DefaultZone = other.DefaultZone + } + } + + return np +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/config_legacy.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/config_legacy.go new file mode 100644 index 0000000000..9a81df685e --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/config_legacy.go @@ -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 +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/convert.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/convert.go new file mode 100644 index 0000000000..e48c21fa7e --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/convert.go @@ -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 +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/custom_types.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/custom_types.go new file mode 100644 index 0000000000..aa9234e971 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/custom_types.go @@ -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 == "" { + 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 +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/env.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/env.go new file mode 100644 index 0000000000..49842f6c45 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/env.go @@ -0,0 +1,144 @@ +package scw + +import ( + "os" + "strconv" + + "github.com/scaleway/scaleway-sdk-go/logger" +) + +// Environment variables +const ( + // Up-to-date + ScwCacheDirEnv = "SCW_CACHE_DIR" + ScwConfigPathEnv = "SCW_CONFIG_PATH" + ScwAccessKeyEnv = "SCW_ACCESS_KEY" + ScwSecretKeyEnv = "SCW_SECRET_KEY" // #nosec G101 + ScwActiveProfileEnv = "SCW_PROFILE" + ScwAPIURLEnv = "SCW_API_URL" + ScwInsecureEnv = "SCW_INSECURE" + ScwDefaultOrganizationIDEnv = "SCW_DEFAULT_ORGANIZATION_ID" + ScwDefaultProjectIDEnv = "SCW_DEFAULT_PROJECT_ID" + ScwDefaultRegionEnv = "SCW_DEFAULT_REGION" + ScwDefaultZoneEnv = "SCW_DEFAULT_ZONE" + DebugEnv = logger.DebugEnv + + // All deprecated (cli&terraform) + terraformAccessKeyEnv = "SCALEWAY_ACCESS_KEY" // used both as access key and secret key + terraformSecretKeyEnv = "SCALEWAY_TOKEN" + terraformOrganizationEnv = "SCALEWAY_ORGANIZATION" + terraformRegionEnv = "SCALEWAY_REGION" + cliTLSVerifyEnv = "SCW_TLSVERIFY" + cliOrganizationEnv = "SCW_ORGANIZATION" + cliRegionEnv = "SCW_REGION" + cliSecretKeyEnv = "SCW_TOKEN" + + // TBD + //cliVerboseEnv = "SCW_VERBOSE_API" + //cliDebugEnv = "DEBUG" + //cliNoCheckVersionEnv = "SCW_NOCHECKVERSION" + //cliTestWithRealAPIEnv = "TEST_WITH_REAL_API" + //cliSecureExecEnv = "SCW_SECURE_EXEC" + //cliGatewayEnv = "SCW_GATEWAY" + //cliSensitiveEnv = "SCW_SENSITIVE" + //cliAccountAPIEnv = "SCW_ACCOUNT_API" + //cliMetadataAPIEnv = "SCW_METADATA_API" + //cliMarketPlaceAPIEnv = "SCW_MARKETPLACE_API" + //cliComputePar1APIEnv = "SCW_COMPUTE_PAR1_API" + //cliComputeAms1APIEnv = "SCW_COMPUTE_AMS1_API" + //cliCommercialTypeEnv = "SCW_COMMERCIAL_TYPE" + //cliTargetArchEnv = "SCW_TARGET_ARCH" +) + +const ( + v1RegionFrPar = "par1" + v1RegionNlAms = "ams1" +) + +func LoadEnvProfile() *Profile { + p := &Profile{} + + accessKey, _, envExist := getEnv(ScwAccessKeyEnv, terraformAccessKeyEnv) + if envExist { + p.AccessKey = &accessKey + } + + secretKey, _, envExist := getEnv(ScwSecretKeyEnv, cliSecretKeyEnv, terraformSecretKeyEnv, terraformAccessKeyEnv) + if envExist { + p.SecretKey = &secretKey + } + + apiURL, _, envExist := getEnv(ScwAPIURLEnv) + if envExist { + p.APIURL = &apiURL + } + + insecureValue, envKey, envExist := getEnv(ScwInsecureEnv, cliTLSVerifyEnv) + if envExist { + insecure, err := strconv.ParseBool(insecureValue) + if err != nil { + logger.Warningf("env variable %s cannot be parsed: %s is invalid boolean", envKey, insecureValue) + } + + if envKey == cliTLSVerifyEnv { + insecure = !insecure // TLSVerify is the inverse of Insecure + } + + p.Insecure = &insecure + } + + organizationID, _, envExist := getEnv(ScwDefaultOrganizationIDEnv, cliOrganizationEnv, terraformOrganizationEnv) + if envExist { + p.DefaultOrganizationID = &organizationID + } + + projectID, _, envExist := getEnv(ScwDefaultProjectIDEnv) + if envExist { + p.DefaultProjectID = &projectID + } + + region, _, envExist := getEnv(ScwDefaultRegionEnv, cliRegionEnv, terraformRegionEnv) + if envExist { + region = v1RegionToV2(region) + p.DefaultRegion = ®ion + } + + zone, _, envExist := getEnv(ScwDefaultZoneEnv) + if envExist { + p.DefaultZone = &zone + } + + return p +} + +func getEnv(upToDateKey string, deprecatedKeys ...string) (string, string, bool) { + value, exist := os.LookupEnv(upToDateKey) + if exist { + logger.Debugf("reading value from %s", upToDateKey) + return value, upToDateKey, true + } + + for _, key := range deprecatedKeys { + value, exist := os.LookupEnv(key) + if exist { + logger.Debugf("reading value from %s", key) + logger.Warningf("%s is deprecated, please use %s instead", key, upToDateKey) + return value, key, true + } + } + + return "", "", false +} + +func v1RegionToV2(region string) string { + switch region { + case v1RegionFrPar: + logger.Warningf("par1 is a deprecated name for region, use fr-par instead") + return "fr-par" + case v1RegionNlAms: + logger.Warningf("ams1 is a deprecated name for region, use nl-ams instead") + return "nl-ams" + default: + return region + } +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/errors.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/errors.go new file mode 100644 index 0000000000..0159b4bf48 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/errors.go @@ -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() {} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/locality.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/locality.go new file mode 100644 index 0000000000..ee3737892a --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/locality.go @@ -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) +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/path.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/path.go new file mode 100644 index 0000000000..0c90adace0 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/path.go @@ -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 +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/request.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/request.go new file mode 100644 index 0000000000..19f0dd4d58 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/request.go @@ -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 +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/request_option.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/request_option.go new file mode 100644 index 0000000000..a5ff376632 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/request_option.go @@ -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) + } +} diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/scw/version.go b/vendor/github.com/scaleway/scaleway-sdk-go/scw/version.go new file mode 100644 index 0000000000..6d59119421 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/scw/version.go @@ -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) diff --git a/vendor/github.com/scaleway/scaleway-sdk-go/validation/is.go b/vendor/github.com/scaleway/scaleway-sdk-go/validation/is.go new file mode 100644 index 0000000000..a7e52d0bd0 --- /dev/null +++ b/vendor/github.com/scaleway/scaleway-sdk-go/validation/is.go @@ -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) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2c498db2c4..d5cada7294 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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