├── .gitignore ├── docs ├── static_files │ └── scaleway-logo.png └── CONTINUOUS_CODE_DEPLOYMENT.md ├── vendor ├── gopkg.in │ └── yaml.v2 │ │ ├── go.mod │ │ ├── .travis.yml │ │ ├── NOTICE │ │ ├── writerc.go │ │ ├── LICENSE.libyaml │ │ ├── sorter.go │ │ ├── README.md │ │ └── yamlprivateh.go ├── modules.txt └── github.com │ └── dnaeon │ └── go-vcr │ ├── LICENSE │ ├── recorder │ ├── go18_nobody.go │ └── go17_nobody.go │ └── cassette │ └── cassette.go ├── go.mod ├── sdk_compilation_test.go ├── scw ├── version.go ├── request_option.go ├── errors_test.go ├── locality_test.go ├── errors.go ├── path.go ├── request.go ├── config_legacy.go ├── config_legacy_test.go ├── custom_types_test.go ├── env_test.go ├── request_test.go ├── locality.go ├── README.md ├── env.go ├── convert.go ├── custom_types.go ├── config.go ├── load_config_test.go ├── client_option.go ├── client_test.go ├── convert_test.go └── client_option_test.go ├── scripts └── check_for_tokens.sh ├── internal ├── uuid │ └── uuid.go ├── auth │ ├── no_auth.go │ ├── token_test.go │ ├── auth.go │ └── token.go ├── parameter │ └── query.go ├── errors │ └── error.go ├── testhelpers │ ├── test_helpers.go │ └── httprecorder │ │ └── recorder.go ├── async │ ├── wait.go │ └── wait_test.go ├── marshaler │ └── duration.go └── e2e │ └── endtoend_test.go ├── doc.go ├── MAINTAINERS.md ├── go.sum ├── .circleci └── config.yml ├── api ├── instance │ └── v1 │ │ ├── testdata │ │ ├── server-incorrect-body.yaml │ │ ├── volume-utils-test.yaml │ │ ├── security-group-test.yaml │ │ └── server-user-data.yaml │ │ ├── volume_utils_test.go │ │ ├── volume_utils.go │ │ ├── instance_utils_test.go │ │ ├── server_utils_test.go │ │ └── instance_sdk_server_test.go ├── marketplace │ └── v1 │ │ ├── marketplace_utils_test.go │ │ └── marketplace_utils.go └── baremetal │ └── v1alpha1 │ └── server_utils.go ├── namegenerator └── name_generator_test.go ├── logger ├── logger.go ├── logger_test.go └── default_logger.go ├── README.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── example_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | -------------------------------------------------------------------------------- /docs/static_files/scaleway-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ice3man543/scaleway-sdk-go/master/docs/static_files/scaleway-logo.png -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/go.mod: -------------------------------------------------------------------------------- 1 | module "gopkg.in/yaml.v2" 2 | 3 | require ( 4 | "gopkg.in/check.v1" v0.0.0-20161208181325-20d25e280405 5 | ) 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scaleway/scaleway-sdk-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/dnaeon/go-vcr v1.0.1 7 | gopkg.in/yaml.v2 v2.2.2 8 | ) 9 | -------------------------------------------------------------------------------- /vendor/modules.txt: -------------------------------------------------------------------------------- 1 | # github.com/dnaeon/go-vcr v1.0.1 2 | github.com/dnaeon/go-vcr/cassette 3 | github.com/dnaeon/go-vcr/recorder 4 | # gopkg.in/yaml.v2 v2.2.2 5 | gopkg.in/yaml.v2 6 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - 1.5 6 | - 1.6 7 | - 1.7 8 | - 1.8 9 | - 1.9 10 | - tip 11 | 12 | go_import_path: gopkg.in/yaml.v2 13 | -------------------------------------------------------------------------------- /sdk_compilation_test.go: -------------------------------------------------------------------------------- 1 | package scalewaysdkgo 2 | 3 | // This test file makes sure that all the auto-generated code compiles. 4 | 5 | import ( 6 | _ "github.com/scaleway/scaleway-sdk-go/api/instance/v1" 7 | _ "github.com/scaleway/scaleway-sdk-go/api/lb/v1" 8 | ) 9 | -------------------------------------------------------------------------------- /scw/version.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | // TODO: versionning process 9 | const version = "0.0.0" 10 | 11 | var userAgent = fmt.Sprintf("scaleway-sdk-go/%s (%s; %s; %s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) 12 | -------------------------------------------------------------------------------- /scripts/check_for_tokens.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | files=$(find . -type f -print | grep testdata); 4 | 5 | for file in $files 6 | do 7 | echo "checking $file"; 8 | if grep -i --quiet "x-auth-token" $file 9 | then 10 | echo "found x-auth-token in file $file"; 11 | exit 1; 12 | fi 13 | done 14 | exit 0; 15 | -------------------------------------------------------------------------------- /internal/uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import "regexp" 4 | 5 | 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}`) 6 | 7 | // IsUUID returns true if the given string has a UUID format. 8 | func IsUUID(s string) bool { 9 | return isUUIDRegexp.MatchString(s) 10 | } 11 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package scalewaysdkgo is the Scaleway API SDK for Go. 2 | // 3 | // In order to use the available APIs, create a `Client`. Once created, it can be used to instantiate an API. 4 | // To use the `instance` API, for example, instantiate it (with the client object) `instance.NewApi(client)`. 5 | // On this instance API, all the available API functions can be called. 6 | // 7 | package scalewaysdkgo 8 | -------------------------------------------------------------------------------- /internal/auth/no_auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "net/http" 4 | 5 | type noAuth struct { 6 | } 7 | 8 | // NewNoAuth return an auth with no authentication method 9 | func NewNoAuth() *noAuth { 10 | return &noAuth{} 11 | } 12 | 13 | func (t *noAuth) Headers() http.Header { 14 | return http.Header{} 15 | } 16 | 17 | func (t *noAuth) AnonymizedHeaders() http.Header { 18 | return http.Header{} 19 | } 20 | -------------------------------------------------------------------------------- /internal/auth/token_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 8 | ) 9 | 10 | func TestToken_Headers(t *testing.T) { 11 | const ( 12 | accessKey = "ACCESS_KEY" 13 | secretKey = "SECRET_KEY" 14 | ) 15 | auth := NewToken(accessKey, secretKey) 16 | testhelpers.Equals(t, http.Header{ 17 | "X-Auth-Token": []string{secretKey}, 18 | }, auth.Headers()) 19 | } 20 | -------------------------------------------------------------------------------- /internal/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "net/http" 4 | 5 | // Auth implement methods required for authentication. 6 | // Valid authentication are currently a token or no auth. 7 | type Auth interface { 8 | // Headers returns headers that must be add to the http request 9 | Headers() http.Header 10 | 11 | // AnonymizedHeaders returns an anonymised version of Headers() 12 | // This method could be use for logging purpose. 13 | AnonymizedHeaders() http.Header 14 | } 15 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | This page lists all active maintainers of `scaleway-sdk-go`. This can be used for 2 | routing PRs, questions, etc. to the right place. 3 | 4 | - See [CONTRIBUTING.md](CONTRIBUTING.md) for general contribution guidelines. 5 | 6 | ### Maintainers 7 | 8 | | Name | Tag | 9 | | :------------- | :-------------- | 10 | | Jérôme Quéré | @jerome-quere | 11 | | Mathias Beke | @DenBeke | 12 | | Quentin Brosse | @QuentinBrosse | 13 | | Olivier Cano | @kindermoumoute | 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= 2 | github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= 3 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 4 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 5 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 6 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 7 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2011-2016 Canonical Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/writerc.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | // Set the writer error and return false. 4 | func yaml_emitter_set_writer_error(emitter *yaml_emitter_t, problem string) bool { 5 | emitter.error = yaml_WRITER_ERROR 6 | emitter.problem = problem 7 | return false 8 | } 9 | 10 | // Flush the output buffer. 11 | func yaml_emitter_flush(emitter *yaml_emitter_t) bool { 12 | if emitter.write_handler == nil { 13 | panic("write handler not set") 14 | } 15 | 16 | // Check if the buffer is empty. 17 | if emitter.buffer_pos == 0 { 18 | return true 19 | } 20 | 21 | if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil { 22 | return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error()) 23 | } 24 | emitter.buffer_pos = 0 25 | return true 26 | } 27 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | test-go-1-12: 7 | docker: 8 | - image: circleci/golang:1.12 9 | steps: 10 | - checkout 11 | - run: go mod download 12 | - run: 13 | name: Run unit tests 14 | command: go test -v ./... 15 | test-go-1-10: 16 | docker: 17 | - image: circleci/golang:1.10 18 | working_directory: /go/src/github.com/scaleway/scaleway-sdk-go 19 | steps: 20 | - checkout 21 | - run: go get -t 22 | - run: 23 | name: Run unit tests 24 | command: go test -v ./... 25 | workflows: 26 | version: 2 27 | test: 28 | jobs: 29 | - test-go-1-12 30 | - test-go-1-10 31 | -------------------------------------------------------------------------------- /internal/parameter/query.go: -------------------------------------------------------------------------------- 1 | package parameter 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | ) 8 | 9 | // AddToQuery add a key/value pair to an URL query 10 | func AddToQuery(query url.Values, key string, value interface{}) { 11 | elemValue := reflect.ValueOf(value) 12 | 13 | if elemValue.Kind() == reflect.Invalid || elemValue.Kind() == reflect.Ptr && elemValue.IsNil() { 14 | return 15 | } 16 | 17 | for elemValue.Kind() == reflect.Ptr { 18 | elemValue = reflect.ValueOf(value).Elem() 19 | } 20 | 21 | elemType := elemValue.Type() 22 | switch { 23 | case elemType.Kind() == reflect.Slice: 24 | for i := 0; i < elemValue.Len(); i++ { 25 | query.Add(key, fmt.Sprint(elemValue.Index(i).Interface())) 26 | } 27 | default: 28 | query.Add(key, fmt.Sprint(elemValue.Interface())) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /docs/CONTINUOUS_CODE_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Continuous code deployment 2 | 3 | Part of this repo is automatically generated from our [protocol buffer](https://en.wikipedia.org/wiki/Protocol_Buffers) monorepo. 4 | This enables us to keep Scaleway toolings up to date with the latest version of our APIs ([developer website](http://developers.scaleway.com), soon-to-be CLI, ...). 5 | 6 | ## Generated files 7 | 8 | Generated files and folders are located in [scaleway-sdk-go/api](../api). 9 | They always start with the following line: 10 | 11 | ```c 12 | // This file was automatically generated. DO NOT EDIT. 13 | ``` 14 | 15 | ## Continuous deployment process 16 | 17 | TODO: explains the continuous deployment process. 18 | 19 | ## Synchronization frequency 20 | 21 | The continuous code deployment process can occur at anytime of the day, sometime many times a day. 22 | Expect it to happen regularly. 23 | 24 | ## Any question? 25 | 26 | If you have any question or request about the continuous code deployment process feel free to [reach us](../README.md#reach-us). 27 | -------------------------------------------------------------------------------- /internal/errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "fmt" 4 | 5 | // Error is a base error that implement scw.SdkError 6 | type Error struct { 7 | Str string 8 | Err error 9 | } 10 | 11 | // Error implement standard xerror.Wrapper interface 12 | func (e *Error) Unwrap() error { 13 | return e.Err 14 | } 15 | 16 | // Error implement standard error interface 17 | func (e *Error) Error() string { 18 | str := "scaleway-sdk-go: " + e.Str 19 | if e.Err != nil { 20 | str += ": " + e.Err.Error() 21 | } 22 | return str 23 | } 24 | 25 | // IsScwSdkError implement SdkError interface 26 | func (e *Error) IsScwSdkError() {} 27 | 28 | // New creates a new error with that same interface as fmt.Errorf 29 | func New(format string, args ...interface{}) *Error { 30 | return &Error{ 31 | Str: fmt.Sprintf(format, args...), 32 | } 33 | } 34 | 35 | // Wrap an error with additional information 36 | func Wrap(err error, format string, args ...interface{}) *Error { 37 | return &Error{ 38 | Err: err, 39 | Str: fmt.Sprintf(format, args...), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/testhelpers/test_helpers.go: -------------------------------------------------------------------------------- 1 | package testhelpers 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | // Assert fails the test if the condition is false. 12 | func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) { 13 | if !condition { 14 | _, file, line, _ := runtime.Caller(1) 15 | fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) 16 | tb.FailNow() 17 | } 18 | } 19 | 20 | // AssertNoError fails the test if an err is not nil. 21 | func AssertNoError(tb testing.TB, err error) { 22 | if err != nil { 23 | _, file, line, _ := runtime.Caller(1) 24 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 25 | tb.FailNow() 26 | } 27 | } 28 | 29 | // Equals fails the test if exp is not equal to act. 30 | func Equals(tb testing.TB, exp, act interface{}) { 31 | if !reflect.DeepEqual(exp, act) { 32 | _, file, line, _ := runtime.Caller(1) 33 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 34 | tb.FailNow() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /api/instance/v1/testdata/server-incorrect-body.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: '{}' 6 | form: {} 7 | headers: 8 | Content-Type: 9 | - application/json 10 | User-Agent: 11 | - scaleway-sdk-go/0.0.0 12 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/servers 13 | method: POST 14 | response: 15 | body: '{"fields": {"organization": ["required key not provided"], "image": ["required 16 | key not provided"], "name": ["required key not provided"], "volumes": ["required 17 | key not provided"]}, "message": "Validation Error", "type": "invalid_request_error"}' 18 | headers: 19 | Content-Length: 20 | - "244" 21 | Content-Security-Policy: 22 | - default-src 'none'; frame-ancestors 'none' 23 | Content-Type: 24 | - application/json 25 | Date: 26 | - Thu, 23 May 2019 15:49:33 GMT 27 | Server: 28 | - scaleway_api 29 | Strict-Transport-Security: 30 | - max-age=63072000 31 | X-Content-Type-Options: 32 | - nosniff 33 | X-Frame-Options: 34 | - DENY 35 | status: 400 Bad Request 36 | code: 400 37 | duration: "" 38 | -------------------------------------------------------------------------------- /scw/request_option.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/internal/auth" 7 | ) 8 | 9 | // RequestOption is a function that applies options to a ScalewayRequest. 10 | type RequestOption func(*ScalewayRequest) 11 | 12 | // WithContext request option sets the context of a ScalewayRequest 13 | func WithContext(ctx context.Context) RequestOption { 14 | return func(s *ScalewayRequest) { 15 | s.ctx = ctx 16 | } 17 | } 18 | 19 | // WithAllPages aggregate all pages in the response of a List request. 20 | // Will error when pagination is not supported on the request. 21 | func WithAllPages() RequestOption { 22 | return func(s *ScalewayRequest) { 23 | s.allPages = true 24 | } 25 | } 26 | 27 | // WithAuthRequest overwrites the client access key and secret key used in the request. 28 | func WithAuthRequest(accessKey, secretKey string) RequestOption { 29 | return func(s *ScalewayRequest) { 30 | s.auth = auth.NewToken(accessKey, secretKey) 31 | } 32 | } 33 | 34 | func (s *ScalewayRequest) apply(opts []RequestOption) { 35 | for _, opt := range opts { 36 | opt(s) 37 | } 38 | } 39 | 40 | func (s *ScalewayRequest) validate() SdkError { 41 | // nothing so far 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /namegenerator/name_generator_test.go: -------------------------------------------------------------------------------- 1 | // Source: github.com/docker/docker/pkg/namesgenerator 2 | 3 | package namegenerator 4 | 5 | import ( 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestNameFormat(t *testing.T) { 11 | name := GetRandomName() 12 | if strings.Count(name, "-") != 1 { 13 | t.Fatalf("Generated name does not contain exactly 1 hyphen") 14 | } 15 | if strings.ContainsAny(name, "0123456789") { 16 | t.Fatalf("Generated name contains numbers!") 17 | } 18 | } 19 | 20 | func TestNameFormatWithPrefix(t *testing.T) { 21 | name := GetRandomName("scw") 22 | if strings.Count(name, "-") != 2 { 23 | t.Fatalf("Generated name does not contain exactly 2 hyphens") 24 | } 25 | if !strings.HasPrefix(name, "scw-") { 26 | t.Fatalf("Generated name must begin with \"tf-scw-\"") 27 | } 28 | if strings.ContainsAny(name, "0123456789") { 29 | t.Fatalf("Generated name contains numbers!") 30 | } 31 | } 32 | 33 | func TestNameFormatWithPrefixes(t *testing.T) { 34 | name := GetRandomName("tf", "scw") 35 | if strings.Count(name, "-") != 3 { 36 | t.Fatalf("Generated name does not contain exactly 3 hyphens") 37 | } 38 | if !strings.HasPrefix(name, "tf-scw-") { 39 | t.Fatalf("Generated name must begin with \"tf-scw-\"") 40 | } 41 | if strings.ContainsAny(name, "0123456789") { 42 | t.Fatalf("Generated name contains numbers!") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/auth/token.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "net/http" 4 | 5 | // Token is the pair accessKey + secretKey. 6 | // This type is public because it's an internal package. 7 | type Token struct { 8 | AccessKey string 9 | SecretKey string 10 | } 11 | 12 | // XAuthTokenHeader is Scaleway standard auth header 13 | const XAuthTokenHeader = "X-Auth-Token" 14 | 15 | // NewToken create a token authentication from an 16 | // access key and a secret key 17 | func NewToken(accessKey, secretKey string) *Token { 18 | return &Token{AccessKey: accessKey, SecretKey: secretKey} 19 | } 20 | 21 | // Headers returns headers that must be add to the http request 22 | func (t *Token) Headers() http.Header { 23 | headers := http.Header{} 24 | headers.Set(XAuthTokenHeader, t.SecretKey) 25 | return headers 26 | } 27 | 28 | // AnonymizedHeaders returns an anonymized version of Headers() 29 | // This method could be use for logging purpose. 30 | func (t *Token) AnonymizedHeaders() http.Header { 31 | headers := http.Header{} 32 | headers.Set(XAuthTokenHeader, HideSecretKey(t.SecretKey)) 33 | return headers 34 | } 35 | 36 | func HideSecretKey(k string) string { 37 | switch { 38 | case len(k) == 0: 39 | return "" 40 | case len(k) > 8: 41 | return k[0:8] + "-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 42 | default: 43 | return "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /vendor/github.com/dnaeon/go-vcr/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Marin Atanasov Nikolov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer 10 | in this position and unchanged. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/LICENSE.libyaml: -------------------------------------------------------------------------------- 1 | The following files were ported to Go from C files of libyaml, and thus 2 | are still covered by their original copyright and license: 3 | 4 | apic.go 5 | emitterc.go 6 | parserc.go 7 | readerc.go 8 | scannerc.go 9 | writerc.go 10 | yamlh.go 11 | yamlprivateh.go 12 | 13 | Copyright (c) 2006 Kirill Simonov 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of 16 | this software and associated documentation files (the "Software"), to deal in 17 | the Software without restriction, including without limitation the rights to 18 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 19 | of the Software, and to permit persons to whom the Software is furnished to do 20 | so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | -------------------------------------------------------------------------------- /scw/errors_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 11 | ) 12 | 13 | func TestHasResponseErrorWithStatus200(t *testing.T) { 14 | 15 | res := &http.Response{StatusCode: 200} 16 | 17 | newErr := hasResponseError(res) 18 | testhelpers.AssertNoError(t, newErr) 19 | 20 | } 21 | 22 | func TestHasResponseErrorWithoutBody(t *testing.T) { 23 | 24 | res := &http.Response{StatusCode: 400} 25 | 26 | newErr := hasResponseError(res) 27 | testhelpers.Assert(t, newErr != nil, "Should have error") 28 | 29 | } 30 | 31 | func TestHasResponseErrorWithValidError(t *testing.T) { 32 | 33 | var ( 34 | errorMessage = "some message" 35 | errorType = "some type" 36 | errorFields = map[string][]string{"some_field": {"some_value"}} 37 | errorStatusCode = 400 38 | errorStatus = "400 Bad Request" 39 | ) 40 | 41 | // Create expected error response 42 | testErrorReponse := &ResponseError{ 43 | Message: errorMessage, 44 | Type: errorType, 45 | Fields: errorFields, 46 | StatusCode: errorStatusCode, 47 | Status: errorStatus, 48 | } 49 | 50 | // Create response body with marshalled error response 51 | bodyBytes, err := json.Marshal(testErrorReponse) 52 | testhelpers.AssertNoError(t, err) 53 | res := &http.Response{Status: errorStatus, StatusCode: errorStatusCode, Body: ioutil.NopCloser(bytes.NewReader(bodyBytes))} 54 | 55 | // Test hasResponseError() 56 | newErr := hasResponseError(res) 57 | testhelpers.Assert(t, newErr != nil, "Should have error") 58 | testhelpers.Equals(t, testErrorReponse, newErr) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /vendor/github.com/dnaeon/go-vcr/recorder/go18_nobody.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Marin Atanasov Nikolov 2 | // Copyright (c) 2016 David Jack 3 | // All rights reserved. 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions 7 | // are met: 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | // 26 | // +build go1.8 27 | 28 | package recorder 29 | 30 | import ( 31 | "io" 32 | "net/http" 33 | ) 34 | 35 | // isNoBody returns true iff r is an http.NoBody. 36 | func isNoBody(r io.ReadCloser) bool { return r == http.NoBody } 37 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "os" 4 | 5 | type LogLevel int 6 | 7 | const ( 8 | // LogLevelDebug indicates Debug severity. 9 | LogLevelDebug LogLevel = iota 10 | // LogLevelInfo indicates Info severity. 11 | LogLevelInfo 12 | // LogLevelWarning indicates Warning severity. 13 | LogLevelWarning 14 | // LogLevelError indicates Error severity. 15 | LogLevelError 16 | ) 17 | 18 | // severityName contains the string representation of each severity. 19 | var severityName = []string{ 20 | LogLevelDebug: "DEBUG", 21 | LogLevelInfo: "INFO", 22 | LogLevelWarning: "WARNING", 23 | LogLevelError: "ERROR", 24 | } 25 | 26 | // Logger does underlying logging work for scaleway-sdk-go. 27 | type Logger interface { 28 | // Debugf logs to DEBUG log. Arguments are handled in the manner of fmt.Printf. 29 | Debugf(format string, args ...interface{}) 30 | // Infof logs to INFO log. Arguments are handled in the manner of fmt.Printf. 31 | Infof(format string, args ...interface{}) 32 | // Warningf logs to WARNING log. Arguments are handled in the manner of fmt.Printf. 33 | Warningf(format string, args ...interface{}) 34 | // Errorf logs to ERROR log. Arguments are handled in the manner of fmt.Printf. 35 | Errorf(format string, args ...interface{}) 36 | // ShouldLog reports whether verbosity level l is at least the requested verbose level. 37 | ShouldLog(level LogLevel) bool 38 | } 39 | 40 | // SetLogger sets logger that is used in by the SDK. 41 | // Not mutex-protected, should be called before any scaleway-sdk-go functions. 42 | func SetLogger(l Logger) { 43 | logger = l 44 | } 45 | 46 | // EnableDebugMode enable LogLevelDebug on the default logger. 47 | // If a custom logger was provided with SetLogger this method has no effect. 48 | func EnableDebugMode() { 49 | DefaultLogger.Init(os.Stderr, LogLevelDebug) 50 | } 51 | -------------------------------------------------------------------------------- /api/marketplace/v1/marketplace_utils_test.go: -------------------------------------------------------------------------------- 1 | package marketplace 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers/httprecorder" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | ) 10 | 11 | func TestGetImageByLabel(t *testing.T) { 12 | 13 | client, r, err := httprecorder.CreateRecordedScwClient("go-vcr") 14 | testhelpers.AssertNoError(t, err) 15 | defer func() { 16 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 17 | }() 18 | 19 | t.Run("matching input for GetLocalImageIDByLabel", func(t *testing.T) { 20 | 21 | // Create SDK objects for Scaleway Instance product 22 | marketplaceAPI := NewAPI(client) 23 | 24 | imageID, err := marketplaceAPI.GetLocalImageIDByLabel(&GetLocalImageIDByLabelRequest{ 25 | Zone: scw.ZoneFrPar1, 26 | CommercialType: "DEV1-S", 27 | ImageLabel: "ubuntu-bionic", 28 | }) 29 | testhelpers.AssertNoError(t, err) 30 | 31 | // ubuntu-bionic DEV1-S at par1: f974feac-abae-4365-b988-8ec7d1cec10d 32 | testhelpers.Equals(t, "f974feac-abae-4365-b988-8ec7d1cec10d", imageID) 33 | 34 | }) 35 | 36 | t.Run("non-matching label for GetLocalImageIDByLabel", func(t *testing.T) { 37 | 38 | // Create SDK objects for Scaleway Instance product 39 | marketplaceAPI := NewAPI(client) 40 | 41 | _, err := marketplaceAPI.GetLocalImageIDByLabel(&GetLocalImageIDByLabelRequest{ 42 | Zone: scw.ZoneFrPar1, 43 | CommercialType: "DEV1-S", 44 | ImageLabel: "foo-bar-image", 45 | }) 46 | testhelpers.Assert(t, err != nil, "Should have error") 47 | testhelpers.Equals(t, "scaleway-sdk-go: couldn't find a matching image for the given label (foo-bar-image), zone (fr-par-1) and commercial type (DEV1-S)", err.Error()) 48 | 49 | }) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /vendor/github.com/dnaeon/go-vcr/recorder/go17_nobody.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Marin Atanasov Nikolov 2 | // Copyright (c) 2016 David Jack 3 | // All rights reserved. 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions 7 | // are met: 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | // 26 | // +build !go1.8 27 | 28 | package recorder 29 | 30 | import ( 31 | "io" 32 | ) 33 | 34 | // isNoBody returns true iff r is an http.NoBody. 35 | // http.NoBody didn't exist before Go 1.7, so the version in this file 36 | // always returns false. 37 | func isNoBody(r io.ReadCloser) bool { return false } 38 | -------------------------------------------------------------------------------- /scw/locality_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 8 | ) 9 | 10 | func TestParseZone(t *testing.T) { 11 | 12 | tests := []struct { 13 | input string 14 | expected Zone 15 | }{ 16 | { 17 | input: "fr-par-1", 18 | expected: ZoneFrPar1, 19 | }, 20 | { 21 | input: "par1", 22 | expected: ZoneFrPar1, 23 | }, 24 | { 25 | input: "ams1", 26 | expected: ZoneNlAms1, 27 | }, 28 | } 29 | 30 | for _, test := range tests { 31 | z, err := ParseZone(test.input) 32 | testhelpers.AssertNoError(t, err) 33 | testhelpers.Equals(t, test.expected, z) 34 | } 35 | 36 | } 37 | 38 | func TestZoneJSONUnmarshall(t *testing.T) { 39 | 40 | t.Run("test with zone", func(t *testing.T) { 41 | 42 | input := `{"Test": "par1"}` 43 | value := struct{ Test Zone }{} 44 | 45 | err := json.Unmarshal([]byte(input), &value) 46 | testhelpers.AssertNoError(t, err) 47 | 48 | testhelpers.Equals(t, ZoneFrPar1, value.Test) 49 | 50 | }) 51 | 52 | t.Run("test with region", func(t *testing.T) { 53 | 54 | input := `{"Test": "par1"}` 55 | value := struct{ Test Region }{} 56 | 57 | err := json.Unmarshal([]byte(input), &value) 58 | testhelpers.AssertNoError(t, err) 59 | 60 | testhelpers.Equals(t, RegionFrPar, value.Test) 61 | 62 | }) 63 | 64 | } 65 | 66 | func TestParseRegion(t *testing.T) { 67 | 68 | tests := []struct { 69 | input string 70 | expected Region 71 | }{ 72 | { 73 | input: "fr-par", 74 | expected: RegionFrPar, 75 | }, 76 | { 77 | input: "par1", 78 | expected: RegionFrPar, 79 | }, 80 | { 81 | input: "ams1", 82 | expected: RegionNlAms, 83 | }, 84 | } 85 | 86 | for _, test := range tests { 87 | r, err := ParseRegion(test.input) 88 | testhelpers.AssertNoError(t, err) 89 | testhelpers.Equals(t, test.expected, r) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /api/baremetal/v1alpha1/server_utils.go: -------------------------------------------------------------------------------- 1 | package baremetal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/internal/async" 7 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | ) 10 | 11 | // WaitForServerRequest is used by WaitForServer method 12 | type WaitForServerRequest struct { 13 | ServerID string 14 | Zone scw.Zone 15 | Timeout time.Duration 16 | } 17 | 18 | // WaitForServer wait for the server to be in a "terminal state" before returning. 19 | // This function can be used to wait for a server to be installed for example. 20 | func (s *API) WaitForServer(req *WaitForServerRequest) (*Server, scw.SdkError) { 21 | 22 | terminalStatus := map[ServerStatus]struct{}{ 23 | ServerStatusReady: {}, 24 | ServerStatusStopped: {}, 25 | ServerStatusError: {}, 26 | ServerStatusUnknown: {}, 27 | } 28 | 29 | installTerminalStatus := map[ServerInstallStatus]struct{}{ 30 | ServerInstallStatusCompleted: {}, 31 | ServerInstallStatusToInstall: {}, 32 | ServerInstallStatusError: {}, 33 | ServerInstallStatusUnknown: {}, 34 | } 35 | 36 | server, err := async.WaitSync(&async.WaitSyncConfig{ 37 | Get: func() (interface{}, error, bool) { 38 | res, err := s.GetServer(&GetServerRequest{ 39 | ServerID: req.ServerID, 40 | Zone: req.Zone, 41 | }) 42 | 43 | if err != nil { 44 | return nil, err, false 45 | } 46 | _, isTerminal := terminalStatus[res.Status] 47 | isTerminalInstall := true 48 | if res.Install != nil { 49 | _, isTerminalInstall = installTerminalStatus[res.Install.Status] 50 | } 51 | 52 | return res, err, isTerminal && isTerminalInstall 53 | }, 54 | Timeout: req.Timeout, 55 | IntervalStrategy: async.LinearIntervalStrategy(5 * time.Second), 56 | }) 57 | if err != nil { 58 | return nil, errors.Wrap(err, "waiting for server failed") 59 | } 60 | 61 | return server.(*Server), nil 62 | } 63 | -------------------------------------------------------------------------------- /api/instance/v1/volume_utils_test.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers/httprecorder" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | ) 10 | 11 | func TestUpdateVolume(t *testing.T) { 12 | 13 | client, r, err := httprecorder.CreateRecordedScwClient("volume-utils-test") 14 | testhelpers.AssertNoError(t, err) 15 | defer func() { 16 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 17 | }() 18 | 19 | instanceAPI := NewAPI(client) 20 | 21 | var ( 22 | zone = scw.ZoneFrPar1 23 | organization = "d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3" 24 | volumeName = "test volume" 25 | volumeSize = 20 * scw.GB 26 | volumeType = VolumeTypeLSSD 27 | newVolumeName = "some new volume name" 28 | 29 | volumeID string 30 | ) 31 | 32 | // Create volume 33 | createVolumeResponse, err := instanceAPI.CreateVolume(&CreateVolumeRequest{ 34 | Zone: zone, 35 | Name: volumeName, 36 | Organization: organization, 37 | Size: &volumeSize, 38 | VolumeType: volumeType, 39 | }) 40 | 41 | testhelpers.AssertNoError(t, err) 42 | 43 | volumeID = createVolumeResponse.Volume.ID 44 | 45 | // Update volume and test whether successfully updated 46 | updateVolumeResponse, err := instanceAPI.UpdateVolume(&UpdateVolumeRequest{ 47 | Zone: zone, 48 | Name: &newVolumeName, 49 | VolumeID: volumeID, 50 | }) 51 | 52 | testhelpers.AssertNoError(t, err) 53 | testhelpers.Assert(t, updateVolumeResponse.Volume != nil, "Should have volume in response") 54 | testhelpers.Equals(t, newVolumeName, updateVolumeResponse.Volume.Name) 55 | testhelpers.Equals(t, volumeSize, updateVolumeResponse.Volume.Size) // check that server is not changed 56 | 57 | // Delete Volume 58 | err = instanceAPI.DeleteVolume(&DeleteVolumeRequest{ 59 | Zone: zone, 60 | VolumeID: volumeID, 61 | }) 62 | testhelpers.AssertNoError(t, err) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 10 | ) 11 | 12 | var ( 13 | expectedErrorf = "ERROR: cd" 14 | expectedWarningf = "WARNING: ij" 15 | expectedInfof = "INFO: op" 16 | expectedDebugf = "DEBUG: uv" 17 | ) 18 | 19 | func TestDebug(t *testing.T) { 20 | buf := &bytes.Buffer{} 21 | logThings(newLogger(buf, LogLevelDebug)) 22 | testThings(t, []string{ 23 | expectedErrorf, 24 | expectedWarningf, 25 | expectedInfof, 26 | expectedDebugf, 27 | }, buf.String()) 28 | } 29 | 30 | func TestInfo(t *testing.T) { 31 | buf := &bytes.Buffer{} 32 | logThings(newLogger(buf, LogLevelInfo)) 33 | testThings(t, []string{ 34 | expectedErrorf, 35 | expectedWarningf, 36 | expectedInfof, 37 | }, buf.String()) 38 | } 39 | 40 | func TestWarning(t *testing.T) { 41 | buf := &bytes.Buffer{} 42 | logThings(newLogger(buf, LogLevelWarning)) 43 | testThings(t, []string{ 44 | expectedErrorf, 45 | expectedWarningf, 46 | }, buf.String()) 47 | } 48 | 49 | func TestError(t *testing.T) { 50 | buf := &bytes.Buffer{} 51 | logThings(newLogger(buf, LogLevelError)) 52 | testThings(t, []string{ 53 | expectedErrorf, 54 | }, buf.String()) 55 | } 56 | 57 | func TestEnableDebugMode(t *testing.T) { 58 | _defaultLogger := DefaultLogger 59 | 60 | DefaultLogger = newLogger(os.Stderr, LogLevelWarning) 61 | EnableDebugMode() 62 | testhelpers.Equals(t, true, DefaultLogger.ShouldLog(LogLevelDebug)) 63 | 64 | DefaultLogger = _defaultLogger 65 | } 66 | 67 | func testThings(t *testing.T, expectedEvents []string, actualOutput string) { 68 | lines := strings.Split(actualOutput, "\n") 69 | for i, line := range lines[:len(lines)-1] { // last line is always empty 70 | tmp := strings.Split(line, " ") 71 | actualMessage := strings.Join(append([]string{tmp[0]}, tmp[3:]...), " ") // date and hour is not kept 72 | testhelpers.Equals(t, expectedEvents[i], actualMessage) 73 | } 74 | } 75 | 76 | func logThings(log Logger) { 77 | log.Errorf("c%s", "d") 78 | log.Warningf("i%s", "j") 79 | log.Infof("o%s", "p") 80 | log.Debugf("u%s", "v") 81 | } 82 | -------------------------------------------------------------------------------- /scw/errors.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 9 | ) 10 | 11 | // SdkError is a base interface for all Scaleway SDK errors. 12 | type SdkError interface { 13 | Error() string 14 | IsScwSdkError() 15 | } 16 | 17 | // ResponseError is an error type for the Scaleway API 18 | type ResponseError struct { 19 | // Message is a human-friendly error message 20 | Message string `json:"message"` 21 | 22 | // Type is a string code that defines the kind of error. This field is only used by instance API 23 | Type string `json:"type,omitempty"` 24 | 25 | // Resource is a string code that defines the resource concerned by the error. This field is only used by instance API 26 | Resource string `json:"resource,omitempty"` 27 | 28 | // Fields contains detail about validation error. This field is only used by instance API 29 | Fields map[string][]string `json:"fields,omitempty"` 30 | 31 | // StatusCode is the HTTP status code received 32 | StatusCode int `json:"-"` 33 | 34 | // Status is the HTTP status received 35 | Status string `json:"-"` 36 | } 37 | 38 | func (e *ResponseError) Error() string { 39 | s := fmt.Sprintf("scaleway-sdk-go: http error %s", e.Status) 40 | 41 | if e.Resource != "" { 42 | s = fmt.Sprintf("%s: resource %s", s, e.Resource) 43 | } 44 | 45 | if e.Message != "" { 46 | s = fmt.Sprintf("%s: %s", s, e.Message) 47 | } 48 | 49 | if len(e.Fields) > 0 { 50 | s = fmt.Sprintf("%s: %v", s, e.Fields) 51 | } 52 | 53 | return s 54 | } 55 | 56 | // IsScwSdkError implement SdkError interface 57 | func (e *ResponseError) IsScwSdkError() {} 58 | 59 | // hasResponseError throws an error when the HTTP status is not OK 60 | func hasResponseError(res *http.Response) SdkError { 61 | if res.StatusCode >= 200 && res.StatusCode <= 299 { 62 | return nil 63 | } 64 | 65 | newErr := &ResponseError{ 66 | StatusCode: res.StatusCode, 67 | Status: res.Status, 68 | } 69 | 70 | if res.Body == nil { 71 | return newErr 72 | } 73 | 74 | err := json.NewDecoder(res.Body).Decode(newErr) 75 | if err != nil { 76 | return errors.Wrap(err, "could not parse error response body") 77 | } 78 | 79 | return newErr 80 | } 81 | -------------------------------------------------------------------------------- /internal/async/wait.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | var ( 9 | defaultInterval = time.Second 10 | defaultTimeout = time.Minute * 5 11 | ) 12 | 13 | type IntervalStrategy func() <-chan time.Time 14 | 15 | // WaitSyncConfig defines the waiting options. 16 | type WaitSyncConfig struct { 17 | // This method will be called from another goroutine. 18 | Get func() (value interface{}, err error, isTerminal bool) 19 | IntervalStrategy IntervalStrategy 20 | Timeout time.Duration 21 | } 22 | 23 | // LinearIntervalStrategy defines a linear interval duration. 24 | func LinearIntervalStrategy(interval time.Duration) IntervalStrategy { 25 | return func() <-chan time.Time { 26 | return time.After(interval) 27 | } 28 | } 29 | 30 | // FibonacciIntervalStrategy defines an interval duration who follow the Fibonacci sequence. 31 | func FibonacciIntervalStrategy(base time.Duration, factor float32) IntervalStrategy { 32 | var x, y float32 = 0, 1 33 | 34 | return func() <-chan time.Time { 35 | x, y = y, x+(y*factor) 36 | return time.After(time.Duration(x) * base) 37 | } 38 | } 39 | 40 | // WaitSync waits and returns when a given stop condition is true or if an error occurs. 41 | func WaitSync(config *WaitSyncConfig) (terminalValue interface{}, err error) { 42 | // initialize configuration 43 | if config.IntervalStrategy == nil { 44 | config.IntervalStrategy = LinearIntervalStrategy(defaultInterval) 45 | } 46 | if config.Timeout == 0 { 47 | config.Timeout = defaultTimeout 48 | } 49 | 50 | resultValue := make(chan interface{}) 51 | resultErr := make(chan error) 52 | timeout := make(chan bool) 53 | 54 | go func() { 55 | for { 56 | // get the payload 57 | value, err, stopCondition := config.Get() 58 | 59 | // send the payload 60 | if err != nil { 61 | resultErr <- err 62 | return 63 | } 64 | if stopCondition { 65 | resultValue <- value 66 | return 67 | } 68 | 69 | // waiting for an interval before next get() call or a timeout 70 | select { 71 | case <-timeout: 72 | return 73 | case <-config.IntervalStrategy(): 74 | // sleep 75 | } 76 | } 77 | }() 78 | 79 | // waiting for a result or a timeout 80 | select { 81 | case val := <-resultValue: 82 | return val, nil 83 | case err := <-resultErr: 84 | return nil, err 85 | case <-time.After(config.Timeout): 86 | timeout <- true 87 | return nil, fmt.Errorf("timeout after %v", config.Timeout) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /scw/path.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | const ( 10 | unixHomeDirEnv = "HOME" 11 | windowsHomeDirEnv = "USERPROFILE" 12 | xdgConfigDirEnv = "XDG_CONFIG_HOME" 13 | 14 | defaultConfigFileName = "config.yaml" 15 | ) 16 | 17 | var ( 18 | // ErrNoHomeDir errors when no user directory is found 19 | ErrNoHomeDir = errors.New("user home directory not found") 20 | ) 21 | 22 | func inConfigFile() string { 23 | v2path, exist := getConfigV2FilePath() 24 | if exist { 25 | return "in config file " + v2path 26 | } 27 | return "" 28 | } 29 | 30 | // GetConfigPath returns the default path. 31 | // Default path is base on the following priority order: 32 | // - $SCW_CONFIG_PATH 33 | // - $XDG_CONFIG_HOME/scw/config.yaml 34 | // - $HOME/.config/scw/config.yaml 35 | // - $USERPROFILE/.config/scw/config.yaml 36 | func GetConfigPath() string { 37 | configPath := os.Getenv(scwConfigPathEnv) 38 | if configPath == "" { 39 | configPath, _ = getConfigV2FilePath() 40 | } 41 | return filepath.Clean(configPath) 42 | } 43 | 44 | // getConfigV2FilePath returns the path to the Scaleway CLI config file 45 | func getConfigV2FilePath() (string, bool) { 46 | configDir, err := getScwConfigDir() 47 | if err != nil { 48 | return "", false 49 | } 50 | return filepath.Clean(filepath.Join(configDir, defaultConfigFileName)), true 51 | } 52 | 53 | // getConfigV1FilePath returns the path to the Scaleway CLI config file 54 | func getConfigV1FilePath() (string, bool) { 55 | path, err := getHomeDir() 56 | if err != nil { 57 | return "", false 58 | } 59 | return filepath.Clean(filepath.Join(path, ".scwrc")), true 60 | } 61 | 62 | // getScwConfigDir returns the path to scw config folder 63 | func getScwConfigDir() (string, error) { 64 | if xdgPath := os.Getenv(xdgConfigDirEnv); xdgPath != "" { 65 | return filepath.Join(xdgPath, "scw"), nil 66 | } 67 | 68 | homeDir, err := getHomeDir() 69 | if err != nil { 70 | return "", err 71 | } 72 | return filepath.Join(homeDir, ".config", "scw"), nil 73 | } 74 | 75 | // getHomeDir returns the path to your home directory 76 | func getHomeDir() (string, error) { 77 | switch { 78 | case os.Getenv(unixHomeDirEnv) != "": 79 | return os.Getenv(unixHomeDirEnv), nil 80 | case os.Getenv(windowsHomeDirEnv) != "": 81 | return os.Getenv(windowsHomeDirEnv), nil 82 | default: 83 | return "", ErrNoHomeDir 84 | } 85 | } 86 | 87 | func fileExist(name string) bool { 88 | _, err := os.Stat(name) 89 | return err == nil 90 | } 91 | -------------------------------------------------------------------------------- /scw/request.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/scaleway/scaleway-sdk-go/internal/auth" 12 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 13 | ) 14 | 15 | // ScalewayRequest contains all the contents related to performing a request on the Scaleway API. 16 | type ScalewayRequest struct { 17 | Method string 18 | Path string 19 | Headers http.Header 20 | Query url.Values 21 | Body io.Reader 22 | 23 | // request options 24 | ctx context.Context 25 | auth auth.Auth 26 | allPages bool 27 | } 28 | 29 | // getAllHeaders constructs a http.Header object and aggregates all headers into the object. 30 | func (req *ScalewayRequest) getAllHeaders(token auth.Auth, userAgent string, anonymized bool) http.Header { 31 | var allHeaders http.Header 32 | if anonymized { 33 | allHeaders = token.AnonymizedHeaders() 34 | } else { 35 | allHeaders = token.Headers() 36 | } 37 | 38 | allHeaders.Set("User-Agent", userAgent) 39 | if req.Body != nil { 40 | allHeaders.Set("Content-Type", "application/json") 41 | } 42 | for key, value := range req.Headers { 43 | allHeaders.Del(key) 44 | for _, v := range value { 45 | allHeaders.Add(key, v) 46 | } 47 | } 48 | 49 | return allHeaders 50 | } 51 | 52 | // getURL constructs a URL based on the base url and the client. 53 | func (req *ScalewayRequest) getURL(baseURL string) (*url.URL, SdkError) { 54 | url, err := url.Parse(baseURL + req.Path) 55 | if err != nil { 56 | return nil, errors.New("invalid url %s: %s", baseURL+req.Path, err) 57 | } 58 | url.RawQuery = req.Query.Encode() 59 | 60 | return url, nil 61 | } 62 | 63 | // SetBody json marshal the given body and write the json content type 64 | // to the request. It also catches when body is a file. 65 | func (req *ScalewayRequest) SetBody(body interface{}) error { 66 | var contentType string 67 | var content io.Reader 68 | 69 | switch b := body.(type) { 70 | case *File: 71 | contentType = b.ContentType 72 | content = b.Content 73 | case io.Reader: 74 | contentType = "text/plain" 75 | content = b 76 | default: 77 | buf, err := json.Marshal(body) 78 | if err != nil { 79 | return err 80 | } 81 | contentType = "application/json" 82 | content = bytes.NewReader(buf) 83 | } 84 | 85 | if req.Headers == nil { 86 | req.Headers = http.Header{} 87 | } 88 | 89 | req.Headers.Set("Content-Type", contentType) 90 | req.Body = content 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /scw/config_legacy.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 10 | "github.com/scaleway/scaleway-sdk-go/logger" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // configV1 is a Scaleway CLI configuration file 15 | type configV1 struct { 16 | // Organization is the identifier of the Scaleway organization 17 | Organization string `json:"organization"` 18 | 19 | // Token is the authentication token for the Scaleway organization 20 | Token string `json:"token"` 21 | 22 | // Version is the actual version of scw CLI 23 | Version string `json:"version"` 24 | } 25 | 26 | func unmarshalConfV1(content []byte) (*configV1, error) { 27 | var config configV1 28 | err := json.Unmarshal(content, &config) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return &config, err 33 | } 34 | 35 | func (v1 *configV1) toV2() *Config { 36 | return &Config{ 37 | Profile: Profile{ 38 | DefaultOrganizationID: &v1.Organization, 39 | SecretKey: &v1.Token, 40 | // ignore v1 version 41 | }, 42 | } 43 | } 44 | 45 | // MigrateLegacyConfig will migrate the legacy config to the V2 when none exist yet. 46 | // Returns a boolean set to true when the migration happened. 47 | // TODO: get accesskey from account? 48 | func MigrateLegacyConfig() (bool, error) { 49 | // STEP 1: try to load config file V2 50 | v2Path, v2PathOk := getConfigV2FilePath() 51 | if !v2PathOk || fileExist(v2Path) { 52 | return false, nil 53 | } 54 | 55 | // STEP 2: try to load config file V1 56 | v1Path, v1PathOk := getConfigV1FilePath() 57 | if !v1PathOk { 58 | return false, nil 59 | } 60 | file, err := ioutil.ReadFile(v1Path) 61 | if err != nil { 62 | return false, nil 63 | } 64 | confV1, err := unmarshalConfV1(file) 65 | if err != nil { 66 | return false, errors.Wrap(err, "content of config file %s is invalid json", v1Path) 67 | } 68 | 69 | // STEP 3: create dir 70 | err = os.MkdirAll(filepath.Dir(v2Path), 0700) 71 | if err != nil { 72 | return false, errors.Wrap(err, "mkdir did not work on %s", filepath.Dir(v2Path)) 73 | } 74 | 75 | // STEP 4: marshal yaml config 76 | newConfig := confV1.toV2() 77 | file, err = yaml.Marshal(newConfig) 78 | if err != nil { 79 | return false, err 80 | } 81 | 82 | // STEP 5: save config 83 | err = ioutil.WriteFile(v2Path, file, defaultConfigPermission) 84 | if err != nil { 85 | return false, errors.Wrap(err, "cannot write file %s", v2Path) 86 | } 87 | 88 | // STEP 6: log success 89 | logger.Warningf("migrated existing config to %s", v2Path) 90 | return true, nil 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | GoDoc 5 | CircleCI 6 | GoReportCard 7 |

8 | 9 | # Scaleway GO SDK 10 | 11 | 12 | **:warning: This is an early release, keep in mind that the API can break** 13 | 14 | 15 | Scaleway is a simple way to build, deploy and scale your infrastructure in the cloud. 16 | We help thousands of developers and businesses to run their infrastructures without any issue. 17 | 18 | ## Documentation 19 | 20 | - [Godoc](https://godoc.org/github.com/scaleway/scaleway-sdk-go) 21 | - [Developers website](https://developers.scaleway.com) (API documentation) 22 | 23 | ## Installation 24 | 25 | ```bash 26 | go get github.com/scaleway/scaleway-sdk-go 27 | ``` 28 | 29 | ## Getting Started 30 | 31 | ```go 32 | package main 33 | 34 | import ( 35 | "fmt" 36 | 37 | "github.com/scaleway/scaleway-sdk-go/api/instance/v1" 38 | "github.com/scaleway/scaleway-sdk-go/scw" 39 | "github.com/scaleway/scaleway-sdk-go/utils" 40 | ) 41 | 42 | func main() { 43 | 44 | // Create a Scaleway client 45 | client, err := scw.NewClient( 46 | // Get your credentials at https://console.scaleway.com/account/credentials 47 | scw.WithDefaultOrganizationID("ORGANISATION_ID"), 48 | scw.WithAuth("ACCESS_KEY", "SECRET_KEY"), 49 | ) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | // Create SDK objects for Scaleway Instance product 55 | instanceApi := instance.NewAPI(client) 56 | 57 | // Call the ListServers method on the Instance SDK 58 | response, err := instanceApi.ListServers(&instance.ListServersRequest{ 59 | Zone: scw.ZoneFrPar1, 60 | }) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | // Do something with the response... 66 | for _, server := range response.Servers { 67 | fmt.Println("Server", server.ID, server.Name) 68 | } 69 | 70 | } 71 | ``` 72 | 73 | ## Examples 74 | 75 | You can find additional examples in the [GoDoc](https://godoc.org/github.com/scaleway/scaleway-sdk-go). 76 | 77 | ## Development 78 | 79 | This repository is at its early stage and is still in active development. 80 | If you are looking for a way to contribute please read [CONTRIBUTING.md](CONTRIBUTING.md). 81 | 82 | ## Reach us 83 | 84 | We love feedback. 85 | Feel free to reach us on [Scaleway Slack community](https://slack.scaleway.com/), we are waiting for you on [#opensource](https://scaleway-community.slack.com/app_redirect?channel=opensource). 86 | -------------------------------------------------------------------------------- /scw/config_legacy_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 10 | ) 11 | 12 | // TestMigrateLegacyConfig tests legacy config properly migrate to V2 config 13 | func TestMigrateLegacyConfig(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | env map[string]string 17 | files map[string]string 18 | isMigrated bool 19 | 20 | expectedFiles map[string]string 21 | }{ 22 | { 23 | name: "No config path", 24 | isMigrated: false, 25 | files: map[string]string{ 26 | ".scwrc": v1ValidConfigFile, 27 | }, 28 | }, 29 | { 30 | name: "Default config path", 31 | isMigrated: true, 32 | env: map[string]string{ 33 | "HOME": "{HOME}", 34 | }, 35 | files: map[string]string{ 36 | ".scwrc": v1ValidConfigFile, 37 | }, 38 | expectedFiles: map[string]string{ 39 | ".config/scw/config.yaml": v2FromV1ConfigFile, 40 | }, 41 | }, 42 | { 43 | name: "V2 config already exist", 44 | isMigrated: false, 45 | env: map[string]string{ 46 | "HOME": "{HOME}", 47 | }, 48 | files: map[string]string{ 49 | ".config/scw/config.yaml": v2SimpleValidConfigFile, 50 | ".scwrc": v1ValidConfigFile, 51 | }, 52 | expectedFiles: map[string]string{ 53 | ".config/scw/config.yaml": v2SimpleValidConfigFile, 54 | }, 55 | }, 56 | } 57 | // create home dir 58 | dir := initEnv(t) 59 | 60 | // delete home dir and reset env variables 61 | defer resetEnv(t, os.Environ(), dir) 62 | 63 | for _, test := range tests { 64 | t.Run(test.name, func(t *testing.T) { 65 | // set up env and config file(s) 66 | setEnv(t, test.env, test.files, dir) 67 | 68 | // remove config file(s) 69 | defer cleanEnv(t, test.files, dir) 70 | defer cleanEnv(t, test.expectedFiles, dir) 71 | 72 | isMigrated, err := MigrateLegacyConfig() 73 | testhelpers.AssertNoError(t, err) 74 | testhelpers.Equals(t, test.isMigrated, isMigrated) 75 | 76 | // test expected files 77 | for fileName, expectedContent := range test.expectedFiles { 78 | content, err := ioutil.ReadFile(filepath.Join(dir, fileName)) 79 | testhelpers.AssertNoError(t, err) 80 | testhelpers.Equals(t, expectedContent, string(content)) 81 | } 82 | 83 | }) 84 | } 85 | } 86 | 87 | // v1 config 88 | var ( 89 | v1ValidOrganizationID = "29aa5db6-1d6d-404e-890d-f896913f9ec1" 90 | v1ValidToken = "a057b0c1-eb47-4bf8-a589-72c1f2029515" 91 | v1Version = "1.19" 92 | 93 | v1ValidConfigFile = `{ 94 | "organization":"` + v1ValidOrganizationID + `", 95 | "token":"` + v1ValidToken + `", 96 | "version":"` + v1Version + `" 97 | }` 98 | 99 | v1InvalidConfigFile = ` 100 | "organization":"` + v1ValidOrganizationID + `", 101 | "token":"` + v1ValidToken + `", 102 | "version":"` + v1Version + `" 103 | ` 104 | ) 105 | -------------------------------------------------------------------------------- /internal/testhelpers/httprecorder/recorder.go: -------------------------------------------------------------------------------- 1 | package httprecorder 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "strings" 8 | 9 | "github.com/dnaeon/go-vcr/cassette" 10 | "github.com/dnaeon/go-vcr/recorder" 11 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 12 | "github.com/scaleway/scaleway-sdk-go/scw" 13 | ) 14 | 15 | // UpdateCassette is true when we are updating the cassette 16 | var UpdateCassette = false 17 | 18 | // CreateRecordedScwClient creates a new scw.Client that records all HTTP requests in a cassette. 19 | // This cassette is then replayed whenever tests are executed again. This means that once the 20 | // requests are recorded in the cassette, no more real HTTP request must be made to run the tests. 21 | // 22 | // It is important to call add a `defer recorder.Stop()` so the given cassette files are correctly 23 | // closed and saved after the requests. 24 | // 25 | // To update the cassette files, add `UPDATE` to the environment variables. 26 | // When using `UPDATE`, also the `SCW_ACCESS_KEY` and `SCW_SECRET_KEY` must be set. 27 | func CreateRecordedScwClient(cassetteName string) (*scw.Client, *recorder.Recorder, error) { 28 | 29 | _, UpdateCassette := os.LookupEnv("UPDATE") 30 | 31 | var activeProfile *scw.Profile 32 | 33 | if UpdateCassette { 34 | config, err := scw.LoadConfig() 35 | if err != nil { 36 | return nil, nil, err 37 | } 38 | activeProfile, err = config.GetActiveProfile() 39 | if err != nil { 40 | return nil, nil, err 41 | } 42 | } 43 | 44 | recorderMode := recorder.ModeReplaying 45 | if UpdateCassette { 46 | recorderMode = recorder.ModeRecording 47 | } 48 | 49 | // Setup recorder and scw client 50 | r, err := recorder.NewAsMode(fmt.Sprintf("testdata/%s", cassetteName), recorderMode, nil) 51 | if err != nil { 52 | return nil, nil, err 53 | } 54 | 55 | // Add a filter which removes Authorization headers from all requests: 56 | r.AddFilter(func(i *cassette.Interaction) error { 57 | delete(i.Request.Headers, "x-auth-token") 58 | delete(i.Request.Headers, "X-Auth-Token") 59 | 60 | if UpdateCassette { 61 | secretKey := *activeProfile.SecretKey 62 | if i != nil && strings.Contains(fmt.Sprintf("%v", *i), secretKey) { 63 | panic(errors.New("found secret key in cassette")) 64 | } 65 | } 66 | 67 | return nil 68 | }) 69 | 70 | // Create new http.Client where transport is the recorder 71 | httpClient := &http.Client{Transport: r} 72 | 73 | var client *scw.Client 74 | 75 | if UpdateCassette { 76 | // When updating the recoreded test requests, we need the access key and secret key. 77 | client, err = scw.NewClient( 78 | scw.WithHTTPClient(httpClient), 79 | scw.WithProfile(activeProfile), 80 | scw.WithEnv(), 81 | ) 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | } else { 86 | // No need for auth when using cassette 87 | client, err = scw.NewClient( 88 | scw.WithoutAuth(), 89 | scw.WithHTTPClient(httpClient), 90 | ) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | } 95 | 96 | return client, r, nil 97 | 98 | } 99 | -------------------------------------------------------------------------------- /scw/custom_types_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 9 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 10 | ) 11 | 12 | func TestSize_String(t *testing.T) { 13 | cases := []struct { 14 | size Size 15 | want string 16 | }{ 17 | {size: 42 * MB, want: "42000000"}, 18 | {size: 42 * B, want: "42"}, 19 | } 20 | 21 | for _, c := range cases { 22 | t.Run(c.want, func(t *testing.T) { 23 | testhelpers.Equals(t, c.want, c.size.String()) 24 | }) 25 | } 26 | } 27 | 28 | func TestTimeSeries_MarshallJSON(t *testing.T) { 29 | cases := []struct { 30 | name string 31 | ts *TimeSeries 32 | want string 33 | err error 34 | }{ 35 | { 36 | name: "basic", 37 | ts: &TimeSeries{ 38 | Name: "cpu_usage", 39 | Points: []*TimeSeriesPoint{ 40 | { 41 | Timestamp: time.Date(2019, time.August, 8, 15, 00, 00, 0, time.UTC), 42 | Value: 0.2, 43 | }, 44 | { 45 | Timestamp: time.Date(2019, time.August, 8, 15, 01, 00, 0, time.UTC), 46 | Value: 10.6, 47 | }, 48 | }, 49 | Metadata: map[string]string{ 50 | "node": "a77e0ce3", 51 | }, 52 | }, 53 | want: `{"name":"cpu_usage","points":[["2019-08-08T15:00:00Z",0.2],["2019-08-08T15:01:00Z",10.6]],"metadata":{"node":"a77e0ce3"}}`, 54 | }, 55 | } 56 | 57 | for _, c := range cases { 58 | t.Run(c.name, func(t *testing.T) { 59 | got, err := json.Marshal(c.ts) 60 | 61 | testhelpers.Equals(t, c.err, err) 62 | if c.err == nil { 63 | testhelpers.Equals(t, c.want, string(got)) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func TestTimeSeries_UnmarshallJSON(t *testing.T) { 70 | cases := []struct { 71 | name string 72 | json string 73 | want *TimeSeries 74 | err error 75 | }{ 76 | { 77 | name: "basic", 78 | json: ` 79 | { 80 | "name": "cpu_usage", 81 | "points": [ 82 | ["2019-08-08T15:00:00Z", 0.2], 83 | ["2019-08-08T15:01:00Z", 10.6] 84 | ], 85 | "metadata": { 86 | "node": "a77e0ce3" 87 | } 88 | } 89 | `, 90 | want: &TimeSeries{ 91 | Name: "cpu_usage", 92 | Points: []*TimeSeriesPoint{ 93 | { 94 | Timestamp: time.Date(2019, time.August, 8, 15, 00, 00, 0, time.UTC), 95 | Value: 0.2, 96 | }, 97 | { 98 | Timestamp: time.Date(2019, time.August, 8, 15, 01, 00, 0, time.UTC), 99 | Value: 10.6, 100 | }, 101 | }, 102 | Metadata: map[string]string{ 103 | "node": "a77e0ce3", 104 | }, 105 | }, 106 | }, 107 | { 108 | name: "with timestamp error", 109 | json: `{"name":"cpu_usage","points":[["2019/08/08T15-00-00Z",0.2]]}`, 110 | err: errors.New("2019/08/08T15-00-00Z timestamp is not in RFC 3339 format"), 111 | }, 112 | } 113 | 114 | for _, c := range cases { 115 | t.Run(c.name, func(t *testing.T) { 116 | ts := &TimeSeries{} 117 | err := json.Unmarshal([]byte(c.json), ts) 118 | 119 | testhelpers.Equals(t, c.err, err) 120 | if c.err == nil { 121 | testhelpers.Equals(t, c.want, ts) 122 | } 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/sorter.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "reflect" 5 | "unicode" 6 | ) 7 | 8 | type keyList []reflect.Value 9 | 10 | func (l keyList) Len() int { return len(l) } 11 | func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 12 | func (l keyList) Less(i, j int) bool { 13 | a := l[i] 14 | b := l[j] 15 | ak := a.Kind() 16 | bk := b.Kind() 17 | for (ak == reflect.Interface || ak == reflect.Ptr) && !a.IsNil() { 18 | a = a.Elem() 19 | ak = a.Kind() 20 | } 21 | for (bk == reflect.Interface || bk == reflect.Ptr) && !b.IsNil() { 22 | b = b.Elem() 23 | bk = b.Kind() 24 | } 25 | af, aok := keyFloat(a) 26 | bf, bok := keyFloat(b) 27 | if aok && bok { 28 | if af != bf { 29 | return af < bf 30 | } 31 | if ak != bk { 32 | return ak < bk 33 | } 34 | return numLess(a, b) 35 | } 36 | if ak != reflect.String || bk != reflect.String { 37 | return ak < bk 38 | } 39 | ar, br := []rune(a.String()), []rune(b.String()) 40 | for i := 0; i < len(ar) && i < len(br); i++ { 41 | if ar[i] == br[i] { 42 | continue 43 | } 44 | al := unicode.IsLetter(ar[i]) 45 | bl := unicode.IsLetter(br[i]) 46 | if al && bl { 47 | return ar[i] < br[i] 48 | } 49 | if al || bl { 50 | return bl 51 | } 52 | var ai, bi int 53 | var an, bn int64 54 | if ar[i] == '0' || br[i] == '0' { 55 | for j := i-1; j >= 0 && unicode.IsDigit(ar[j]); j-- { 56 | if ar[j] != '0' { 57 | an = 1 58 | bn = 1 59 | break 60 | } 61 | } 62 | } 63 | for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ { 64 | an = an*10 + int64(ar[ai]-'0') 65 | } 66 | for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ { 67 | bn = bn*10 + int64(br[bi]-'0') 68 | } 69 | if an != bn { 70 | return an < bn 71 | } 72 | if ai != bi { 73 | return ai < bi 74 | } 75 | return ar[i] < br[i] 76 | } 77 | return len(ar) < len(br) 78 | } 79 | 80 | // keyFloat returns a float value for v if it is a number/bool 81 | // and whether it is a number/bool or not. 82 | func keyFloat(v reflect.Value) (f float64, ok bool) { 83 | switch v.Kind() { 84 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 85 | return float64(v.Int()), true 86 | case reflect.Float32, reflect.Float64: 87 | return v.Float(), true 88 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 89 | return float64(v.Uint()), true 90 | case reflect.Bool: 91 | if v.Bool() { 92 | return 1, true 93 | } 94 | return 0, true 95 | } 96 | return 0, false 97 | } 98 | 99 | // numLess returns whether a < b. 100 | // a and b must necessarily have the same kind. 101 | func numLess(a, b reflect.Value) bool { 102 | switch a.Kind() { 103 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 104 | return a.Int() < b.Int() 105 | case reflect.Float32, reflect.Float64: 106 | return a.Float() < b.Float() 107 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 108 | return a.Uint() < b.Uint() 109 | case reflect.Bool: 110 | return !a.Bool() && b.Bool() 111 | } 112 | panic("not a number") 113 | } 114 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/README.md: -------------------------------------------------------------------------------- 1 | # YAML support for the Go language 2 | 3 | Introduction 4 | ------------ 5 | 6 | The yaml package enables Go programs to comfortably encode and decode YAML 7 | values. It was developed within [Canonical](https://www.canonical.com) as 8 | part of the [juju](https://juju.ubuntu.com) project, and is based on a 9 | pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML) 10 | C library to parse and generate YAML data quickly and reliably. 11 | 12 | Compatibility 13 | ------------- 14 | 15 | The yaml package supports most of YAML 1.1 and 1.2, including support for 16 | anchors, tags, map merging, etc. Multi-document unmarshalling is not yet 17 | implemented, and base-60 floats from YAML 1.1 are purposefully not 18 | supported since they're a poor design and are gone in YAML 1.2. 19 | 20 | Installation and usage 21 | ---------------------- 22 | 23 | The import path for the package is *gopkg.in/yaml.v2*. 24 | 25 | To install it, run: 26 | 27 | go get gopkg.in/yaml.v2 28 | 29 | API documentation 30 | ----------------- 31 | 32 | If opened in a browser, the import path itself leads to the API documentation: 33 | 34 | * [https://gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) 35 | 36 | API stability 37 | ------------- 38 | 39 | The package API for yaml v2 will remain stable as described in [gopkg.in](https://gopkg.in). 40 | 41 | 42 | License 43 | ------- 44 | 45 | The yaml package is licensed under the Apache License 2.0. Please see the LICENSE file for details. 46 | 47 | 48 | Example 49 | ------- 50 | 51 | ```Go 52 | package main 53 | 54 | import ( 55 | "fmt" 56 | "log" 57 | 58 | "gopkg.in/yaml.v2" 59 | ) 60 | 61 | var data = ` 62 | a: Easy! 63 | b: 64 | c: 2 65 | d: [3, 4] 66 | ` 67 | 68 | // Note: struct fields must be public in order for unmarshal to 69 | // correctly populate the data. 70 | type T struct { 71 | A string 72 | B struct { 73 | RenamedC int `yaml:"c"` 74 | D []int `yaml:",flow"` 75 | } 76 | } 77 | 78 | func main() { 79 | t := T{} 80 | 81 | err := yaml.Unmarshal([]byte(data), &t) 82 | if err != nil { 83 | log.Fatalf("error: %v", err) 84 | } 85 | fmt.Printf("--- t:\n%v\n\n", t) 86 | 87 | d, err := yaml.Marshal(&t) 88 | if err != nil { 89 | log.Fatalf("error: %v", err) 90 | } 91 | fmt.Printf("--- t dump:\n%s\n\n", string(d)) 92 | 93 | m := make(map[interface{}]interface{}) 94 | 95 | err = yaml.Unmarshal([]byte(data), &m) 96 | if err != nil { 97 | log.Fatalf("error: %v", err) 98 | } 99 | fmt.Printf("--- m:\n%v\n\n", m) 100 | 101 | d, err = yaml.Marshal(&m) 102 | if err != nil { 103 | log.Fatalf("error: %v", err) 104 | } 105 | fmt.Printf("--- m dump:\n%s\n\n", string(d)) 106 | } 107 | ``` 108 | 109 | This example will generate the following output: 110 | 111 | ``` 112 | --- t: 113 | {Easy! {2 [3 4]}} 114 | 115 | --- t dump: 116 | a: Easy! 117 | b: 118 | c: 2 119 | d: [3, 4] 120 | 121 | 122 | --- m: 123 | map[a:Easy! b:map[c:2 d:[3 4]]] 124 | 125 | --- m dump: 126 | a: Easy! 127 | b: 128 | c: 2 129 | d: 130 | - 3 131 | - 4 132 | ``` 133 | 134 | -------------------------------------------------------------------------------- /internal/async/wait_test.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 9 | ) 10 | 11 | const flakiness = 500 * time.Millisecond 12 | 13 | type value struct { 14 | doneIterations int 15 | totalDuration time.Duration 16 | } 17 | 18 | func getMock(iterations int, sleepTime time.Duration) func() (interface{}, error, bool) { 19 | cpt := iterations 20 | var startTime time.Time 21 | 22 | return func() (interface{}, error, bool) { 23 | if cpt == iterations { 24 | startTime = time.Now() 25 | } 26 | cpt -= 1 27 | 28 | // fake working time 29 | time.Sleep(sleepTime) 30 | 31 | v := &value{ 32 | doneIterations: iterations - cpt, 33 | totalDuration: time.Since(startTime), 34 | } 35 | return v, nil, cpt == 0 36 | } 37 | } 38 | 39 | func TestWaitSync(t *testing.T) { 40 | testsCases := []struct { 41 | name string 42 | config *WaitSyncConfig 43 | expValue interface{} 44 | expErr error 45 | }{ 46 | { 47 | name: "With default timeout and interval", 48 | config: &WaitSyncConfig{ 49 | Get: getMock(2, 0), 50 | }, 51 | expValue: &value{ 52 | doneIterations: 2, 53 | totalDuration: time.Second, 54 | }, 55 | }, 56 | { 57 | name: "With useless timeout", 58 | config: &WaitSyncConfig{ 59 | Get: getMock(2, time.Second), 60 | Timeout: 4 * time.Second, 61 | }, 62 | expValue: &value{ 63 | doneIterations: 2, 64 | totalDuration: 3 * time.Second, 65 | }, 66 | }, 67 | { 68 | name: "Should timeout", 69 | config: &WaitSyncConfig{ 70 | Get: getMock(2, 2*time.Second), 71 | Timeout: time.Second, 72 | }, 73 | expValue: nil, 74 | expErr: fmt.Errorf("timeout after 1s"), 75 | }, 76 | { 77 | name: "With interval", 78 | config: &WaitSyncConfig{ 79 | Get: getMock(2, 0), 80 | IntervalStrategy: LinearIntervalStrategy(2 * time.Second), 81 | }, 82 | expValue: &value{ 83 | doneIterations: 2, 84 | totalDuration: 2 * time.Second, 85 | }, 86 | }, 87 | { 88 | name: "With fibonacci interval", 89 | config: &WaitSyncConfig{ 90 | Get: getMock(5, 0), 91 | IntervalStrategy: FibonacciIntervalStrategy(time.Second, 1), 92 | }, 93 | expValue: &value{ 94 | doneIterations: 5, 95 | totalDuration: 7 * time.Second, 96 | }, 97 | }, 98 | { 99 | name: "Should timeout with interval", 100 | config: &WaitSyncConfig{ 101 | Get: getMock(2, time.Second), 102 | Timeout: 2 * time.Second, 103 | IntervalStrategy: LinearIntervalStrategy(2 * time.Second), 104 | }, 105 | expValue: nil, 106 | expErr: fmt.Errorf("timeout after 2s"), 107 | }, 108 | } 109 | for _, c := range testsCases { 110 | c := c // do not remove me 111 | t.Run(c.name, func(t *testing.T) { 112 | t.Parallel() 113 | 114 | terminalValue, err := WaitSync(c.config) 115 | 116 | testhelpers.Equals(t, c.expErr, err) 117 | 118 | if c.expValue != nil { 119 | exp := c.expValue.(*value) 120 | acc := terminalValue.(*value) 121 | testhelpers.Equals(t, exp.doneIterations, acc.doneIterations) 122 | 123 | ok := exp.totalDuration > acc.totalDuration-flakiness && exp.totalDuration < acc.totalDuration+flakiness 124 | testhelpers.Assert(t, ok, "totalDuration don't match the target: (acc: %v, exp: %v)", acc.totalDuration, exp.totalDuration) 125 | } 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@scaleway.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /scw/env_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 8 | ) 9 | 10 | // TestLoadConfig tests config getters return correct values 11 | func TestLoadEnvProfile(t *testing.T) { 12 | 13 | tests := []struct { 14 | name string 15 | env map[string]string 16 | 17 | expectedAccessKey *string 18 | expectedSecretKey *string 19 | expectedAPIURL *string 20 | expectedInsecure *bool 21 | expectedDefaultOrganizationID *string 22 | expectedDefaultRegion *string 23 | expectedDefaultZone *string 24 | }{ 25 | // up-to-date env variables 26 | { 27 | name: "No config with env variables", 28 | env: map[string]string{ 29 | scwAccessKeyEnv: v2ValidAccessKey, 30 | scwSecretKeyEnv: v2ValidSecretKey, 31 | scwAPIURLEnv: v2ValidAPIURL, 32 | scwInsecureEnv: "false", 33 | scwDefaultOrganizationIDEnv: v2ValidDefaultOrganizationID, 34 | scwDefaultRegionEnv: v2ValidDefaultRegion, 35 | scwDefaultZoneEnv: v2ValidDefaultZone, 36 | }, 37 | expectedAccessKey: s(v2ValidAccessKey), 38 | expectedSecretKey: s(v2ValidSecretKey), 39 | expectedAPIURL: s(v2ValidAPIURL), 40 | expectedInsecure: b(false), 41 | expectedDefaultOrganizationID: s(v2ValidDefaultOrganizationID), 42 | expectedDefaultRegion: s(v2ValidDefaultRegion), 43 | expectedDefaultZone: s(v2ValidDefaultZone), 44 | }, 45 | { 46 | name: "No config with terraform legacy env variables", 47 | env: map[string]string{ 48 | terraformAccessKeyEnv: v2ValidAccessKey, 49 | terraformSecretKeyEnv: v2ValidSecretKey, 50 | terraformOrganizationEnv: v2ValidDefaultOrganizationID, 51 | terraformRegionEnv: v2ValidDefaultRegion, 52 | }, 53 | expectedAccessKey: s(v2ValidAccessKey), 54 | expectedSecretKey: s(v2ValidSecretKey), 55 | expectedDefaultOrganizationID: s(v2ValidDefaultOrganizationID), 56 | expectedDefaultRegion: s(v2ValidDefaultRegion), 57 | }, 58 | { 59 | name: "No config with CLI legacy env variables", 60 | env: map[string]string{ 61 | cliSecretKeyEnv: v2ValidSecretKey2, 62 | cliOrganizationEnv: v2ValidDefaultOrganizationID2, 63 | cliRegionEnv: v2ValidDefaultRegion2, 64 | cliTLSVerifyEnv: "false", 65 | }, 66 | expectedSecretKey: s(v2ValidSecretKey2), 67 | expectedInsecure: b(true), 68 | expectedDefaultOrganizationID: s(v2ValidDefaultOrganizationID2), 69 | expectedDefaultRegion: s(v2ValidDefaultRegion2), 70 | }, 71 | } 72 | 73 | // create home dir 74 | dir := initEnv(t) 75 | 76 | // delete home dir and reset env variables 77 | defer resetEnv(t, os.Environ(), dir) 78 | for _, test := range tests { 79 | t.Run(test.name, func(t *testing.T) { 80 | // set up env and config file(s) 81 | setEnv(t, test.env, nil, dir) 82 | 83 | // remove config file(s) 84 | defer cleanEnv(t, nil, dir) 85 | 86 | // load config 87 | p := LoadEnvProfile() 88 | 89 | // assert getters 90 | testhelpers.Equals(t, test.expectedAccessKey, p.AccessKey) 91 | testhelpers.Equals(t, test.expectedSecretKey, p.SecretKey) 92 | testhelpers.Equals(t, test.expectedAPIURL, p.APIURL) 93 | testhelpers.Equals(t, test.expectedDefaultOrganizationID, p.DefaultOrganizationID) 94 | testhelpers.Equals(t, test.expectedDefaultRegion, p.DefaultRegion) 95 | testhelpers.Equals(t, test.expectedDefaultZone, p.DefaultZone) 96 | testhelpers.Equals(t, test.expectedInsecure, p.Insecure) 97 | 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /api/marketplace/v1/marketplace_utils.go: -------------------------------------------------------------------------------- 1 | package marketplace 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | ) 10 | 11 | // getLocalImage returns the correct local version of an image matching 12 | // the current zone and the compatible commercial type 13 | func (version *Version) getLocalImage(zone scw.Zone, commercialType string) (*LocalImage, error) { 14 | 15 | for _, localImage := range version.LocalImages { 16 | 17 | // Check if in correct zone 18 | if localImage.Zone != zone { 19 | continue 20 | } 21 | 22 | // Check if compatible with wanted commercial type 23 | for _, compatibleCommercialType := range localImage.CompatibleCommercialTypes { 24 | if compatibleCommercialType == commercialType { 25 | return localImage, nil 26 | } 27 | } 28 | } 29 | 30 | return nil, fmt.Errorf("couldn't find compatible local image for this image version (%s)", version.ID) 31 | 32 | } 33 | 34 | // getLatestVersion returns the current/latests version on an image, 35 | // or an error in case the image doesn't have a public version. 36 | func (image *Image) getLatestVersion() (*Version, error) { 37 | 38 | for _, version := range image.Versions { 39 | if version.ID == image.CurrentPublicVersion { 40 | return version, nil 41 | } 42 | } 43 | 44 | return nil, errors.New("latest version could not be found for image %s", image.Label) 45 | } 46 | 47 | // GetLocalImageIDByLabelRequest is used by GetLocalImageIDByLabel 48 | type GetLocalImageIDByLabelRequest struct { 49 | ImageLabel string 50 | Zone scw.Zone 51 | CommercialType string 52 | } 53 | 54 | // GetLocalImageIDByLabel search for an image with the given label (exact match) in the given region 55 | // it returns the latest version of this specific image. 56 | func (s *API) GetLocalImageIDByLabel(req *GetLocalImageIDByLabelRequest) (string, error) { 57 | 58 | if req.Zone == "" { 59 | defaultZone, _ := s.client.GetDefaultZone() 60 | req.Zone = defaultZone 61 | } 62 | 63 | listImageRequest := &ListImagesRequest{} 64 | listImageResponse, err := s.ListImages(listImageRequest, scw.WithAllPages()) 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | images := listImageResponse.Images 70 | label := strings.Replace(req.ImageLabel, "-", "_", -1) 71 | 72 | for _, image := range images { 73 | 74 | // Match label of the image 75 | if label == image.Label { 76 | 77 | latestVersion, err := image.getLatestVersion() 78 | if err != nil { 79 | return "", errors.Wrap(err, "couldn't find a matching image for the given label (%s), zone (%s) and commercial type (%s)", req.ImageLabel, req.Zone, req.CommercialType) 80 | } 81 | 82 | localImage, err := latestVersion.getLocalImage(req.Zone, req.CommercialType) 83 | if err != nil { 84 | return "", errors.Wrap(err, "couldn't find a matching image for the given label (%s), zone (%s) and commercial type (%s)", req.ImageLabel, req.Zone, req.CommercialType) 85 | } 86 | 87 | return localImage.ID, nil 88 | } 89 | 90 | } 91 | 92 | return "", errors.New("couldn't find a matching image for the given label (%s), zone (%s) and commercial type (%s)", req.ImageLabel, req.Zone, req.CommercialType) 93 | } 94 | 95 | // UnsafeSetTotalCount should not be used 96 | // Internal usage only 97 | func (r *ListImagesResponse) UnsafeSetTotalCount(totalCount int) { 98 | r.TotalCount = uint32(totalCount) 99 | } 100 | 101 | // UnsafeSetTotalCount should not be used 102 | // Internal usage only 103 | func (r *ListVersionsResponse) UnsafeSetTotalCount(totalCount int) { 104 | r.TotalCount = uint32(totalCount) 105 | } 106 | -------------------------------------------------------------------------------- /api/instance/v1/volume_utils.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 9 | "github.com/scaleway/scaleway-sdk-go/scw" 10 | ) 11 | 12 | // UpdateVolumeRequest contains the parameters to update on a volume 13 | type UpdateVolumeRequest struct { 14 | Zone scw.Zone `json:"-"` 15 | // VolumeID is the volumes unique ID 16 | VolumeID string `json:"-"` 17 | // Name display the volumes names 18 | Name *string `json:"name,omitempty"` 19 | } 20 | 21 | // UpdateVolumeResponse contains the updated volume. 22 | type UpdateVolumeResponse struct { 23 | Volume *Volume `json:"volume,omitempty"` 24 | } 25 | 26 | // setVolumeRequest contains all the params to PUT volumes 27 | type setVolumeRequest struct { 28 | Zone scw.Zone `json:"-"` 29 | // ID display the volumes unique ID 30 | ID string `json:"id"` 31 | // Name display the volumes names 32 | Name string `json:"name"` 33 | // ExportURI show the volumes NBD export URI 34 | ExportURI string `json:"export_uri"` 35 | // Size display the volumes disk size 36 | Size scw.Size `json:"size"` 37 | // VolumeType display the volumes type 38 | // 39 | // Default value: l_ssd 40 | VolumeType VolumeType `json:"volume_type"` 41 | // CreationDate display the volumes creation date 42 | CreationDate time.Time `json:"creation_date"` 43 | // ModificationDate display the volumes modification date 44 | ModificationDate time.Time `json:"modification_date"` 45 | // Organization display the volumes organization 46 | Organization string `json:"organization"` 47 | // Server display information about the server attached to the volume 48 | Server *ServerSummary `json:"server"` 49 | } 50 | 51 | // UpdateVolume updates the set fields on the volume. 52 | func (s *API) UpdateVolume(req *UpdateVolumeRequest, opts ...scw.RequestOption) (*UpdateVolumeResponse, error) { 53 | var err error 54 | 55 | if req.Zone == "" { 56 | defaultZone, _ := s.client.GetDefaultZone() 57 | req.Zone = defaultZone 58 | } 59 | 60 | if fmt.Sprint(req.Zone) == "" { 61 | return nil, errors.New("field Zone cannot be empty in request") 62 | } 63 | 64 | if fmt.Sprint(req.VolumeID) == "" { 65 | return nil, errors.New("field VolumeID cannot be empty in request") 66 | } 67 | 68 | getVolumeResponse, err := s.GetVolume(&GetVolumeRequest{ 69 | Zone: req.Zone, 70 | VolumeID: req.VolumeID, 71 | }) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | setVolumeRequest := &setVolumeRequest{ 77 | Zone: req.Zone, 78 | ID: getVolumeResponse.Volume.ID, 79 | Name: getVolumeResponse.Volume.Name, 80 | ExportURI: getVolumeResponse.Volume.ExportURI, 81 | Size: getVolumeResponse.Volume.Size, 82 | VolumeType: getVolumeResponse.Volume.VolumeType, 83 | CreationDate: getVolumeResponse.Volume.CreationDate, 84 | ModificationDate: getVolumeResponse.Volume.ModificationDate, 85 | Organization: getVolumeResponse.Volume.Organization, 86 | Server: getVolumeResponse.Volume.Server, 87 | } 88 | 89 | // Override the values that need to be updated 90 | if req.Name != nil { 91 | setVolumeRequest.Name = *req.Name 92 | } 93 | 94 | scwReq := &scw.ScalewayRequest{ 95 | Method: "PUT", 96 | Path: "/instance/v1/zones/" + fmt.Sprint(req.Zone) + "/volumes/" + fmt.Sprint(req.VolumeID) + "", 97 | Headers: http.Header{}, 98 | } 99 | 100 | err = scwReq.SetBody(setVolumeRequest) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | var res UpdateVolumeResponse 106 | 107 | err = s.client.Do(scwReq, &res, opts...) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | return &res, nil 113 | } 114 | -------------------------------------------------------------------------------- /logger/default_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "strconv" 10 | ) 11 | 12 | var DefaultLogger = newLogger(os.Stderr, LogLevelWarning) 13 | var logger Logger = DefaultLogger 14 | 15 | // loggerT is the default logger used by scaleway-sdk-go. 16 | type loggerT struct { 17 | m [4]*log.Logger 18 | v LogLevel 19 | } 20 | 21 | // Init create a new default logger. 22 | // Not mutex-protected, should be called before any scaleway-sdk-go functions. 23 | func (g *loggerT) Init(w io.Writer, level LogLevel) { 24 | g.m = newLogger(w, level).m 25 | } 26 | 27 | // Debugf logs to the DEBUG log. Arguments are handled in the manner of fmt.Printf. 28 | func Debugf(format string, args ...interface{}) { logger.Debugf(format, args...) } 29 | func (g *loggerT) Debugf(format string, args ...interface{}) { 30 | g.m[LogLevelDebug].Printf(format, args...) 31 | } 32 | 33 | // Infof logs to the INFO log. Arguments are handled in the manner of fmt.Printf. 34 | func Infof(format string, args ...interface{}) { logger.Infof(format, args...) } 35 | func (g *loggerT) Infof(format string, args ...interface{}) { 36 | g.m[LogLevelInfo].Printf(format, args...) 37 | } 38 | 39 | // Warningf logs to the WARNING log. Arguments are handled in the manner of fmt.Printf. 40 | func Warningf(format string, args ...interface{}) { logger.Warningf(format, args...) } 41 | func (g *loggerT) Warningf(format string, args ...interface{}) { 42 | g.m[LogLevelWarning].Printf(format, args...) 43 | } 44 | 45 | // Errorf logs to the ERROR log. Arguments are handled in the manner of fmt.Printf. 46 | func Errorf(format string, args ...interface{}) { logger.Errorf(format, args...) } 47 | func (g *loggerT) Errorf(format string, args ...interface{}) { 48 | g.m[LogLevelError].Printf(format, args...) 49 | } 50 | 51 | // ShouldLog reports whether verbosity level l is at least the requested verbose level. 52 | func ShouldLog(level LogLevel) bool { return logger.ShouldLog(level) } 53 | func (g *loggerT) ShouldLog(level LogLevel) bool { 54 | return level <= g.v 55 | } 56 | 57 | func isEnabled(envKey string) bool { 58 | env, exist := os.LookupEnv(envKey) 59 | if !exist { 60 | return false 61 | } 62 | 63 | value, err := strconv.ParseBool(env) 64 | if err != nil { 65 | fmt.Fprintf(os.Stderr, "ERROR: environment variable %s has invalid boolean value\n", envKey) 66 | } 67 | 68 | return value 69 | } 70 | 71 | // newLogger creates a logger to be used as default logger. 72 | // All logs are written to w. 73 | func newLogger(w io.Writer, level LogLevel) *loggerT { 74 | errorW := ioutil.Discard 75 | warningW := ioutil.Discard 76 | infoW := ioutil.Discard 77 | debugW := ioutil.Discard 78 | if isEnabled("SCW_DEBUG") { 79 | level = LogLevelDebug 80 | } 81 | switch level { 82 | case LogLevelDebug: 83 | debugW = w 84 | case LogLevelInfo: 85 | infoW = w 86 | case LogLevelWarning: 87 | warningW = w 88 | case LogLevelError: 89 | errorW = w 90 | } 91 | 92 | // Error logs will be written to errorW, warningW, infoW and debugW. 93 | // Warning logs will be written to warningW, infoW and debugW. 94 | // Info logs will be written to infoW and debugW. 95 | // Debug logs will be written to debugW. 96 | var m [4]*log.Logger 97 | m[LogLevelError] = log.New(io.MultiWriter(debugW, infoW, warningW, errorW), severityName[LogLevelError]+": ", log.LstdFlags) 98 | m[LogLevelWarning] = log.New(io.MultiWriter(debugW, infoW, warningW), severityName[LogLevelWarning]+": ", log.LstdFlags) 99 | m[LogLevelInfo] = log.New(io.MultiWriter(debugW, infoW), severityName[LogLevelInfo]+": ", log.LstdFlags) 100 | m[LogLevelDebug] = log.New(debugW, severityName[LogLevelDebug]+": ", log.LstdFlags) 101 | return &loggerT{m: m, v: level} 102 | } 103 | -------------------------------------------------------------------------------- /scw/request_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | "time" 10 | 11 | "github.com/scaleway/scaleway-sdk-go/internal/auth" 12 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 13 | ) 14 | 15 | const ( 16 | testBaseURL = "http://example.com" 17 | testPath = "/some/path/" 18 | testKey = "some_key" 19 | testValue = "some_value" 20 | testBody = "some body" 21 | testUserAgent = "user/agent" 22 | 23 | testHeaderKey = "Some-Header-Key" 24 | testHeaderVal = "some_header_val" 25 | testTokenKey = "some_secret_key" 26 | ) 27 | 28 | func TestGetURL(t *testing.T) { 29 | 30 | req := ScalewayRequest{ 31 | Path: testPath, 32 | Query: url.Values{ 33 | testKey: []string{testValue}, 34 | }, 35 | } 36 | 37 | newURL, err := req.getURL(testBaseURL) 38 | testhelpers.AssertNoError(t, err) 39 | 40 | expectedURL := fmt.Sprintf("%s%s?%s=%s", testBaseURL, testPath, testKey, testValue) 41 | 42 | testhelpers.Equals(t, expectedURL, newURL.String()) 43 | 44 | } 45 | 46 | func TestGetHeadersWithoutBody(t *testing.T) { 47 | 48 | req := ScalewayRequest{ 49 | Headers: http.Header{ 50 | testHeaderKey: []string{testHeaderVal}, 51 | }, 52 | } 53 | token := auth.NewToken(testAccessKey, testTokenKey) 54 | 55 | expectedHeaders := http.Header{ 56 | testHeaderKey: []string{testHeaderVal}, 57 | "X-Auth-Token": []string{testTokenKey}, 58 | "User-Agent": []string{testUserAgent}, 59 | } 60 | 61 | allHeaders := req.getAllHeaders(token, testUserAgent, false) 62 | 63 | testhelpers.Equals(t, expectedHeaders, allHeaders) 64 | 65 | } 66 | 67 | func TestGetHeadersWithBody(t *testing.T) { 68 | req := ScalewayRequest{ 69 | Headers: http.Header{ 70 | testHeaderKey: []string{testHeaderVal}, 71 | }, 72 | Body: bytes.NewReader([]byte(testBody)), 73 | } 74 | token := auth.NewToken(testSecretKey, testTokenKey) 75 | 76 | expectedHeaders := http.Header{ 77 | testHeaderKey: []string{testHeaderVal}, 78 | "X-Auth-Token": []string{testTokenKey}, 79 | "Content-Type": []string{"application/json"}, 80 | "User-Agent": []string{testUserAgent}, 81 | } 82 | 83 | allHeaders := req.getAllHeaders(token, testUserAgent, false) 84 | 85 | testhelpers.Equals(t, expectedHeaders, allHeaders) 86 | } 87 | 88 | func TestSetBody(t *testing.T) { 89 | 90 | body := struct { 91 | Region Region `json:"-"` 92 | Id string `json:"-"` 93 | Name string `json:"name,omitempty"` 94 | Slice []string `json:"slice,omitempty"` 95 | Flag bool `json:"flag,omitempty"` 96 | Timeout *time.Duration `json:"timeout,omitempty"` 97 | }{ 98 | Region: RegionNlAms, 99 | Id: "plop", 100 | Name: "plop", 101 | Slice: []string{"plop", "plop"}, 102 | Flag: true, 103 | Timeout: DurationPtr(time.Second), 104 | } 105 | 106 | req := ScalewayRequest{ 107 | Headers: http.Header{}, 108 | } 109 | 110 | testhelpers.AssertNoError(t, req.SetBody(body)) 111 | 112 | r, isBytesReader := req.Body.(*bytes.Reader) 113 | 114 | testhelpers.Assert(t, isBytesReader, "req.Body should be bytes Reader") 115 | 116 | b := make([]byte, r.Len()) 117 | _, err := r.Read(b) 118 | testhelpers.AssertNoError(t, err) 119 | 120 | testhelpers.Equals(t, []string{"application/json"}, req.Headers["Content-Type"]) 121 | testhelpers.Equals(t, `{"name":"plop","slice":["plop","plop"],"flag":true,"timeout":1000000000}`, string(b)) 122 | 123 | } 124 | 125 | func TestSetFileBody(t *testing.T) { 126 | 127 | body := &File{ 128 | Content: bytes.NewReader([]byte(testBody)), 129 | ContentType: "plain/text", 130 | } 131 | 132 | req := ScalewayRequest{ 133 | Headers: http.Header{}, 134 | } 135 | 136 | testhelpers.AssertNoError(t, req.SetBody(body)) 137 | 138 | r, isBytesReader := req.Body.(*bytes.Reader) 139 | 140 | testhelpers.Assert(t, isBytesReader, "req.Body should be bytes Reader") 141 | 142 | b := make([]byte, r.Len()) 143 | _, err := r.Read(b) 144 | testhelpers.AssertNoError(t, err) 145 | 146 | testhelpers.Equals(t, []string{"plain/text"}, req.Headers["Content-Type"]) 147 | testhelpers.Equals(t, `some body`, string(b)) 148 | 149 | } 150 | -------------------------------------------------------------------------------- /scw/locality.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/logger" 7 | ) 8 | 9 | // Zone is an availability zone 10 | type Zone string 11 | 12 | const ( 13 | // ZoneFrPar1 represents the fr-par-1 zone 14 | ZoneFrPar1 = Zone("fr-par-1") 15 | // ZoneFrPar2 represents the fr-par-2 zone 16 | ZoneFrPar2 = Zone("fr-par-2") 17 | // ZoneNlAms1 represents the nl-ams-1 zone 18 | ZoneNlAms1 = Zone("nl-ams-1") 19 | ) 20 | 21 | var ( 22 | // AllZones is an array that list all zones 23 | AllZones = []Zone{ 24 | ZoneFrPar1, 25 | ZoneFrPar2, 26 | ZoneNlAms1, 27 | } 28 | ) 29 | 30 | // Exists checks whether a zone exists 31 | func (zone *Zone) Exists() bool { 32 | for _, z := range AllZones { 33 | if z == *zone { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | // Region is a geographical location 41 | type Region string 42 | 43 | const ( 44 | // RegionFrPar represents the fr-par region 45 | RegionFrPar = Region("fr-par") 46 | // RegionNlAms represents the nl-ams region 47 | RegionNlAms = Region("nl-ams") 48 | ) 49 | 50 | var ( 51 | // AllRegions is an array that list all regions 52 | AllRegions = []Region{ 53 | RegionFrPar, 54 | RegionNlAms, 55 | } 56 | ) 57 | 58 | // Exists checks whether a region exists 59 | func (region *Region) Exists() bool { 60 | for _, r := range AllRegions { 61 | if r == *region { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | // GetZones is a function that returns the zones for the specified region 69 | func (region Region) GetZones() []Zone { 70 | switch region { 71 | case RegionFrPar: 72 | return []Zone{ZoneFrPar1, ZoneFrPar2} 73 | case RegionNlAms: 74 | return []Zone{ZoneNlAms1} 75 | default: 76 | return []Zone{} 77 | } 78 | } 79 | 80 | // ParseZone parse a string value into a Zone object 81 | func ParseZone(zone string) (Zone, error) { 82 | switch zone { 83 | case "par1": 84 | // would be triggered by API market place 85 | // logger.Warningf("par1 is a deprecated name for zone, use fr-par-1 instead") 86 | return ZoneFrPar1, nil 87 | case "ams1": 88 | // would be triggered by API market place 89 | // logger.Warningf("ams1 is a deprecated name for zone, use nl-ams-1 instead") 90 | return ZoneNlAms1, nil 91 | default: 92 | newZone := Zone(zone) 93 | if !newZone.Exists() { 94 | logger.Warningf("%s is an unknown zone", newZone) 95 | } 96 | return newZone, nil 97 | } 98 | } 99 | 100 | // UnmarshalJSON implements the Unmarshaler interface for a Zone. 101 | // this to call ParseZone on the string input and return the correct Zone object. 102 | func (zone *Zone) UnmarshalJSON(input []byte) error { 103 | 104 | // parse input value as string 105 | var stringValue string 106 | err := json.Unmarshal(input, &stringValue) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | // parse string as Zone 112 | *zone, err = ParseZone(stringValue) 113 | if err != nil { 114 | return err 115 | } 116 | return nil 117 | } 118 | 119 | // ParseRegion parse a string value into a Zone object 120 | func ParseRegion(region string) (Region, error) { 121 | switch region { 122 | case "par1": 123 | // would be triggered by API market place 124 | // logger.Warningf("par1 is a deprecated name for region, use fr-par instead") 125 | return RegionFrPar, nil 126 | case "ams1": 127 | // would be triggered by API market place 128 | // logger.Warningf("ams1 is a deprecated name for region, use nl-ams instead") 129 | return RegionNlAms, nil 130 | default: 131 | newRegion := Region(region) 132 | if !newRegion.Exists() { 133 | logger.Warningf("%s is an unknown region", newRegion) 134 | } 135 | return newRegion, nil 136 | } 137 | } 138 | 139 | // UnmarshalJSON implements the Unmarshaler interface for a Region. 140 | // this to call ParseRegion on the string input and return the correct Region object. 141 | func (region *Region) UnmarshalJSON(input []byte) error { 142 | // parse input value as string 143 | var stringValue string 144 | err := json.Unmarshal(input, &stringValue) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | // parse string as Region 150 | *region, err = ParseRegion(stringValue) 151 | if err != nil { 152 | return err 153 | } 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /scw/README.md: -------------------------------------------------------------------------------- 1 | # Scaleway config 2 | 3 | ## TL;DR 4 | 5 | Recommended config file: 6 | 7 | ```yaml 8 | # get your credentials on https://console.scaleway.com/account/credentials 9 | access_key: SCWXXXXXXXXXXXXXXXXX 10 | secret_key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 11 | default_organization_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 12 | default_region: fr-par 13 | default_zone: fr-par-1 14 | ``` 15 | 16 | ## Config file path 17 | 18 | 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: 19 | 20 | 1. Custom directory: `$SCW_CONFIG_PATH` 21 | 2. [XDG base directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html): `$XDG_CONFIG_HOME/scw/config.yaml` 22 | 3. Unix home directory: `$HOME/.config/scw/config.yaml` 23 | 3. Windows home directory: `%USERPROFILE%/.config/scw/config.yaml` 24 | 25 | ## V1 config (DEPRECATED) 26 | 27 | The V1 config (AKA legacy config) `.scwrc` is deprecated. 28 | 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). 29 | 30 | ## Reading config order 31 | 32 | [ClientOption](https://godoc.org/github.com/scaleway/scaleway-sdk-go/scw#ClientOption) ordering will decide the order in which the config should apply: 33 | 34 | ```go 35 | p, _ := scw.MustLoadConfig().GetActiveProfile() 36 | 37 | scw.NewClient( 38 | scw.WithProfile(p), // active profile applies first 39 | scw.WithEnv(), // existing env variables may overwrite active profile 40 | scw.WithDefaultRegion(scw.RegionFrPar) // any prior region set will be discarded to usr the new one 41 | ) 42 | ``` 43 | 44 | ## Environment variables 45 | 46 | | Variable | Description | Legacy variables | 47 | | :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ | 48 | | `$SCW_ACCESS_KEY` | Access key of a token ([get yours](https://console.scaleway.com/account/credentials)) | `$SCALEWAY_ACCESS_KEY` (used by terraform) | 49 | | `$SCW_SECRET_KEY` | Secret key of a token ([get yours](https://console.scaleway.com/account/credentials)) | `$SCW_TOKEN` (used by cli), `$SCALEWAY_TOKEN` (used by terraform), `$SCALEWAY_ACCESS_KEY` (used by terraform) | 50 | | `$SCW_DEFAULT_ORGANIZATION_ID` | Your default organization ID, if you don't have one use your organization ID ([get yours](https://console.scaleway.com/account/credentials)) | `$SCW_ORGANIZATION` (used by cli),`$SCALEWAY_ORGANIZATION` (used by terraform) | 51 | | `$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) | 52 | | `$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) | 53 | | `$SCW_API_URL` | Url of the API | - | 54 | | `$SCW_INSECURE` | Set this to `true` to enable the insecure mode | `$SCW_TLSVERIFY` (inverse flag used by the cli) | 55 | -------------------------------------------------------------------------------- /scw/env.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/logger" 8 | ) 9 | 10 | // Environment variables 11 | const ( 12 | // Up-to-date 13 | scwConfigPathEnv = "SCW_CONFIG_PATH" 14 | scwAccessKeyEnv = "SCW_ACCESS_KEY" 15 | scwSecretKeyEnv = "SCW_SECRET_KEY" 16 | scwActiveProfileEnv = "SCW_PROFILE" 17 | scwAPIURLEnv = "SCW_API_URL" 18 | scwInsecureEnv = "SCW_INSECURE" 19 | scwDefaultOrganizationIDEnv = "SCW_DEFAULT_ORGANIZATION_ID" 20 | scwDefaultRegionEnv = "SCW_DEFAULT_REGION" 21 | scwDefaultZoneEnv = "SCW_DEFAULT_ZONE" 22 | 23 | // All deprecated (cli&terraform) 24 | terraformAccessKeyEnv = "SCALEWAY_ACCESS_KEY" // used both as access key and secret key 25 | terraformSecretKeyEnv = "SCALEWAY_TOKEN" 26 | terraformOrganizationEnv = "SCALEWAY_ORGANIZATION" 27 | terraformRegionEnv = "SCALEWAY_REGION" 28 | cliTLSVerifyEnv = "SCW_TLSVERIFY" 29 | cliOrganizationEnv = "SCW_ORGANIZATION" 30 | cliRegionEnv = "SCW_REGION" 31 | cliSecretKeyEnv = "SCW_TOKEN" 32 | 33 | // TBD 34 | //cliVerboseEnv = "SCW_VERBOSE_API" 35 | //cliDebugEnv = "DEBUG" 36 | //cliNoCheckVersionEnv = "SCW_NOCHECKVERSION" 37 | //cliTestWithRealAPIEnv = "TEST_WITH_REAL_API" 38 | //cliSecureExecEnv = "SCW_SECURE_EXEC" 39 | //cliGatewayEnv = "SCW_GATEWAY" 40 | //cliSensitiveEnv = "SCW_SENSITIVE" 41 | //cliAccountAPIEnv = "SCW_ACCOUNT_API" 42 | //cliMetadataAPIEnv = "SCW_METADATA_API" 43 | //cliMarketPlaceAPIEnv = "SCW_MARKETPLACE_API" 44 | //cliComputePar1APIEnv = "SCW_COMPUTE_PAR1_API" 45 | //cliComputeAms1APIEnv = "SCW_COMPUTE_AMS1_API" 46 | //cliCommercialTypeEnv = "SCW_COMMERCIAL_TYPE" 47 | //cliTargetArchEnv = "SCW_TARGET_ARCH" 48 | ) 49 | 50 | const ( 51 | v1RegionFrPar = "par1" 52 | v1RegionNlAms = "ams1" 53 | ) 54 | 55 | func LoadEnvProfile() *Profile { 56 | p := &Profile{} 57 | 58 | accessKey, _, envExist := getEnv(scwAccessKeyEnv, terraformAccessKeyEnv) 59 | if envExist { 60 | p.AccessKey = &accessKey 61 | } 62 | 63 | secretKey, _, envExist := getEnv(scwSecretKeyEnv, cliSecretKeyEnv, terraformSecretKeyEnv, terraformAccessKeyEnv) 64 | if envExist { 65 | p.SecretKey = &secretKey 66 | } 67 | 68 | apiURL, _, envExist := getEnv(scwAPIURLEnv) 69 | if envExist { 70 | p.APIURL = &apiURL 71 | } 72 | 73 | insecureValue, envKey, envExist := getEnv(scwInsecureEnv, cliTLSVerifyEnv) 74 | if envExist { 75 | insecure, err := strconv.ParseBool(insecureValue) 76 | if err != nil { 77 | logger.Warningf("env variable %s cannot be parsed: %s is invalid boolean", envKey, insecureValue) 78 | } 79 | 80 | if envKey == cliTLSVerifyEnv { 81 | insecure = !insecure // TLSVerify is the inverse of Insecure 82 | } 83 | 84 | p.Insecure = &insecure 85 | } 86 | 87 | organizationID, _, envExist := getEnv(scwDefaultOrganizationIDEnv, cliOrganizationEnv, terraformOrganizationEnv) 88 | if envExist { 89 | p.DefaultOrganizationID = &organizationID 90 | } 91 | 92 | region, _, envExist := getEnv(scwDefaultRegionEnv, cliRegionEnv, terraformRegionEnv) 93 | if envExist { 94 | region = v1RegionToV2(region) 95 | p.DefaultRegion = ®ion 96 | } 97 | 98 | zone, _, envExist := getEnv(scwDefaultZoneEnv) 99 | if envExist { 100 | p.DefaultZone = &zone 101 | } 102 | 103 | return p 104 | } 105 | 106 | func getEnv(upToDateKey string, deprecatedKeys ...string) (string, string, bool) { 107 | value, exist := os.LookupEnv(upToDateKey) 108 | if exist { 109 | logger.Infof("reading value from %s", upToDateKey) 110 | return value, upToDateKey, true 111 | } 112 | 113 | for _, key := range deprecatedKeys { 114 | value, exist := os.LookupEnv(key) 115 | if exist { 116 | logger.Infof("reading value from %s", key) 117 | logger.Warningf("%s is deprecated, please use %s instead", key, upToDateKey) 118 | return value, key, true 119 | } 120 | } 121 | 122 | return "", "", false 123 | } 124 | 125 | func v1RegionToV2(region string) string { 126 | switch region { 127 | case v1RegionFrPar: 128 | logger.Warningf("par1 is a deprecated name for region, use fr-par instead") 129 | return "fr-par" 130 | case v1RegionNlAms: 131 | logger.Warningf("ams1 is a deprecated name for region, use nl-ams instead") 132 | return "nl-ams" 133 | default: 134 | return region 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to `scaleway-sdk-go` 2 | 3 | `scaleway-sdk-go` is Apache 2.0 licensed and accepts contributions via GitHub. 4 | This document will cover how to contribute to the project and report issues. 5 | 6 |

7 | 8 |

9 | 10 | ## Topics 11 | 12 | - [Reporting Security Issues](#reporting-security-issues) 13 | - [Reporting Issues](#reporting-issues) 14 | - [Suggesting feature](#suggesting-feature) 15 | - [Contributing Code](#contributing-code) 16 | - [Community Guidelines](#community-guidelines) 17 | 18 | ## Reporting security issues 19 | 20 | At Scaleway we take security seriously. 21 | If you have any issue regarding security, please notify us by sending an email to [security@scaleway.com](mailto:security@scaleway.com). 22 | 23 | Please _DO NOT_ create a GitHub issue. 24 | 25 | We will follow up with you promptly with more information and a plan for remediation. 26 | We currently do not offer a paid security bounty program, but we would love to send some Scaleway swag your way along with our deepest gratitude for your assistance in making Scaleway a more secure Cloud ecosystem. 27 | 28 | ## Reporting issues 29 | 30 | A great way to contribute to the project is to send a detailed report when you encounter a bug. 31 | We always appreciate a well-written, thorough bug report, and will thank you for it! 32 | Before opening a new issue, we appreciate you reviewing open issues to see if there are any similar requests. 33 | If there is a match, thumbs up the issue with a 👍 and leave a comment if you have additional information. 34 | 35 | When reporting an issue, include the following: 36 | 37 | - The version of `scaleway-sdk-go` you are using (v2.0.0-beta1, v2.0.0, master,...) 38 | - Go version 39 | - GOOS 40 | - GOARCH 41 | 42 | ## Suggesting a feature 43 | 44 | When requesting a feature, some of the questions we want to answer are: 45 | 46 | - What value does this feature bring to end users? 47 | - How urgent is the need (nice to have feature or need to have)? 48 | - Does this align with the goals of `scaleway-sdk-go`? 49 | 50 | ## Contributing code 51 | 52 | Before contributing to the code, make sure you have read about the [continuous code deployment](docs/CONTINUOUS_CODE_DEPLOYMENT.md) process we are using on this repo. 53 | 54 | ### Submit code 55 | 56 | To submit code: 57 | 58 | - Create a fork of the project 59 | - Create a topic branch from where you want to base your work (usually master) 60 | - Add tests to cover contributed code 61 | - Push your commit(s) to your topic branch on your fork 62 | - Open a pull request against `scaleway-sdk-go` master branch that follows [PR guidelines](#pull-request-guidelines) 63 | 64 | The maintainers of `scaleway-sdk-go` use a "Let's Get This Merged" (LGTM) message in the pull request to note that the commits are ready to merge. 65 | After one or more maintainer states LGTM, we will merge. 66 | If you have questions or comments on your code, feel free to correct these in your branch through new commits. 67 | 68 | ### Pull Request Guidelines 69 | 70 | The goal of the following guidelines is to have Pull Requests (PRs) that are fairly easy to review and comprehend, and code that is easy to maintain in the future. 71 | 72 | - **Pull Request title should be clear** on what is being fixed or added to the code base. 73 | If you are addressing an open issue, please start the title with "fix: #XXX" or "feature: #XXX" 74 | - **Keep it readable for human reviewers** and prefer a subset of functionality (code) with tests and documentation over delivering them separately 75 | - **Don't forget commenting code** to help reviewers understand and to keep [our Go Report Card](https://goreportcard.com/report/github.com/scaleway/scaleway-sdk-go) at A+ 76 | - **Notify Work In Progress PRs** by prefixing the title with `[WIP]` 77 | - **Please, keep us updated.** 78 | We will try our best to merge your PR, but please notice that PRs may be closed after 30 days of inactivity. 79 | 80 | Your pull request should be rebased against the current master branch. Please do not merge 81 | the current master branch in with your topic branch, nor use the Update Branch button provided 82 | by GitHub on the pull request page. 83 | 84 | Keep in mind only the **Pull Request Title** will be used as commit message as we stash all commits on merge. 85 | 86 | ## Community guidelines 87 | 88 | See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). 89 | 90 | Thank you for reading through all of this, if you have any question feel free to [reach us](README.md#reach-us)! 91 | -------------------------------------------------------------------------------- /scw/convert.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import "time" 4 | 5 | // StringPtr returns a pointer to the string value passed in. 6 | func StringPtr(v string) *string { 7 | return &v 8 | } 9 | 10 | // StringSlicePtr converts a slice of string values into a slice of 11 | // string pointers 12 | func StringSlicePtr(src []string) []*string { 13 | dst := make([]*string, len(src)) 14 | for i := 0; i < len(src); i++ { 15 | dst[i] = &(src[i]) 16 | } 17 | return dst 18 | } 19 | 20 | // StringsPtr returns a pointer to the []string value passed in. 21 | func StringsPtr(v []string) *[]string { 22 | return &v 23 | } 24 | 25 | // StringsSlicePtr converts a slice of []string values into a slice of 26 | // []string pointers 27 | func StringsSlicePtr(src [][]string) []*[]string { 28 | dst := make([]*[]string, len(src)) 29 | for i := 0; i < len(src); i++ { 30 | dst[i] = &(src[i]) 31 | } 32 | return dst 33 | } 34 | 35 | // BytesPtr returns a pointer to the []byte value passed in. 36 | func BytesPtr(v []byte) *[]byte { 37 | return &v 38 | } 39 | 40 | // BytesSlicePtr converts a slice of []byte values into a slice of 41 | // []byte pointers 42 | func BytesSlicePtr(src [][]byte) []*[]byte { 43 | dst := make([]*[]byte, len(src)) 44 | for i := 0; i < len(src); i++ { 45 | dst[i] = &(src[i]) 46 | } 47 | return dst 48 | } 49 | 50 | // BoolPtr returns a pointer to the bool value passed in. 51 | func BoolPtr(v bool) *bool { 52 | return &v 53 | } 54 | 55 | // BoolSlicePtr converts a slice of bool values into a slice of 56 | // bool pointers 57 | func BoolSlicePtr(src []bool) []*bool { 58 | dst := make([]*bool, len(src)) 59 | for i := 0; i < len(src); i++ { 60 | dst[i] = &(src[i]) 61 | } 62 | return dst 63 | } 64 | 65 | // Int32Ptr returns a pointer to the int32 value passed in. 66 | func Int32Ptr(v int32) *int32 { 67 | return &v 68 | } 69 | 70 | // Int32SlicePtr converts a slice of int32 values into a slice of 71 | // int32 pointers 72 | func Int32SlicePtr(src []int32) []*int32 { 73 | dst := make([]*int32, len(src)) 74 | for i := 0; i < len(src); i++ { 75 | dst[i] = &(src[i]) 76 | } 77 | return dst 78 | } 79 | 80 | // Int64Ptr returns a pointer to the int64 value passed in. 81 | func Int64Ptr(v int64) *int64 { 82 | return &v 83 | } 84 | 85 | // Int64SlicePtr converts a slice of int64 values into a slice of 86 | // int64 pointers 87 | func Int64SlicePtr(src []int64) []*int64 { 88 | dst := make([]*int64, len(src)) 89 | for i := 0; i < len(src); i++ { 90 | dst[i] = &(src[i]) 91 | } 92 | return dst 93 | } 94 | 95 | // Uint32Ptr returns a pointer to the uint32 value passed in. 96 | func Uint32Ptr(v uint32) *uint32 { 97 | return &v 98 | } 99 | 100 | // Uint32SlicePtr converts a slice of uint32 values into a slice of 101 | // uint32 pointers 102 | func Uint32SlicePtr(src []uint32) []*uint32 { 103 | dst := make([]*uint32, len(src)) 104 | for i := 0; i < len(src); i++ { 105 | dst[i] = &(src[i]) 106 | } 107 | return dst 108 | } 109 | 110 | // Uint64Ptr returns a pointer to the uint64 value passed in. 111 | func Uint64Ptr(v uint64) *uint64 { 112 | return &v 113 | } 114 | 115 | // Uint64SlicePtr converts a slice of uint64 values into a slice of 116 | // uint64 pointers 117 | func Uint64SlicePtr(src []uint64) []*uint64 { 118 | dst := make([]*uint64, len(src)) 119 | for i := 0; i < len(src); i++ { 120 | dst[i] = &(src[i]) 121 | } 122 | return dst 123 | } 124 | 125 | // Float32Ptr returns a pointer to the float32 value passed in. 126 | func Float32Ptr(v float32) *float32 { 127 | return &v 128 | } 129 | 130 | // Float32SlicePtr converts a slice of float32 values into a slice of 131 | // float32 pointers 132 | func Float32SlicePtr(src []float32) []*float32 { 133 | dst := make([]*float32, len(src)) 134 | for i := 0; i < len(src); i++ { 135 | dst[i] = &(src[i]) 136 | } 137 | return dst 138 | } 139 | 140 | // Float64Ptr returns a pointer to the float64 value passed in. 141 | func Float64Ptr(v float64) *float64 { 142 | return &v 143 | } 144 | 145 | // Float64SlicePtr converts a slice of float64 values into a slice of 146 | // float64 pointers 147 | func Float64SlicePtr(src []float64) []*float64 { 148 | dst := make([]*float64, len(src)) 149 | for i := 0; i < len(src); i++ { 150 | dst[i] = &(src[i]) 151 | } 152 | return dst 153 | } 154 | 155 | // DurationPtr returns a pointer to the Duration value passed in. 156 | func DurationPtr(v time.Duration) *time.Duration { 157 | return &v 158 | } 159 | 160 | // SizePtr returns a pointer to the Size value passed in. 161 | func SizePtr(v Size) *Size { 162 | return &v 163 | } 164 | -------------------------------------------------------------------------------- /scw/custom_types.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 10 | ) 11 | 12 | // ServiceInfo contains API metadata 13 | // These metadata are only here for debugging. Do not rely on these values 14 | type ServiceInfo struct { 15 | // Name is the name of the API 16 | Name string `json:"name"` 17 | 18 | // Description is a human readable description for the API 19 | Description string `json:"description"` 20 | 21 | // Version is the version of the API 22 | Version string `json:"version"` 23 | 24 | // DocumentationUrl is the a web url where the documentation of the API can be found 25 | DocumentationUrl *string `json:"documentation_url"` 26 | } 27 | 28 | // File is the structure used to receive / send a file from / to the API 29 | type File struct { 30 | // Name of the file 31 | Name string `json:"name"` 32 | 33 | // ContentType used in the HTTP header `Content-Type` 34 | ContentType string `json:"content_type"` 35 | 36 | // Content of the file 37 | Content io.Reader `json:"content"` 38 | } 39 | 40 | // Money represents an amount of money with its currency type. 41 | type Money struct { 42 | // CurrencyCode is the 3-letter currency code defined in ISO 4217. 43 | CurrencyCode string `json:"currency_code,omitempty"` 44 | 45 | // Units is the whole units of the amount. 46 | // For example if `currencyCode` is `"USD"`, then 1 unit is one US dollar. 47 | Units int64 `json:"units,omitempty"` 48 | 49 | // Nanos is the number of nano (10^-9) units of the amount. 50 | // The value must be between -999,999,999 and +999,999,999 inclusive. 51 | // If `units` is positive, `nanos` must be positive or zero. 52 | // If `units` is zero, `nanos` can be positive, zero, or negative. 53 | // If `units` is negative, `nanos` must be negative or zero. 54 | // For example $-1.75 is represented as `units`=-1 and `nanos`=-750,000,000. 55 | Nanos int32 `json:"nanos,omitempty"` 56 | } 57 | 58 | // NewMoneyFromFloat conerts a float with currency to a Money object. 59 | func NewMoneyFromFloat(value float64, currency string) *Money { 60 | return &Money{ 61 | CurrencyCode: currency, 62 | Units: int64(value), 63 | Nanos: int32((value - float64(int64(value))) * 1000000000), 64 | } 65 | } 66 | 67 | // ToFloat converts a Money object to a float. 68 | func (m *Money) ToFloat() float64 { 69 | return float64(m.Units) + float64(m.Nanos)/1000000000 70 | } 71 | 72 | // Money represents a size in bytes. 73 | type Size uint64 74 | 75 | const ( 76 | B Size = 1 77 | KB = 1000 * B 78 | MB = 1000 * KB 79 | GB = 1000 * MB 80 | TB = 1000 * GB 81 | PB = 1000 * TB 82 | ) 83 | 84 | // String returns the string representation of a Size. 85 | func (s Size) String() string { 86 | return fmt.Sprintf("%d", s) 87 | } 88 | 89 | // TimeSeries represents a time series that could be used for graph purposes. 90 | type TimeSeries struct { 91 | // Name of the metric. 92 | Name string `json:"name"` 93 | 94 | // Points contains all the points that composed the series. 95 | Points []*TimeSeriesPoint `json:"points"` 96 | 97 | // Metadata contains some string metadata related to a metric. 98 | Metadata map[string]string `json:"metadata"` 99 | } 100 | 101 | // TimeSeriesPoint represents a point of a time series. 102 | type TimeSeriesPoint struct { 103 | Timestamp time.Time 104 | Value float32 105 | } 106 | 107 | func (tsp *TimeSeriesPoint) MarshalJSON() ([]byte, error) { 108 | timestamp := tsp.Timestamp.Format(time.RFC3339) 109 | value, err := json.Marshal(tsp.Value) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return []byte(`["` + timestamp + `",` + string(value) + "]"), nil 115 | } 116 | 117 | func (tsp *TimeSeriesPoint) UnmarshalJSON(b []byte) error { 118 | point := [2]interface{}{} 119 | 120 | err := json.Unmarshal(b, &point) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if len(point) != 2 { 126 | return errors.New("invalid point array") 127 | } 128 | 129 | strTimestamp, isStrTimestamp := point[0].(string) 130 | if !isStrTimestamp { 131 | return errors.New("%s timestamp is not a string in RFC 3339 format", point[0]) 132 | } 133 | timestamp, err := time.Parse(time.RFC3339, strTimestamp) 134 | if err != nil { 135 | return errors.New("%s timestamp is not in RFC 3339 format", point[0]) 136 | } 137 | tsp.Timestamp = timestamp 138 | 139 | // By default, JSON unmarshal a float in float64 but the TimeSeriesPoint is a float32 value. 140 | value, isValue := point[1].(float64) 141 | if !isValue { 142 | return errors.New("%s is not a valid float32 value", point[1]) 143 | } 144 | tsp.Value = float32(value) 145 | 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /internal/marshaler/duration.go: -------------------------------------------------------------------------------- 1 | package marshaler 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // Duration implements a JSON Marshaler to encode a time.Duration in milliseconds. 9 | type Duration int64 10 | 11 | const milliSec = Duration(time.Millisecond) 12 | 13 | // NewDuration converts a *time.Duration to a *Duration type. 14 | func NewDuration(t *time.Duration) *Duration { 15 | if t == nil { 16 | return nil 17 | } 18 | d := Duration(t.Nanoseconds()) 19 | return &d 20 | } 21 | 22 | // Standard converts a *Duration to a *time.Duration type. 23 | func (d *Duration) Standard() *time.Duration { 24 | return (*time.Duration)(d) 25 | } 26 | 27 | // MarshalJSON encodes the Duration in milliseconds. 28 | func (d Duration) MarshalJSON() ([]byte, error) { 29 | return json.Marshal(int64(d / milliSec)) 30 | } 31 | 32 | // UnmarshalJSON decodes milliseconds to Duration. 33 | func (d *Duration) UnmarshalJSON(b []byte) error { 34 | var tmp int64 35 | err := json.Unmarshal(b, &tmp) 36 | if err != nil { 37 | return err 38 | } 39 | *d = Duration(tmp) * milliSec 40 | return nil 41 | } 42 | 43 | // DurationSlice is a slice of *Duration 44 | type DurationSlice []*Duration 45 | 46 | // NewDurationSlice converts a []*time.Duration to a DurationSlice type. 47 | func NewDurationSlice(t []*time.Duration) DurationSlice { 48 | ds := make([]*Duration, len(t)) 49 | for i := range ds { 50 | ds[i] = NewDuration(t[i]) 51 | } 52 | return ds 53 | } 54 | 55 | // Standard converts a DurationSlice to a []*time.Duration type. 56 | func (ds *DurationSlice) Standard() []*time.Duration { 57 | t := make([]*time.Duration, len(*ds)) 58 | for i := range t { 59 | t[i] = (*ds)[i].Standard() 60 | } 61 | return t 62 | } 63 | 64 | // Durationint32Map is a int32 map of *Duration 65 | type Durationint32Map map[int32]*Duration 66 | 67 | // NewDurationint32Map converts a map[int32]*time.Duration to a Durationint32Map type. 68 | func NewDurationint32Map(t map[int32]*time.Duration) Durationint32Map { 69 | dm := make(Durationint32Map, len(t)) 70 | for i := range t { 71 | dm[i] = NewDuration(t[i]) 72 | } 73 | return dm 74 | } 75 | 76 | // Standard converts a Durationint32Map to a map[int32]*time.Duration type. 77 | func (dm *Durationint32Map) Standard() map[int32]*time.Duration { 78 | t := make(map[int32]*time.Duration, len(*dm)) 79 | for key, value := range *dm { 80 | t[key] = value.Standard() 81 | } 82 | return t 83 | } 84 | 85 | // LongDuration implements a JSON Marshaler to encode a time.Duration in days. 86 | type LongDuration int64 87 | 88 | const day = LongDuration(time.Hour) * 24 89 | 90 | // NewLongDuration converts a *time.Duration to a *LongDuration type. 91 | func NewLongDuration(t *time.Duration) *LongDuration { 92 | if t == nil { 93 | return nil 94 | } 95 | d := LongDuration(t.Nanoseconds()) 96 | return &d 97 | } 98 | 99 | // Standard converts a *LongDuration to a *time.Duration type. 100 | func (d *LongDuration) Standard() *time.Duration { 101 | return (*time.Duration)(d) 102 | } 103 | 104 | // MarshalJSON encodes the LongDuration in days. 105 | func (d LongDuration) MarshalJSON() ([]byte, error) { 106 | return json.Marshal(int64(d / day)) 107 | } 108 | 109 | // UnmarshalJSON decodes days to LongDuration. 110 | func (d *LongDuration) UnmarshalJSON(b []byte) error { 111 | var tmp int64 112 | err := json.Unmarshal(b, &tmp) 113 | if err != nil { 114 | return err 115 | } 116 | *d = LongDuration(tmp) * day 117 | return nil 118 | } 119 | 120 | // LongDurationSlice is a slice of *LongDuration 121 | type LongDurationSlice []*LongDuration 122 | 123 | // NewLongDurationSlice converts a []*time.Duration to a LongDurationSlice type. 124 | func NewLongDurationSlice(t []*time.Duration) LongDurationSlice { 125 | ds := make([]*LongDuration, len(t)) 126 | for i := range ds { 127 | ds[i] = NewLongDuration(t[i]) 128 | } 129 | return ds 130 | } 131 | 132 | // Standard converts a LongDurationSlice to a []*time.Duration type. 133 | func (ds *LongDurationSlice) Standard() []*time.Duration { 134 | t := make([]*time.Duration, len(*ds)) 135 | for i := range t { 136 | t[i] = (*ds)[i].Standard() 137 | } 138 | return t 139 | } 140 | 141 | // LongDurationint32Map is a int32 map of *LongDuration 142 | type LongDurationint32Map map[int32]*LongDuration 143 | 144 | // NewLongDurationint32Map converts a map[int32]*time.LongDuration to a LongDurationint32Map type. 145 | func NewLongDurationint32Map(t map[int32]*time.Duration) LongDurationint32Map { 146 | dm := make(LongDurationint32Map, len(t)) 147 | for i := range t { 148 | dm[i] = NewLongDuration(t[i]) 149 | } 150 | return dm 151 | } 152 | 153 | // Standard converts a LongDurationint32Map to a map[int32]*time.LongDuration type. 154 | func (dm *LongDurationint32Map) Standard() map[int32]*time.Duration { 155 | t := make(map[int32]*time.Duration, len(*dm)) 156 | for key, value := range *dm { 157 | t[key] = value.Standard() 158 | } 159 | return t 160 | } 161 | -------------------------------------------------------------------------------- /api/instance/v1/instance_utils_test.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers/httprecorder" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | ) 10 | 11 | func TestInstanceHelpers(t *testing.T) { 12 | 13 | client, r, err := httprecorder.CreateRecordedScwClient("utils-test") 14 | testhelpers.AssertNoError(t, err) 15 | defer func() { 16 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 17 | }() 18 | 19 | instanceAPI := NewAPI(client) 20 | 21 | var ( 22 | serverID string 23 | ipID string 24 | volumeID string 25 | zone = scw.ZoneFrPar1 26 | organization = "d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3" 27 | image = "f974feac-abae-4365-b988-8ec7d1cec10d" 28 | reverse = &NullableStringValue{Value: "1.1.1.1"} 29 | nullReverse = &NullableStringValue{Null: true} 30 | ) 31 | 32 | t.Run("create server", func(t *testing.T) { 33 | createServerResponse, err := instanceAPI.CreateServer(&CreateServerRequest{ 34 | Zone: zone, 35 | Name: "instance_utils_test", 36 | Organization: organization, 37 | Image: image, 38 | }) 39 | testhelpers.AssertNoError(t, err) 40 | serverID = createServerResponse.Server.ID 41 | for _, volume := range createServerResponse.Server.Volumes { 42 | volumeID = volume.ID 43 | } 44 | }) 45 | 46 | t.Run("test ip related functions", func(t *testing.T) { 47 | 48 | // Create IP 49 | createIPResponse, err := instanceAPI.CreateIP(&CreateIPRequest{ 50 | Zone: zone, 51 | Organization: organization, 52 | }) 53 | testhelpers.AssertNoError(t, err) 54 | ipID = createIPResponse.IP.ID 55 | 56 | // Attach IP 57 | ipAttachResponse, err := instanceAPI.AttachIP(&AttachIPRequest{ 58 | IPID: ipID, 59 | Zone: zone, 60 | ServerID: serverID, 61 | }) 62 | 63 | testhelpers.AssertNoError(t, err) 64 | testhelpers.Equals(t, serverID, ipAttachResponse.IP.Server.ID) 65 | 66 | // Detach IP 67 | ipDetachResponse, err := instanceAPI.DetachIP(&DetachIPRequest{ 68 | IPID: ipID, 69 | Zone: zone, 70 | }) 71 | 72 | testhelpers.AssertNoError(t, err) 73 | testhelpers.Assert(t, nil == ipDetachResponse.IP.Server, "Server object should be nil for detached IP.") 74 | 75 | // Set reverse 76 | ipSetReverseResponse, err := instanceAPI.UpdateIP(&UpdateIPRequest{ 77 | IPID: ipID, 78 | Zone: zone, 79 | Reverse: reverse, 80 | }) 81 | testhelpers.AssertNoError(t, err) 82 | testhelpers.Equals(t, reverse.Value, *ipSetReverseResponse.IP.Reverse) 83 | 84 | // Omitempty reverse 85 | ipSetReverseResponse, err = instanceAPI.UpdateIP(&UpdateIPRequest{ 86 | IPID: ipID, 87 | Zone: zone, 88 | Reverse: nil, 89 | }) 90 | testhelpers.AssertNoError(t, err) 91 | testhelpers.Equals(t, reverse.Value, *ipSetReverseResponse.IP.Reverse) 92 | 93 | // Unset reverse 94 | ipDeleteReverseResponse, err := instanceAPI.UpdateIP(&UpdateIPRequest{ 95 | IPID: ipID, 96 | Zone: zone, 97 | Reverse: nullReverse, 98 | }) 99 | testhelpers.AssertNoError(t, err) 100 | testhelpers.Equals(t, (*string)(nil), ipDeleteReverseResponse.IP.Reverse) 101 | 102 | // Delete IP 103 | err = instanceAPI.DeleteIP(&DeleteIPRequest{ 104 | Zone: zone, 105 | IPID: ipID, 106 | }) 107 | testhelpers.AssertNoError(t, err) 108 | 109 | }) 110 | 111 | t.Run("Test attach and detach volume", func(t *testing.T) { 112 | 113 | detachVolumeResponse, err := instanceAPI.DetachVolume(&DetachVolumeRequest{ 114 | Zone: zone, 115 | VolumeID: volumeID, 116 | }) 117 | testhelpers.AssertNoError(t, err) 118 | 119 | testhelpers.Assert(t, detachVolumeResponse.Server != nil, "Should have server in response") 120 | testhelpers.Assert(t, detachVolumeResponse.Server.Volumes != nil, "Should have volumes in response") 121 | testhelpers.Assert(t, len(detachVolumeResponse.Server.Volumes) == 0, "Server should have zero volumes after detaching") 122 | 123 | attachVolumeResponse, err := instanceAPI.AttachVolume(&AttachVolumeRequest{ 124 | Zone: zone, 125 | ServerID: serverID, 126 | VolumeID: volumeID, 127 | }) 128 | testhelpers.AssertNoError(t, err) 129 | 130 | testhelpers.Assert(t, attachVolumeResponse.Server != nil, "Should have server in response") 131 | testhelpers.Assert(t, attachVolumeResponse.Server.Volumes != nil, "Should have volumes in response") 132 | testhelpers.Assert(t, len(attachVolumeResponse.Server.Volumes) == 1, "Server should have one volumes after attaching") 133 | testhelpers.Equals(t, volumeID, attachVolumeResponse.Server.Volumes["0"].ID) 134 | }) 135 | 136 | t.Run("teardown: delete server and volume", func(t *testing.T) { 137 | // Delete Server 138 | err = instanceAPI.DeleteServer(&DeleteServerRequest{ 139 | Zone: zone, 140 | ServerID: serverID, 141 | }) 142 | testhelpers.AssertNoError(t, err) 143 | 144 | // Delete Volume 145 | err = instanceAPI.DeleteVolume(&DeleteVolumeRequest{ 146 | Zone: zone, 147 | VolumeID: volumeID, 148 | }) 149 | testhelpers.AssertNoError(t, err) 150 | 151 | }) 152 | 153 | } 154 | -------------------------------------------------------------------------------- /api/instance/v1/testdata/volume-utils-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: '{"name":"test volume","organization":"d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3","volume_type":"l_ssd","size":20000000000}' 6 | form: {} 7 | headers: 8 | Content-Type: 9 | - application/json 10 | User-Agent: 11 | - scaleway-sdk-go/0.0.0 (go1.12.5; darwin; amd64) 12 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/volumes 13 | method: POST 14 | response: 15 | body: '{"volume": {"size": 20000000000, "state": "available", "name": "test volume", 16 | "modification_date": "2019-06-19T15:22:50.083278+00:00", "organization": "d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3", 17 | "export_uri": null, "creation_date": "2019-06-19T15:22:50.083278+00:00", "id": 18 | "f5f47e99-c0dc-40c9-be24-c3fc8294af08", "volume_type": "l_ssd", "server": null}}' 19 | headers: 20 | Cache-Control: 21 | - no-cache 22 | Content-Length: 23 | - "350" 24 | Content-Security-Policy: 25 | - default-src 'none'; frame-ancestors 'none' 26 | Content-Type: 27 | - application/json 28 | Date: 29 | - Wed, 19 Jun 2019 15:22:50 GMT 30 | Location: 31 | - https://cp-par1.scaleway.com/volumes/f5f47e99-c0dc-40c9-be24-c3fc8294af08 32 | Server: 33 | - scaleway_api 34 | Strict-Transport-Security: 35 | - max-age=63072000 36 | X-Content-Type-Options: 37 | - nosniff 38 | X-Frame-Options: 39 | - DENY 40 | status: 201 Created 41 | code: 201 42 | duration: "" 43 | - request: 44 | body: "" 45 | form: {} 46 | headers: 47 | User-Agent: 48 | - scaleway-sdk-go/0.0.0 (go1.12.5; darwin; amd64) 49 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/volumes/f5f47e99-c0dc-40c9-be24-c3fc8294af08 50 | method: GET 51 | response: 52 | body: '{"volume": {"size": 20000000000, "state": "available", "name": "test volume", 53 | "modification_date": "2019-06-19T15:22:50.083278+00:00", "organization": "d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3", 54 | "export_uri": null, "creation_date": "2019-06-19T15:22:50.083278+00:00", "id": 55 | "f5f47e99-c0dc-40c9-be24-c3fc8294af08", "volume_type": "l_ssd", "server": null}}' 56 | headers: 57 | Cache-Control: 58 | - no-cache 59 | Content-Length: 60 | - "350" 61 | Content-Security-Policy: 62 | - default-src 'none'; frame-ancestors 'none' 63 | Content-Type: 64 | - application/json 65 | Date: 66 | - Wed, 19 Jun 2019 15:22:50 GMT 67 | Server: 68 | - scaleway_api 69 | Strict-Transport-Security: 70 | - max-age=63072000 71 | X-Content-Type-Options: 72 | - nosniff 73 | X-Frame-Options: 74 | - DENY 75 | status: 200 OK 76 | code: 200 77 | duration: "" 78 | - request: 79 | body: '{"id":"f5f47e99-c0dc-40c9-be24-c3fc8294af08","name":"some new volume name","export_uri":"","size":20000000000,"volume_type":"l_ssd","creation_date":"2019-06-19T15:22:50.083278Z","modification_date":"2019-06-19T15:22:50.083278Z","organization":"d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3","server":null}' 80 | form: {} 81 | headers: 82 | Content-Type: 83 | - application/json 84 | User-Agent: 85 | - scaleway-sdk-go/0.0.0 (go1.12.5; darwin; amd64) 86 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/volumes/f5f47e99-c0dc-40c9-be24-c3fc8294af08 87 | method: PUT 88 | response: 89 | body: '{"volume": {"size": 20000000000, "state": "available", "name": "some new 90 | volume name", "modification_date": "2019-06-19T15:22:50.083278+00:00", "organization": 91 | "d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3", "export_uri": null, "creation_date": 92 | "2019-06-19T15:22:50.083278+00:00", "id": "f5f47e99-c0dc-40c9-be24-c3fc8294af08", 93 | "volume_type": "l_ssd", "server": null}}' 94 | headers: 95 | Cache-Control: 96 | - no-cache 97 | Content-Length: 98 | - "359" 99 | Content-Security-Policy: 100 | - default-src 'none'; frame-ancestors 'none' 101 | Content-Type: 102 | - application/json 103 | Date: 104 | - Wed, 19 Jun 2019 15:22:50 GMT 105 | Server: 106 | - scaleway_api 107 | Strict-Transport-Security: 108 | - max-age=63072000 109 | X-Content-Type-Options: 110 | - nosniff 111 | X-Frame-Options: 112 | - DENY 113 | status: 200 OK 114 | code: 200 115 | duration: "" 116 | - request: 117 | body: "" 118 | form: {} 119 | headers: 120 | User-Agent: 121 | - scaleway-sdk-go/0.0.0 (go1.12.5; darwin; amd64) 122 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/volumes/f5f47e99-c0dc-40c9-be24-c3fc8294af08 123 | method: DELETE 124 | response: 125 | body: "" 126 | headers: 127 | Cache-Control: 128 | - no-cache 129 | Content-Security-Policy: 130 | - default-src 'none'; frame-ancestors 'none' 131 | Content-Type: 132 | - application/json 133 | Date: 134 | - Wed, 19 Jun 2019 15:22:50 GMT 135 | Server: 136 | - scaleway_api 137 | Strict-Transport-Security: 138 | - max-age=63072000 139 | X-Content-Type-Options: 140 | - nosniff 141 | X-Frame-Options: 142 | - DENY 143 | status: 204 No Content 144 | code: 204 145 | duration: "" 146 | -------------------------------------------------------------------------------- /scw/config.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/scaleway/scaleway-sdk-go/internal/auth" 9 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 10 | "github.com/scaleway/scaleway-sdk-go/logger" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | const ( 15 | documentationLink = "https://github.com/scaleway/scaleway-sdk-go/blob/master/scw/README.md" 16 | defaultConfigPermission = 0600 17 | ) 18 | 19 | type Config struct { 20 | Profile `yaml:",inline"` 21 | ActiveProfile *string `yaml:"active_profile,omitempty"` 22 | Profiles map[string]*Profile `yaml:"profiles,omitempty"` 23 | } 24 | 25 | type Profile struct { 26 | AccessKey *string `yaml:"access_key,omitempty"` 27 | SecretKey *string `yaml:"secret_key,omitempty"` 28 | APIURL *string `yaml:"api_url,omitempty"` 29 | Insecure *bool `yaml:"insecure,omitempty"` 30 | DefaultOrganizationID *string `yaml:"default_organization_id,omitempty"` 31 | DefaultRegion *string `yaml:"default_region,omitempty"` 32 | DefaultZone *string `yaml:"default_zone,omitempty"` 33 | } 34 | 35 | func (p *Profile) String() string { 36 | p2 := *p 37 | p2.SecretKey = hideSecretKey(p2.SecretKey) 38 | configRaw, _ := yaml.Marshal(p2) 39 | return string(configRaw) 40 | } 41 | 42 | // clone deep copy config object 43 | func (c *Config) clone() *Config { 44 | c2 := &Config{} 45 | configRaw, _ := yaml.Marshal(c) 46 | _ = yaml.Unmarshal(configRaw, c2) 47 | return c2 48 | } 49 | 50 | func (c *Config) String() string { 51 | c2 := c.clone() 52 | c2.SecretKey = hideSecretKey(c2.SecretKey) 53 | for _, p := range c2.Profiles { 54 | p.SecretKey = hideSecretKey(p.SecretKey) 55 | } 56 | 57 | configRaw, _ := yaml.Marshal(c2) 58 | return string(configRaw) 59 | } 60 | 61 | func hideSecretKey(key *string) *string { 62 | if key == nil { 63 | return nil 64 | } 65 | 66 | newKey := auth.HideSecretKey(*key) 67 | return &newKey 68 | } 69 | 70 | func unmarshalConfV2(content []byte) (*Config, error) { 71 | var config Config 72 | 73 | err := yaml.Unmarshal(content, &config) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return &config, nil 78 | } 79 | 80 | // MustLoadConfig is like LoadConfig but panic instead of returning an error. 81 | func MustLoadConfig() *Config { 82 | c, err := LoadConfigFromPath(GetConfigPath()) 83 | if err != nil { 84 | panic(err) 85 | } 86 | return c 87 | } 88 | 89 | // LoadConfig read the config from the default path. 90 | func LoadConfig() (*Config, error) { 91 | return LoadConfigFromPath(GetConfigPath()) 92 | } 93 | 94 | // LoadConfigFromPath read the config from the given path. 95 | func LoadConfigFromPath(path string) (*Config, error) { 96 | 97 | file, err := ioutil.ReadFile(path) 98 | if err != nil { 99 | return nil, errors.Wrap(err, "cannot read config file") 100 | } 101 | 102 | _, err = unmarshalConfV1(file) 103 | if err == nil { 104 | // reject V1 config 105 | 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) 106 | } 107 | 108 | confV2, err := unmarshalConfV2(file) 109 | if err != nil { 110 | return nil, errors.Wrap(err, "content of config file %s is invalid", path) 111 | } 112 | 113 | return confV2, nil 114 | } 115 | 116 | // GetProfile returns the profile corresponding to the given profile name. 117 | func (c *Config) GetProfile(profileName string) (*Profile, error) { 118 | if profileName == "" { 119 | return nil, errors.New("active profile cannot be empty") 120 | } 121 | 122 | p, exist := c.Profiles[profileName] 123 | if !exist { 124 | return nil, errors.New("given profile %s does not exist", profileName) 125 | } 126 | 127 | return p, nil 128 | } 129 | 130 | // GetActiveProfile returns the active profile of the config based on the following order: 131 | // env SCW_PROFILE > config active_profile > config root profile 132 | func (c *Config) GetActiveProfile() (*Profile, error) { 133 | switch { 134 | case os.Getenv(scwActiveProfileEnv) != "": 135 | logger.Debugf("using active profile from env: %s=%s", scwActiveProfileEnv, os.Getenv(scwActiveProfileEnv)) 136 | return c.GetProfile(os.Getenv(scwActiveProfileEnv)) 137 | case c.ActiveProfile != nil: 138 | logger.Debugf("using active profile from config: active_profile=%s", scwActiveProfileEnv, *c.ActiveProfile) 139 | return c.GetProfile(*c.ActiveProfile) 140 | default: 141 | return &c.Profile, nil 142 | } 143 | 144 | } 145 | 146 | // SaveTo will save the config to the default config path. This 147 | // action will overwrite the previous file when it exists. 148 | func (c *Config) Save() error { 149 | return c.SaveTo(GetConfigPath()) 150 | } 151 | 152 | // SaveTo will save the config to the given path. This action will 153 | // overwrite the previous file when it exists. 154 | func (c *Config) SaveTo(path string) error { 155 | path = filepath.Clean(path) 156 | 157 | // STEP 1: marshal config 158 | file, err := yaml.Marshal(c) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | // STEP 2: create config path dir in cases it didn't exist before 164 | err = os.MkdirAll(filepath.Dir(path), 0700) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | // STEP 3: write new config file 170 | err = ioutil.WriteFile(path, file, defaultConfigPermission) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | return nil 176 | 177 | } 178 | -------------------------------------------------------------------------------- /internal/e2e/endtoend_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/api/test/v1" 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | ) 10 | 11 | func newE2EClient(withAuthInClient bool) (*test.API, string, error) { 12 | client, err := scw.NewClient( 13 | scw.WithoutAuth(), 14 | scw.WithDefaultRegion(scw.RegionFrPar), 15 | ) 16 | if err != nil { 17 | return nil, "", err 18 | } 19 | testClient := test.NewAPI(client) 20 | 21 | registerResponse, err := testClient.Register(&test.RegisterRequest{ 22 | Username: "sidi", 23 | }) 24 | if err != nil { 25 | return nil, "", err 26 | } 27 | if withAuthInClient { 28 | client, err = scw.NewClient( 29 | scw.WithDefaultRegion(scw.RegionFrPar), 30 | scw.WithAuth("", registerResponse.SecretKey), 31 | ) 32 | testClient = test.NewAPI(client) 33 | } 34 | 35 | return testClient, registerResponse.SecretKey, err 36 | } 37 | 38 | func TestAuthInRequest(t *testing.T) { 39 | client, secretKey, err := newE2EClient(false) 40 | testhelpers.AssertNoError(t, err) 41 | 42 | requestWithAuth := scw.WithAuthRequest("", secretKey) 43 | _, err = client.CreateHuman(&test.CreateHumanRequest{}, requestWithAuth) 44 | testhelpers.AssertNoError(t, err) 45 | } 46 | 47 | func TestHuman(t *testing.T) { 48 | client, _, err := newE2EClient(true) 49 | testhelpers.AssertNoError(t, err) 50 | 51 | // create 52 | human, err := client.CreateHuman(&test.CreateHumanRequest{ 53 | Height: 170.5, 54 | ShoeSize: 35.1, 55 | AltitudeInMeter: -12, 56 | AltitudeInMillimeter: -12050, 57 | FingersCount: 21, 58 | HairCount: 9223372036854775808, 59 | IsHappy: true, 60 | EyesColor: test.EyeColorsAmber, 61 | ProjectID: "b3ba839a-dcf2-4b0a-ac81-fc32370052a0", 62 | }) 63 | 64 | testhelpers.AssertNoError(t, err) 65 | testhelpers.Equals(t, human.Height, 170.5) 66 | testhelpers.Equals(t, human.ShoeSize, float32(35.1)) 67 | testhelpers.Equals(t, human.AltitudeInMeter, int32(-12)) 68 | testhelpers.Equals(t, human.AltitudeInMillimeter, int64(-12050)) 69 | testhelpers.Equals(t, human.FingersCount, uint32(21)) 70 | testhelpers.Equals(t, human.HairCount, uint64(9223372036854775808)) 71 | testhelpers.Equals(t, human.IsHappy, true) 72 | testhelpers.Equals(t, human.EyesColor, test.EyeColorsAmber) 73 | testhelpers.Equals(t, human.ProjectID, "b3ba839a-dcf2-4b0a-ac81-fc32370052a0") 74 | 75 | // single parameter update 76 | human, err = client.UpdateHuman(&test.UpdateHumanRequest{ 77 | HumanID: human.ID, 78 | IsHappy: scw.BoolPtr(false), 79 | }) 80 | 81 | testhelpers.AssertNoError(t, err) 82 | testhelpers.Equals(t, human.Height, 170.5) 83 | testhelpers.Equals(t, human.ShoeSize, float32(35.1)) 84 | testhelpers.Equals(t, human.AltitudeInMeter, int32(-12)) 85 | testhelpers.Equals(t, human.AltitudeInMillimeter, int64(-12050)) 86 | testhelpers.Equals(t, human.FingersCount, uint32(21)) 87 | testhelpers.Equals(t, human.HairCount, uint64(9223372036854775808)) 88 | testhelpers.Equals(t, human.IsHappy, false) 89 | testhelpers.Equals(t, human.EyesColor, test.EyeColorsAmber) 90 | testhelpers.Equals(t, human.ProjectID, "b3ba839a-dcf2-4b0a-ac81-fc32370052a0") 91 | 92 | // update 93 | human, err = client.UpdateHuman(&test.UpdateHumanRequest{ 94 | HumanID: human.ID, 95 | Height: scw.Float64Ptr(155.666), 96 | ShoeSize: scw.Float32Ptr(36.0), 97 | AltitudeInMeter: scw.Int32Ptr(2147483647), 98 | AltitudeInMillimeter: scw.Int64Ptr(2147483647285), 99 | FingersCount: scw.Uint32Ptr(20), 100 | HairCount: scw.Uint64Ptr(9223372036854775809), 101 | IsHappy: scw.BoolPtr(true), 102 | EyesColor: test.EyeColorsBlue, 103 | }) 104 | 105 | testhelpers.AssertNoError(t, err) 106 | testhelpers.Equals(t, human.Height, 155.666) 107 | testhelpers.Equals(t, human.ShoeSize, float32(36.0)) 108 | testhelpers.Equals(t, human.AltitudeInMeter, int32(2147483647)) 109 | testhelpers.Equals(t, human.AltitudeInMillimeter, int64(2147483647285)) 110 | testhelpers.Equals(t, human.FingersCount, uint32(20)) 111 | testhelpers.Equals(t, human.HairCount, uint64(9223372036854775809)) 112 | testhelpers.Equals(t, human.IsHappy, true) 113 | testhelpers.Equals(t, human.EyesColor, test.EyeColorsBlue) 114 | testhelpers.Equals(t, human.ProjectID, "b3ba839a-dcf2-4b0a-ac81-fc32370052a0") 115 | 116 | // get 117 | human, err = client.GetHuman(&test.GetHumanRequest{ 118 | HumanID: human.ID, 119 | }) 120 | 121 | testhelpers.AssertNoError(t, err) 122 | testhelpers.Equals(t, human.Height, 155.666) 123 | testhelpers.Equals(t, human.ShoeSize, float32(36.0)) 124 | testhelpers.Equals(t, human.AltitudeInMeter, int32(2147483647)) 125 | testhelpers.Equals(t, human.AltitudeInMillimeter, int64(2147483647285)) 126 | testhelpers.Equals(t, human.FingersCount, uint32(20)) 127 | testhelpers.Equals(t, human.HairCount, uint64(9223372036854775809)) 128 | testhelpers.Equals(t, human.IsHappy, true) 129 | testhelpers.Equals(t, human.EyesColor, test.EyeColorsBlue) 130 | testhelpers.Equals(t, human.ProjectID, "b3ba839a-dcf2-4b0a-ac81-fc32370052a0") 131 | 132 | // delete 133 | _, err = client.DeleteHuman(&test.DeleteHumanRequest{ 134 | HumanID: human.ID, 135 | }) 136 | testhelpers.AssertNoError(t, err) 137 | 138 | // get 139 | _, err = client.GetHuman(&test.GetHumanRequest{ 140 | HumanID: human.ID, 141 | }) 142 | testhelpers.Equals(t, "scaleway-sdk-go: http error 404 Not Found: human not found", err.Error()) 143 | } 144 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package scalewaysdkgo 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/api/instance/v1" 8 | "github.com/scaleway/scaleway-sdk-go/api/lb/v1" 9 | "github.com/scaleway/scaleway-sdk-go/scw" 10 | ) 11 | 12 | func Example_apiClient() { 13 | 14 | // Create a Scaleway client 15 | client, err := scw.NewClient( 16 | scw.WithAuth("ACCESS_KEY", "SECRET_KEY"), // Get your credentials at https://console.scaleway.com/account/credentials 17 | ) 18 | if err != nil { 19 | // handle error 20 | } 21 | 22 | // Create SDK objects for specific Scaleway Products 23 | instance := instance.NewAPI(client) 24 | lb := lb.NewAPI(client) 25 | 26 | // Start using the SDKs 27 | _, _ = instance, lb 28 | 29 | } 30 | 31 | func Example_apiClientWithConfig() { 32 | 33 | // Get Scaleway Config 34 | config, err := scw.LoadConfig() 35 | if err != nil { 36 | // handle error 37 | } 38 | 39 | // Use active profile 40 | profile, err := config.GetActiveProfile() 41 | if err != nil { 42 | // handle error 43 | } 44 | 45 | // Create a Scaleway client 46 | client, err := scw.NewClient( 47 | scw.WithProfile(profile), 48 | scw.WithEnv(), // env variable may overwrite profile values 49 | ) 50 | if err != nil { 51 | // handle error 52 | } 53 | 54 | // Create SDK objects for specific Scaleway Products 55 | instance := instance.NewAPI(client) 56 | lb := lb.NewAPI(client) 57 | 58 | // Start using the SDKs 59 | _, _ = instance, lb 60 | 61 | } 62 | 63 | func Example_listServers() { 64 | 65 | // Create a Scaleway client 66 | client, err := scw.NewClient( 67 | scw.WithAuth("ACCESS_KEY", "SECRET_KEY"), // Get your credentials at https://console.scaleway.com/account/credentials 68 | ) 69 | if err != nil { 70 | // handle error 71 | } 72 | 73 | // Create SDK objects for Scaleway Instance product 74 | instanceAPI := instance.NewAPI(client) 75 | 76 | // Call the ListServers method on the Instance SDK 77 | response, err := instanceAPI.ListServers(&instance.ListServersRequest{ 78 | Zone: scw.ZoneFrPar1, 79 | }) 80 | if err != nil { 81 | // handle error 82 | } 83 | 84 | // Do something with the response... 85 | fmt.Println(response) 86 | } 87 | 88 | func Example_createServer() { 89 | 90 | // Create a Scaleway client 91 | client, err := scw.NewClient( 92 | scw.WithAuth("ACCESS_KEY", "SECRET_KEY"), // Get your credentials at https://console.scaleway.com/account/credentials 93 | scw.WithDefaultOrganizationID("ORGANIZATION_ID"), 94 | scw.WithDefaultZone(scw.ZoneFrPar1), 95 | ) 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | // Create SDK objects for Scaleway Instance and marketplace 101 | instanceAPI := instance.NewAPI(client) 102 | 103 | serverType := "DEV1-S" 104 | image := "ubuntu-bionic" 105 | 106 | // Create a new DEV1-S server 107 | createRes, err := instanceAPI.CreateServer(&instance.CreateServerRequest{ 108 | Name: "my-server-01", 109 | CommercialType: serverType, 110 | Image: image, 111 | DynamicIPRequired: scw.BoolPtr(true), 112 | }) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | // Start the server and wait until it's ready. 118 | err = instanceAPI.ServerActionAndWait(&instance.ServerActionAndWaitRequest{ 119 | ServerID: createRes.Server.ID, 120 | Action: instance.ServerActionPoweron, 121 | Timeout: 5 * time.Minute, 122 | }) 123 | if err != nil { 124 | panic(err) 125 | } 126 | } 127 | 128 | func Example_rebootAllServers() { 129 | 130 | // Create a Scaleway client 131 | client, err := scw.NewClient( 132 | scw.WithAuth("ACCESS_KEY", "SECRET_KEY"), // Get your credentials at https://console.scaleway.com/account/credentials 133 | scw.WithDefaultZone(scw.ZoneFrPar1), 134 | ) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | // Create SDK objects for Scaleway Instance product 140 | instanceAPI := instance.NewAPI(client) 141 | 142 | // Call the ListServers method of the Instance SDK 143 | response, err := instanceAPI.ListServers(&instance.ListServersRequest{}) 144 | if err != nil { 145 | panic(err) 146 | } 147 | 148 | // For each server if they are running we reboot them using ServerActionAndWait 149 | for _, server := range response.Servers { 150 | if server.State == instance.ServerStateRunning { 151 | fmt.Println("Rebooting server with ID", server.ID) 152 | err = instanceAPI.ServerActionAndWait(&instance.ServerActionAndWaitRequest{ 153 | ServerID: server.ID, 154 | Action: instance.ServerActionReboot, 155 | Timeout: 5 * time.Minute, 156 | }) 157 | if err != nil { 158 | panic(err) 159 | } 160 | } 161 | } 162 | fmt.Println("All servers were successfully rebooted") 163 | } 164 | 165 | func Example_createLoadBalancer() { 166 | 167 | // Create a Scaleway client 168 | client, err := scw.NewClient( 169 | scw.WithAuth("ACCESS_KEY", "SECRET_KEY"), // Get your credentials at https://console.scaleway.com/account/credentials 170 | ) 171 | if err != nil { 172 | // handle error 173 | } 174 | 175 | // Create SDK objects for Scaleway LoadConfig Balancer product 176 | lbAPI := lb.NewAPI(client) 177 | 178 | // Call the CreateLb method on the LB SDK to create a new load balancer. 179 | newLb, err := lbAPI.CreateLb(&lb.CreateLbRequest{ 180 | Name: "My new load balancer", 181 | Description: "This is a example of a load balancer", 182 | OrganizationID: "000a115d-2852-4b0a-9ce8-47f1134ba95a", 183 | Region: scw.RegionFrPar, 184 | }) 185 | 186 | if err != nil { 187 | // handle error 188 | } 189 | 190 | // Do something with the newly created LB... 191 | fmt.Println(newLb) 192 | 193 | } 194 | -------------------------------------------------------------------------------- /api/instance/v1/server_utils_test.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 11 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers/httprecorder" 12 | "github.com/scaleway/scaleway-sdk-go/namegenerator" 13 | "github.com/scaleway/scaleway-sdk-go/scw" 14 | ) 15 | 16 | func TestAPI_GetServerType(t *testing.T) { 17 | client, r, err := httprecorder.CreateRecordedScwClient("get-server-type") 18 | testhelpers.AssertNoError(t, err) 19 | defer func() { 20 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 21 | }() 22 | 23 | instanceAPI := NewAPI(client) 24 | 25 | serverType, err := instanceAPI.GetServerType(&GetServerTypeRequest{ 26 | Zone: scw.ZoneFrPar1, 27 | Name: "GP1-XS", 28 | }) 29 | 30 | testhelpers.AssertNoError(t, err) 31 | testhelpers.Equals(t, 1*scw.GB, serverType.PerVolumeConstraint.LSSD.MinSize) 32 | testhelpers.Equals(t, 800*scw.GB, serverType.PerVolumeConstraint.LSSD.MaxSize) 33 | 34 | } 35 | 36 | func TestAPI_ServerUserData(t *testing.T) { 37 | client, r, err := httprecorder.CreateRecordedScwClient("server-user-data") 38 | testhelpers.AssertNoError(t, err) 39 | defer func() { 40 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 41 | }() 42 | 43 | instanceAPI := NewAPI(client) 44 | 45 | key := "hello" 46 | contentStr := "world" 47 | 48 | serverRes, err := instanceAPI.CreateServer(&CreateServerRequest{ 49 | Zone: scw.ZoneFrPar1, 50 | CommercialType: "DEV1-S", 51 | Name: namegenerator.GetRandomName("srv"), 52 | Image: "f974feac-abae-4365-b988-8ec7d1cec10d", 53 | Organization: "14d2f7ae-9775-414c-9bed-6810e060d500", 54 | }) 55 | testhelpers.AssertNoError(t, err) 56 | 57 | content := strings.NewReader(contentStr) 58 | err = instanceAPI.SetServerUserData(&SetServerUserDataRequest{ 59 | Zone: scw.ZoneFrPar1, 60 | ServerID: serverRes.Server.ID, 61 | Key: key, 62 | Content: content, 63 | }) 64 | testhelpers.AssertNoError(t, err) 65 | 66 | data, err := instanceAPI.GetServerUserData(&GetServerUserDataRequest{ 67 | Zone: scw.ZoneFrPar1, 68 | ServerID: serverRes.Server.ID, 69 | Key: key, 70 | }) 71 | testhelpers.AssertNoError(t, err) 72 | 73 | resUserData, err := ioutil.ReadAll(data) 74 | testhelpers.AssertNoError(t, err) 75 | testhelpers.Equals(t, contentStr, string(resUserData)) 76 | } 77 | 78 | func TestAPI_AllServerUserData(t *testing.T) { 79 | client, r, err := httprecorder.CreateRecordedScwClient("all-server-user-data") 80 | testhelpers.AssertNoError(t, err) 81 | defer func() { 82 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 83 | }() 84 | 85 | instanceAPI := NewAPI(client) 86 | 87 | serverRes, err := instanceAPI.CreateServer(&CreateServerRequest{ 88 | Zone: scw.ZoneFrPar1, 89 | CommercialType: "DEV1-S", 90 | Name: namegenerator.GetRandomName("srv"), 91 | Image: "f974feac-abae-4365-b988-8ec7d1cec10d", 92 | Organization: "14d2f7ae-9775-414c-9bed-6810e060d500", 93 | }) 94 | testhelpers.AssertNoError(t, err) 95 | 96 | steps := []map[string]string{ 97 | { 98 | "hello": "world", 99 | "scale": "way", 100 | "xavier": "niel", 101 | "tic": "tac", 102 | "cloud-init": "on", 103 | }, 104 | { 105 | "xavier": "niel", 106 | "scale": "way", 107 | "steve": "wozniak", 108 | "cloud-init": "off", 109 | }, 110 | {}, 111 | } 112 | 113 | for _, data := range steps { 114 | // create user data 115 | userData := make(map[string]io.Reader, len(data)) 116 | for k, v := range data { 117 | userData[k] = bytes.NewBufferString(v) 118 | } 119 | 120 | // set all user data 121 | err := instanceAPI.SetAllServerUserData(&SetAllServerUserDataRequest{ 122 | Zone: scw.ZoneFrPar1, 123 | ServerID: serverRes.Server.ID, 124 | UserData: userData, 125 | }) 126 | testhelpers.AssertNoError(t, err) 127 | 128 | // get all user data 129 | allData, err := instanceAPI.GetAllServerUserData(&GetAllServerUserDataRequest{ 130 | Zone: scw.ZoneFrPar1, 131 | ServerID: serverRes.Server.ID, 132 | }) 133 | testhelpers.AssertNoError(t, err) 134 | 135 | testhelpers.Equals(t, len(data), len(allData.UserData)) 136 | 137 | for expectedKey, expectedValue := range data { 138 | currentReader, exists := allData.UserData[expectedKey] 139 | testhelpers.Assert(t, exists, "%s key not found in result", expectedKey) 140 | 141 | currentValue, err := ioutil.ReadAll(currentReader) 142 | testhelpers.AssertNoError(t, err) 143 | 144 | testhelpers.Equals(t, expectedValue, string(currentValue)) 145 | } 146 | } 147 | } 148 | 149 | func TestAPI_CreateServer(t *testing.T) { 150 | client, r, err := httprecorder.CreateRecordedScwClient("create-server") 151 | testhelpers.AssertNoError(t, err) 152 | defer func() { 153 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 154 | }() 155 | 156 | instanceAPI := NewAPI(client) 157 | 158 | res, err := instanceAPI.CreateServer(&CreateServerRequest{ 159 | Zone: scw.ZoneFrPar1, 160 | CommercialType: "GP1-XS", 161 | Image: "ubuntu-bionic", 162 | }) 163 | 164 | testhelpers.AssertNoError(t, err) 165 | // this UUID might change when running the cassette later when the image "ubuntu-bionic" got a new version 166 | testhelpers.Equals(t, "f974feac-abae-4365-b988-8ec7d1cec10d", res.Server.Image.ID) 167 | err = instanceAPI.DeleteServer(&DeleteServerRequest{ 168 | Zone: scw.ZoneFrPar1, 169 | ServerID: res.Server.ID, 170 | }) 171 | testhelpers.AssertNoError(t, err) 172 | } 173 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/yamlprivateh.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | const ( 4 | // The size of the input raw buffer. 5 | input_raw_buffer_size = 512 6 | 7 | // The size of the input buffer. 8 | // It should be possible to decode the whole raw buffer. 9 | input_buffer_size = input_raw_buffer_size * 3 10 | 11 | // The size of the output buffer. 12 | output_buffer_size = 128 13 | 14 | // The size of the output raw buffer. 15 | // It should be possible to encode the whole output buffer. 16 | output_raw_buffer_size = (output_buffer_size*2 + 2) 17 | 18 | // The size of other stacks and queues. 19 | initial_stack_size = 16 20 | initial_queue_size = 16 21 | initial_string_size = 16 22 | ) 23 | 24 | // Check if the character at the specified position is an alphabetical 25 | // character, a digit, '_', or '-'. 26 | func is_alpha(b []byte, i int) bool { 27 | return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-' 28 | } 29 | 30 | // Check if the character at the specified position is a digit. 31 | func is_digit(b []byte, i int) bool { 32 | return b[i] >= '0' && b[i] <= '9' 33 | } 34 | 35 | // Get the value of a digit. 36 | func as_digit(b []byte, i int) int { 37 | return int(b[i]) - '0' 38 | } 39 | 40 | // Check if the character at the specified position is a hex-digit. 41 | func is_hex(b []byte, i int) bool { 42 | return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || b[i] >= 'a' && b[i] <= 'f' 43 | } 44 | 45 | // Get the value of a hex-digit. 46 | func as_hex(b []byte, i int) int { 47 | bi := b[i] 48 | if bi >= 'A' && bi <= 'F' { 49 | return int(bi) - 'A' + 10 50 | } 51 | if bi >= 'a' && bi <= 'f' { 52 | return int(bi) - 'a' + 10 53 | } 54 | return int(bi) - '0' 55 | } 56 | 57 | // Check if the character is ASCII. 58 | func is_ascii(b []byte, i int) bool { 59 | return b[i] <= 0x7F 60 | } 61 | 62 | // Check if the character at the start of the buffer can be printed unescaped. 63 | func is_printable(b []byte, i int) bool { 64 | return ((b[i] == 0x0A) || // . == #x0A 65 | (b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E 66 | (b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF 67 | (b[i] > 0xC2 && b[i] < 0xED) || 68 | (b[i] == 0xED && b[i+1] < 0xA0) || 69 | (b[i] == 0xEE) || 70 | (b[i] == 0xEF && // #xE000 <= . <= #xFFFD 71 | !(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF 72 | !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) 73 | } 74 | 75 | // Check if the character at the specified position is NUL. 76 | func is_z(b []byte, i int) bool { 77 | return b[i] == 0x00 78 | } 79 | 80 | // Check if the beginning of the buffer is a BOM. 81 | func is_bom(b []byte, i int) bool { 82 | return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF 83 | } 84 | 85 | // Check if the character at the specified position is space. 86 | func is_space(b []byte, i int) bool { 87 | return b[i] == ' ' 88 | } 89 | 90 | // Check if the character at the specified position is tab. 91 | func is_tab(b []byte, i int) bool { 92 | return b[i] == '\t' 93 | } 94 | 95 | // Check if the character at the specified position is blank (space or tab). 96 | func is_blank(b []byte, i int) bool { 97 | //return is_space(b, i) || is_tab(b, i) 98 | return b[i] == ' ' || b[i] == '\t' 99 | } 100 | 101 | // Check if the character at the specified position is a line break. 102 | func is_break(b []byte, i int) bool { 103 | return (b[i] == '\r' || // CR (#xD) 104 | b[i] == '\n' || // LF (#xA) 105 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 106 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 107 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029) 108 | } 109 | 110 | func is_crlf(b []byte, i int) bool { 111 | return b[i] == '\r' && b[i+1] == '\n' 112 | } 113 | 114 | // Check if the character is a line break or NUL. 115 | func is_breakz(b []byte, i int) bool { 116 | //return is_break(b, i) || is_z(b, i) 117 | return ( // is_break: 118 | b[i] == '\r' || // CR (#xD) 119 | b[i] == '\n' || // LF (#xA) 120 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 121 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 122 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) 123 | // is_z: 124 | b[i] == 0) 125 | } 126 | 127 | // Check if the character is a line break, space, or NUL. 128 | func is_spacez(b []byte, i int) bool { 129 | //return is_space(b, i) || is_breakz(b, i) 130 | return ( // is_space: 131 | b[i] == ' ' || 132 | // is_breakz: 133 | b[i] == '\r' || // CR (#xD) 134 | b[i] == '\n' || // LF (#xA) 135 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 136 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 137 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) 138 | b[i] == 0) 139 | } 140 | 141 | // Check if the character is a line break, space, tab, or NUL. 142 | func is_blankz(b []byte, i int) bool { 143 | //return is_blank(b, i) || is_breakz(b, i) 144 | return ( // is_blank: 145 | b[i] == ' ' || b[i] == '\t' || 146 | // is_breakz: 147 | b[i] == '\r' || // CR (#xD) 148 | b[i] == '\n' || // LF (#xA) 149 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 150 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 151 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) 152 | b[i] == 0) 153 | } 154 | 155 | // Determine the width of the character. 156 | func width(b byte) int { 157 | // Don't replace these by a switch without first 158 | // confirming that it is being inlined. 159 | if b&0x80 == 0x00 { 160 | return 1 161 | } 162 | if b&0xE0 == 0xC0 { 163 | return 2 164 | } 165 | if b&0xF0 == 0xE0 { 166 | return 3 167 | } 168 | if b&0xF8 == 0xF0 { 169 | return 4 170 | } 171 | return 0 172 | 173 | } 174 | -------------------------------------------------------------------------------- /api/instance/v1/instance_sdk_server_test.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers/httprecorder" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | ) 10 | 11 | func TestServerUpdate(t *testing.T) { 12 | 13 | client, r, err := httprecorder.CreateRecordedScwClient("server-test") 14 | testhelpers.AssertNoError(t, err) 15 | defer func() { 16 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 17 | }() 18 | 19 | instanceAPI := NewAPI(client) 20 | 21 | var ( 22 | serverID string 23 | volumeID string 24 | zone = scw.ZoneFrPar1 25 | name = "instance_sdk_server_test" 26 | dynamicIPRequired = scw.BoolPtr(true) 27 | commercialType = "START1-S" 28 | image = "f974feac-abae-4365-b988-8ec7d1cec10d" 29 | enableIPv6 = true 30 | bootType = ServerBootTypeLocal 31 | tags = []string{"foo", "bar"} 32 | organization = "d429f6a1-c0a6-48cf-8b5a-1f9dfe76ffd3" 33 | ) 34 | 35 | t.Run("create server", func(t *testing.T) { 36 | // Create server 37 | createServerResponse, err := instanceAPI.CreateServer(&CreateServerRequest{ 38 | Zone: zone, 39 | Name: name, 40 | Organization: organization, 41 | Image: image, 42 | EnableIPv6: enableIPv6, 43 | CommercialType: commercialType, 44 | Tags: tags, 45 | DynamicIPRequired: dynamicIPRequired, 46 | }) 47 | testhelpers.AssertNoError(t, err) 48 | serverID = createServerResponse.Server.ID 49 | 50 | testhelpers.Assert(t, createServerResponse.Server != nil, "Should have server in response") 51 | testhelpers.Assert(t, 1 == len(createServerResponse.Server.Volumes), "should have exactly one volume because we didn't pass volumes map in the requests.") 52 | for _, volume := range createServerResponse.Server.Volumes { 53 | volumeID = volume.ID 54 | } 55 | 56 | testhelpers.Equals(t, name, createServerResponse.Server.Name) 57 | testhelpers.Equals(t, organization, createServerResponse.Server.Organization) 58 | testhelpers.Equals(t, image, createServerResponse.Server.Image.ID) 59 | testhelpers.Equals(t, enableIPv6, createServerResponse.Server.EnableIPv6) 60 | testhelpers.Equals(t, bootType, createServerResponse.Server.BootType) 61 | testhelpers.Equals(t, commercialType, createServerResponse.Server.CommercialType) 62 | testhelpers.Equals(t, tags, createServerResponse.Server.Tags) 63 | testhelpers.Equals(t, *dynamicIPRequired, createServerResponse.Server.DynamicIPRequired) 64 | }) 65 | 66 | t.Run("update server", func(t *testing.T) { 67 | var ( 68 | newName = "some_new_name_for_server" 69 | updatedTags = []string{} 70 | ) 71 | 72 | // Update server 73 | updateServerResponse, err := instanceAPI.updateServer((*updateServerRequest)(&UpdateServerRequest{ 74 | ServerID: serverID, 75 | Zone: zone, 76 | Name: &newName, 77 | Tags: &updatedTags, 78 | })) 79 | testhelpers.Assert(t, updateServerResponse.Server != nil, "Should have server in response") 80 | testhelpers.AssertNoError(t, err) 81 | 82 | // Initial values that are not altered in the above request should remaing the same 83 | testhelpers.Equals(t, organization, updateServerResponse.Server.Organization) 84 | testhelpers.Equals(t, image, updateServerResponse.Server.Image.ID) 85 | testhelpers.Equals(t, enableIPv6, updateServerResponse.Server.EnableIPv6) 86 | testhelpers.Equals(t, bootType, updateServerResponse.Server.BootType) 87 | testhelpers.Equals(t, commercialType, updateServerResponse.Server.CommercialType) 88 | testhelpers.Equals(t, *dynamicIPRequired, updateServerResponse.Server.DynamicIPRequired) 89 | testhelpers.Assert(t, 1 == len(updateServerResponse.Server.Volumes), "should have exactly one volume because we didn't pass volumes map in the requests.") 90 | 91 | testhelpers.Equals(t, newName, updateServerResponse.Server.Name) 92 | testhelpers.Equals(t, updatedTags, updateServerResponse.Server.Tags) 93 | }) 94 | 95 | t.Run("remove server volumes", func(t *testing.T) { 96 | // Remove/detach volumes 97 | updateServerResponse, err := instanceAPI.updateServer((*updateServerRequest)(&UpdateServerRequest{ 98 | ServerID: serverID, 99 | Zone: zone, 100 | Volumes: &map[string]*VolumeTemplate{}, 101 | })) 102 | testhelpers.AssertNoError(t, err) 103 | testhelpers.Assert(t, updateServerResponse.Server != nil, "Should have server in response") 104 | testhelpers.Assert(t, 0 == len(updateServerResponse.Server.Volumes), "volume should be detached from server.") 105 | }) 106 | 107 | t.Run("cleanup server and volume", func(t *testing.T) { 108 | // Delete Server 109 | err = instanceAPI.DeleteServer(&DeleteServerRequest{ 110 | Zone: zone, 111 | ServerID: serverID, 112 | }) 113 | testhelpers.AssertNoError(t, err) 114 | 115 | // Delete Volume 116 | err = instanceAPI.DeleteVolume(&DeleteVolumeRequest{ 117 | Zone: zone, 118 | VolumeID: volumeID, 119 | }) 120 | testhelpers.AssertNoError(t, err) 121 | }) 122 | 123 | } 124 | 125 | func TestCreateServerWithIncorrectBody(t *testing.T) { 126 | 127 | client, r, err := httprecorder.CreateRecordedScwClient("server-incorrect-body") 128 | testhelpers.AssertNoError(t, err) 129 | defer func() { 130 | testhelpers.AssertNoError(t, r.Stop()) // Make sure recorder is stopped once done with it 131 | }() 132 | 133 | instanceAPI := NewAPI(client) 134 | 135 | var ( 136 | zone = scw.ZoneFrPar1 137 | ) 138 | 139 | // Create server 140 | _, err = instanceAPI.CreateServer(&CreateServerRequest{ 141 | Zone: zone, 142 | }) 143 | testhelpers.Assert(t, err != nil, "This request should error") 144 | 145 | } 146 | -------------------------------------------------------------------------------- /api/instance/v1/testdata/security-group-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: '{"name":"name","description":"description","organization":"b3ba839a-dcf2-4b0a-ac81-fc32370052a0","stateful":true,"inbound_default_policy":"accept","outbound_default_policy":"drop"}' 6 | form: {} 7 | headers: 8 | Content-Type: 9 | - application/json 10 | User-Agent: 11 | - scaleway-sdk-go/0.0.0 (go1.12.6; darwin; amd64) 12 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/security_groups 13 | method: POST 14 | response: 15 | body: '{"security_group": {"outbound_default_policy": "drop", "description": "description", 16 | "servers": [], "stateful": true, "creation_date": "2019-06-19T14:14:55.331499+00:00", 17 | "id": "7f19c115-fc6e-4cbb-b7f8-e97fc2cfa0b2", "name": "name", "modification_date": 18 | "2019-06-19T14:14:55.331499+00:00", "inbound_default_policy": "accept", "organization": 19 | "b3ba839a-dcf2-4b0a-ac81-fc32370052a0", "organization_default": false, "enable_default_security": 20 | true}}' 21 | headers: 22 | Cache-Control: 23 | - no-cache 24 | Content-Length: 25 | - "446" 26 | Content-Security-Policy: 27 | - default-src 'none'; frame-ancestors 'none' 28 | Content-Type: 29 | - application/json 30 | Date: 31 | - Wed, 19 Jun 2019 14:14:55 GMT 32 | Location: 33 | - https://cp-par1.scaleway.com/security_groups/7f19c115-fc6e-4cbb-b7f8-e97fc2cfa0b2 34 | Server: 35 | - scaleway_api 36 | Strict-Transport-Security: 37 | - max-age=63072000 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Frame-Options: 41 | - DENY 42 | status: 201 Created 43 | code: 201 44 | duration: "" 45 | - request: 46 | body: "" 47 | form: {} 48 | headers: 49 | User-Agent: 50 | - scaleway-sdk-go/0.0.0 (go1.12.6; darwin; amd64) 51 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/security_groups/7f19c115-fc6e-4cbb-b7f8-e97fc2cfa0b2 52 | method: GET 53 | response: 54 | body: '{"security_group": {"outbound_default_policy": "drop", "description": "description", 55 | "servers": [], "stateful": true, "creation_date": "2019-06-19T14:14:55.331499+00:00", 56 | "id": "7f19c115-fc6e-4cbb-b7f8-e97fc2cfa0b2", "name": "name", "modification_date": 57 | "2019-06-19T14:14:55.331499+00:00", "inbound_default_policy": "accept", "organization": 58 | "b3ba839a-dcf2-4b0a-ac81-fc32370052a0", "organization_default": false, "enable_default_security": 59 | true}}' 60 | headers: 61 | Cache-Control: 62 | - no-cache 63 | Content-Length: 64 | - "446" 65 | Content-Security-Policy: 66 | - default-src 'none'; frame-ancestors 'none' 67 | Content-Type: 68 | - application/json 69 | Date: 70 | - Wed, 19 Jun 2019 14:14:55 GMT 71 | Server: 72 | - scaleway_api 73 | Strict-Transport-Security: 74 | - max-age=63072000 75 | X-Content-Type-Options: 76 | - nosniff 77 | X-Frame-Options: 78 | - DENY 79 | status: 200 OK 80 | code: 200 81 | duration: "" 82 | - request: 83 | body: '{"name":"new_name","description":"new_description","enable_default_security":true,"inbound_default_policy":"drop","outbound_default_policy":"accept","organization":"b3ba839a-dcf2-4b0a-ac81-fc32370052a0","organization_default":false,"creation_date":"2019-06-19T14:14:55.331499Z","modification_date":"2019-06-19T14:14:55.331499Z","servers":[],"stateful":false}' 84 | form: {} 85 | headers: 86 | Content-Type: 87 | - application/json 88 | User-Agent: 89 | - scaleway-sdk-go/0.0.0 (go1.12.6; darwin; amd64) 90 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/security_groups/7f19c115-fc6e-4cbb-b7f8-e97fc2cfa0b2 91 | method: PUT 92 | response: 93 | body: '{"security_group": {"outbound_default_policy": "accept", "description": 94 | "new_description", "servers": [], "stateful": false, "creation_date": "2019-06-19T14:14:55.331499+00:00", 95 | "id": "7f19c115-fc6e-4cbb-b7f8-e97fc2cfa0b2", "name": "new_name", "modification_date": 96 | "2019-06-19T14:14:55.785078+00:00", "inbound_default_policy": "drop", "organization": 97 | "b3ba839a-dcf2-4b0a-ac81-fc32370052a0", "organization_default": false, "enable_default_security": 98 | true}}' 99 | headers: 100 | Cache-Control: 101 | - no-cache 102 | Content-Length: 103 | - "455" 104 | Content-Security-Policy: 105 | - default-src 'none'; frame-ancestors 'none' 106 | Content-Type: 107 | - application/json 108 | Date: 109 | - Wed, 19 Jun 2019 14:14:56 GMT 110 | Server: 111 | - scaleway_api 112 | Strict-Transport-Security: 113 | - max-age=63072000 114 | X-Content-Type-Options: 115 | - nosniff 116 | X-Frame-Options: 117 | - DENY 118 | status: 200 OK 119 | code: 200 120 | duration: "" 121 | - request: 122 | body: "" 123 | form: {} 124 | headers: 125 | User-Agent: 126 | - scaleway-sdk-go/0.0.0 (go1.12.6; darwin; amd64) 127 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/security_groups/7f19c115-fc6e-4cbb-b7f8-e97fc2cfa0b2 128 | method: DELETE 129 | response: 130 | body: "" 131 | headers: 132 | Cache-Control: 133 | - no-cache 134 | Content-Security-Policy: 135 | - default-src 'none'; frame-ancestors 'none' 136 | Content-Type: 137 | - application/json 138 | Date: 139 | - Wed, 19 Jun 2019 14:14:56 GMT 140 | Server: 141 | - scaleway_api 142 | Strict-Transport-Security: 143 | - max-age=63072000 144 | X-Content-Type-Options: 145 | - nosniff 146 | X-Frame-Options: 147 | - DENY 148 | status: 204 No Content 149 | code: 204 150 | duration: "" 151 | -------------------------------------------------------------------------------- /scw/load_config_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 11 | ) 12 | 13 | // TestLoad tests all valid configuration files: 14 | // - v2 config 15 | // - v1 to v2 config migration 16 | // - custom-path with v2 config 17 | // - custom-path with v1 config 18 | // - XDG config path with v2 config 19 | // - Windows config path with v2 config 20 | func TestLoad(t *testing.T) { 21 | tests := []struct { 22 | name string 23 | env map[string]string 24 | files map[string]string 25 | expected *Config 26 | expectedError string 27 | expectedFiles map[string]string 28 | }{ 29 | // valid config 30 | { 31 | name: "Custom-path config is empty", // custom config path 32 | env: map[string]string{ 33 | scwConfigPathEnv: "{HOME}/valid1/test.conf", 34 | }, 35 | files: map[string]string{ 36 | "valid1/test.conf": emptyFile, 37 | }, 38 | expected: &Config{}, 39 | }, 40 | { 41 | name: "Custom-path config with valid V2", 42 | env: map[string]string{ 43 | scwConfigPathEnv: "{HOME}/valid3/test.conf", 44 | }, 45 | files: map[string]string{ 46 | "valid3/test.conf": v2SimpleValidConfigFile, 47 | }, 48 | expected: v2SimpleValidConfig, 49 | }, 50 | { 51 | name: "Default config with valid V2", // default config path 52 | env: map[string]string{ 53 | "HOME": "{HOME}", 54 | }, 55 | files: map[string]string{ 56 | ".config/scw/config.yaml": v2SimpleValidConfigFile, 57 | }, 58 | expected: v2SimpleValidConfig, 59 | expectedFiles: map[string]string{ 60 | ".config/scw/config.yaml": v2SimpleValidConfigFile, 61 | }, 62 | }, 63 | { 64 | name: "Default config with valid V2 and valid V1", 65 | env: map[string]string{ 66 | "HOME": "{HOME}", 67 | }, 68 | files: map[string]string{ 69 | ".config/scw/config.yaml": v2SimpleValidConfigFile, 70 | ".scwrc": v1ValidConfigFile, 71 | }, 72 | expected: v2SimpleValidConfig, 73 | expectedFiles: map[string]string{ 74 | ".config/scw/config.yaml": v2SimpleValidConfigFile, 75 | }, 76 | }, 77 | { 78 | name: "XDG config with valid V2", 79 | env: map[string]string{ 80 | "HOME": "{HOME}", 81 | xdgConfigDirEnv: "{HOME}/plop", 82 | }, 83 | files: map[string]string{ 84 | "plop/scw/config.yaml": v2SimpleValidConfigFile, 85 | }, 86 | expected: v2SimpleValidConfig, 87 | }, 88 | { 89 | name: "Windows config with valid V2", 90 | env: map[string]string{ 91 | windowsHomeDirEnv: "{HOME}", 92 | }, 93 | files: map[string]string{ 94 | ".config/scw/config.yaml": v2SimpleValidConfigFile, 95 | }, 96 | expected: v2SimpleValidConfig, 97 | }, 98 | 99 | // errors 100 | { 101 | name: "Err: custom-path config does not exist", 102 | env: map[string]string{ 103 | scwConfigPathEnv: "{HOME}/fake/test.conf", 104 | }, 105 | expectedError: "scaleway-sdk-go: cannot read config file: open {HOME}/fake/test.conf: no such file or directory", 106 | }, 107 | { 108 | name: "Err: custom-path config with invalid V2", 109 | env: map[string]string{ 110 | scwConfigPathEnv: "{HOME}/invalid1/test.conf", 111 | }, 112 | files: map[string]string{ 113 | "invalid1/test.conf": v2SimpleInvalidConfigFile, 114 | }, 115 | expectedError: "scaleway-sdk-go: content of config file {HOME}/invalid1/test.conf is invalid: yaml: found unexpected end of stream", 116 | }, 117 | { 118 | name: "Err: default config with invalid V2", 119 | env: map[string]string{ 120 | "HOME": "{HOME}", 121 | }, 122 | files: map[string]string{ 123 | ".config/scw/config.yaml": v2SimpleInvalidConfigFile, 124 | }, 125 | expectedError: "scaleway-sdk-go: content of config file {HOME}/.config/scw/config.yaml is invalid: yaml: found unexpected end of stream", 126 | }, 127 | { 128 | name: "Err: default config with invalid profile", 129 | env: map[string]string{ 130 | "HOME": "{HOME}", 131 | }, 132 | files: map[string]string{ 133 | ".config/scw/config.yaml": v2SimpleConfigFileWithInvalidProfile, 134 | }, 135 | expectedError: "scaleway-sdk-go: given profile flantier does not exist", 136 | }, 137 | } 138 | 139 | // create home dir 140 | dir := initEnv(t) 141 | 142 | // delete home dir and reset env variables 143 | defer resetEnv(t, os.Environ(), dir) 144 | 145 | for _, test := range tests { 146 | t.Run(test.name, func(t *testing.T) { 147 | // set up env and config file(s) 148 | setEnv(t, test.env, test.files, dir) 149 | test.expectedError = strings.Replace(test.expectedError, "{HOME}", dir, -1) 150 | 151 | // remove config file(s) 152 | defer cleanEnv(t, test.files, dir) 153 | 154 | // load config 155 | config, err := LoadConfig() 156 | 157 | // test expected outputs 158 | if test.expectedError != "" { 159 | if err == nil { 160 | _, tmpErr := config.GetActiveProfile() 161 | if tmpErr != nil { 162 | testhelpers.Equals(t, test.expectedError, tmpErr.Error()) 163 | return 164 | } 165 | } 166 | testhelpers.Assert(t, err != nil, "error should not be nil") 167 | testhelpers.Equals(t, test.expectedError, err.Error()) 168 | } else { 169 | testhelpers.AssertNoError(t, err) 170 | testhelpers.Equals(t, test.expected, config) 171 | 172 | _, err = config.GetActiveProfile() 173 | testhelpers.AssertNoError(t, err) 174 | } 175 | 176 | // test expected files 177 | for path, expectedContent := range test.expectedFiles { 178 | targetPath := filepath.Join(dir, path) 179 | content, err := ioutil.ReadFile(targetPath) 180 | testhelpers.AssertNoError(t, err) 181 | testhelpers.Equals(t, expectedContent, string(content)) 182 | testhelpers.AssertNoError(t, os.RemoveAll(targetPath)) // delete at the end 183 | } 184 | }) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /api/instance/v1/testdata/server-user-data.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: '{"name":"srv-wonderful-goldberg","commercial_type":"DEV1-S","image":"f974feac-abae-4365-b988-8ec7d1cec10d","organization":"14d2f7ae-9775-414c-9bed-6810e060d500"}' 6 | form: {} 7 | headers: 8 | Content-Type: 9 | - application/json 10 | User-Agent: 11 | - scaleway-sdk-go/0.0.0 (go1.12.5; darwin; amd64) 12 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/servers 13 | method: POST 14 | response: 15 | body: '{"server": {"allowed_actions": ["poweron", "backup"], "maintenances": [], 16 | "state_detail": "", "image": {"creation_date": "2019-03-05T10:13:15.974944+00:00", 17 | "default_bootscript": {"kernel": "http://169.254.42.24/kernel/x86_64-mainline-lts-4.9-4.9.93-rev1/vmlinuz-4.9.93", 18 | "initrd": "http://169.254.42.24/initrd/initrd-Linux-x86_64-v3.14.6.gz", "default": 19 | false, "bootcmdargs": "LINUX_COMMON scaleway boot=local nbd.max_part=16", "architecture": 20 | "x86_64", "title": "x86_64 mainline 4.9.93 rev1", "dtb": "", "organization": 21 | "11111111-1111-4111-8111-111111111111", "id": "15fbd2f7-a0f9-412b-8502-6a44da8d98b8", 22 | "public": false}, "from_server": null, "arch": "x86_64", "id": "f974feac-abae-4365-b988-8ec7d1cec10d", 23 | "root_volume": {"size": 10000000000, "id": "dd5f5c10-23b1-4c9c-8445-eb6740957c84", 24 | "volume_type": "l_ssd", "name": "snapshot-de728daa-0bf6-4c64-abf5-a9477e791c83-2019-03-05_10:13"}, 25 | "name": "Ubuntu Bionic Beaver", "modification_date": "2019-03-05T13:32:29.274319+00:00", 26 | "state": "available", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", 27 | "extra_volumes": {}, "public": true}, "creation_date": "2019-06-20T17:59:31.822202+00:00", 28 | "public_ip": null, "private_ip": null, "id": "d7b6fd2a-32f9-446f-ba49-6a67be590a07", 29 | "dynamic_ip_required": true, "modification_date": "2019-06-20T17:59:32.192591+00:00", 30 | "enable_ipv6": false, "hostname": "srv-wonderful-goldberg", "state": "stopped", 31 | "bootscript": {"kernel": "http://169.254.42.24/kernel/x86_64-mainline-lts-4.9-4.9.93-rev1/vmlinuz-4.9.93", 32 | "initrd": "http://169.254.42.24/initrd/initrd-Linux-x86_64-v3.14.6.gz", "default": 33 | false, "bootcmdargs": "LINUX_COMMON scaleway boot=local nbd.max_part=16", "architecture": 34 | "x86_64", "title": "x86_64 mainline 4.9.93 rev1", "dtb": "", "organization": 35 | "11111111-1111-4111-8111-111111111111", "id": "15fbd2f7-a0f9-412b-8502-6a44da8d98b8", 36 | "public": false}, "location": null, "boot_type": "local", "ipv6": null, "commercial_type": 37 | "DEV1-S", "tags": [], "arch": "x86_64", "extra_networks": [], "compute_cluster": 38 | null, "name": "srv-wonderful-goldberg", "protected": false, "volumes": {"0": 39 | {"size": 20000000000, "state": "available", "name": "snapshot-de728daa-0bf6-4c64-abf5-a9477e791c83-2019-03-05_10:13", 40 | "modification_date": "2019-06-20T17:59:32.094121+00:00", "organization": "14d2f7ae-9775-414c-9bed-6810e060d500", 41 | "export_uri": null, "creation_date": "2019-06-20T17:59:31.822202+00:00", "id": 42 | "fe034b14-2c3f-4f30-96f8-d695f24aa6de", "volume_type": "l_ssd", "server": {"id": 43 | "d7b6fd2a-32f9-446f-ba49-6a67be590a07", "name": "srv-wonderful-goldberg"}}}, 44 | "security_group": {"id": "e5bf4522-94b4-4933-bebb-9b21f786b4af", "name": "Default 45 | security group"}, "organization": "14d2f7ae-9775-414c-9bed-6810e060d500"}}' 46 | headers: 47 | Cache-Control: 48 | - no-cache 49 | Content-Length: 50 | - "2718" 51 | Content-Security-Policy: 52 | - default-src 'none'; frame-ancestors 'none' 53 | Content-Type: 54 | - application/json 55 | Date: 56 | - Thu, 20 Jun 2019 17:59:32 GMT 57 | Location: 58 | - https://cp-par1.scaleway.com/servers/d7b6fd2a-32f9-446f-ba49-6a67be590a07 59 | Server: 60 | - scaleway_api 61 | Strict-Transport-Security: 62 | - max-age=63072000 63 | X-Content-Type-Options: 64 | - nosniff 65 | X-Frame-Options: 66 | - DENY 67 | status: 201 Created 68 | code: 201 69 | duration: "" 70 | - request: 71 | body: world 72 | form: {} 73 | headers: 74 | Content-Type: 75 | - text/plain 76 | User-Agent: 77 | - scaleway-sdk-go/0.0.0 (go1.12.5; darwin; amd64) 78 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/servers/d7b6fd2a-32f9-446f-ba49-6a67be590a07/user_data/hello 79 | method: PATCH 80 | response: 81 | body: "" 82 | headers: 83 | Cache-Control: 84 | - no-cache 85 | Content-Security-Policy: 86 | - default-src 'none'; frame-ancestors 'none' 87 | Content-Type: 88 | - text/plain 89 | Date: 90 | - Thu, 20 Jun 2019 17:59:32 GMT 91 | Server: 92 | - scaleway_api 93 | Strict-Transport-Security: 94 | - max-age=63072000 95 | X-Content-Type-Options: 96 | - nosniff 97 | X-Frame-Options: 98 | - DENY 99 | status: 204 No Content 100 | code: 204 101 | duration: "" 102 | - request: 103 | body: "" 104 | form: {} 105 | headers: 106 | User-Agent: 107 | - scaleway-sdk-go/0.0.0 (go1.12.5; darwin; amd64) 108 | url: https://api.scaleway.com/instance/v1/zones/fr-par-1/servers/d7b6fd2a-32f9-446f-ba49-6a67be590a07/user_data/hello 109 | method: GET 110 | response: 111 | body: world 112 | headers: 113 | Cache-Control: 114 | - no-cache 115 | Content-Length: 116 | - "5" 117 | Content-Security-Policy: 118 | - default-src 'none'; frame-ancestors 'none' 119 | Content-Type: 120 | - text/plain 121 | Date: 122 | - Thu, 20 Jun 2019 17:59:32 GMT 123 | Server: 124 | - scaleway_api 125 | Strict-Transport-Security: 126 | - max-age=63072000 127 | X-Content-Type-Options: 128 | - nosniff 129 | X-Frame-Options: 130 | - DENY 131 | status: 200 OK 132 | code: 200 133 | duration: "" 134 | -------------------------------------------------------------------------------- /scw/client_option.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/internal/auth" 8 | "github.com/scaleway/scaleway-sdk-go/internal/errors" 9 | ) 10 | 11 | // ClientOption is a function which applies options to a settings object. 12 | type ClientOption func(*settings) 13 | 14 | // httpClient wraps the net/http Client Do method 15 | type httpClient interface { 16 | Do(*http.Request) (*http.Response, error) 17 | } 18 | 19 | // WithHTTPClient client option allows passing a custom http.Client which will be used for all requests. 20 | func WithHTTPClient(httpClient httpClient) ClientOption { 21 | return func(s *settings) { 22 | s.httpClient = httpClient 23 | } 24 | } 25 | 26 | // WithoutAuth client option sets the client token to an empty token. 27 | func WithoutAuth() ClientOption { 28 | return func(s *settings) { 29 | s.token = auth.NewNoAuth() 30 | } 31 | } 32 | 33 | // WithAuth client option sets the client access key and secret key. 34 | func WithAuth(accessKey, secretKey string) ClientOption { 35 | return func(s *settings) { 36 | s.token = auth.NewToken(accessKey, secretKey) 37 | } 38 | } 39 | 40 | // WithAPIURL client option overrides the API URL of the Scaleway API to the given URL. 41 | func WithAPIURL(apiURL string) ClientOption { 42 | return func(s *settings) { 43 | s.apiURL = apiURL 44 | } 45 | } 46 | 47 | // WithInsecure client option enables insecure transport on the client. 48 | func WithInsecure() ClientOption { 49 | return func(s *settings) { 50 | s.insecure = true 51 | } 52 | } 53 | 54 | // WithUserAgent client option append a user agent to the default user agent of the SDK. 55 | func WithUserAgent(ua string) ClientOption { 56 | return func(s *settings) { 57 | if s.userAgent != "" && ua != "" { 58 | s.userAgent += " " 59 | } 60 | s.userAgent += ua 61 | } 62 | } 63 | 64 | // withDefaultUserAgent client option overrides the default user agent of the SDK. 65 | func withDefaultUserAgent(ua string) ClientOption { 66 | return func(s *settings) { 67 | s.userAgent = ua 68 | } 69 | } 70 | 71 | // WithProfile client option configures a client from the given profile. 72 | func WithProfile(p *Profile) ClientOption { 73 | return func(s *settings) { 74 | accessKey := "" 75 | if p.AccessKey != nil { 76 | accessKey = *p.AccessKey 77 | } 78 | 79 | if p.SecretKey != nil { 80 | s.token = auth.NewToken(accessKey, *p.SecretKey) 81 | } 82 | 83 | if p.APIURL != nil { 84 | s.apiURL = *p.APIURL 85 | } 86 | 87 | if p.Insecure != nil { 88 | s.insecure = *p.Insecure 89 | } 90 | 91 | if p.DefaultOrganizationID != nil { 92 | organizationID := *p.DefaultOrganizationID 93 | s.defaultOrganizationID = &organizationID 94 | } 95 | 96 | if p.DefaultRegion != nil { 97 | defaultRegion := Region(*p.DefaultRegion) 98 | s.defaultRegion = &defaultRegion 99 | } 100 | 101 | if p.DefaultZone != nil { 102 | defaultZone := Zone(*p.DefaultZone) 103 | s.defaultZone = &defaultZone 104 | } 105 | } 106 | } 107 | 108 | // WithProfile client option configures a client from the environment variables. 109 | func WithEnv() ClientOption { 110 | return WithProfile(LoadEnvProfile()) 111 | } 112 | 113 | // WithDefaultOrganizationID client option sets the client default organization ID. 114 | // 115 | // It will be used as the default value of the organization_id field in all requests made with this client. 116 | func WithDefaultOrganizationID(organizationID string) ClientOption { 117 | return func(s *settings) { 118 | s.defaultOrganizationID = &organizationID 119 | } 120 | } 121 | 122 | // WithDefaultRegion client option sets the client default region. 123 | // 124 | // It will be used as the default value of the region field in all requests made with this client. 125 | func WithDefaultRegion(region Region) ClientOption { 126 | return func(s *settings) { 127 | s.defaultRegion = ®ion 128 | } 129 | } 130 | 131 | // WithDefaultZone client option sets the client default zone. 132 | // 133 | // It will be used as the default value of the zone field in all requests made with this client. 134 | func WithDefaultZone(zone Zone) ClientOption { 135 | return func(s *settings) { 136 | s.defaultZone = &zone 137 | } 138 | } 139 | 140 | // WithDefaultPageSize client option overrides the default page size of the SDK. 141 | // 142 | // It will be used as the default value of the page_size field in all requests made with this client. 143 | func WithDefaultPageSize(pageSize int32) ClientOption { 144 | return func(s *settings) { 145 | s.defaultPageSize = &pageSize 146 | } 147 | } 148 | 149 | // settings hold the values of all client options 150 | type settings struct { 151 | apiURL string 152 | token auth.Auth 153 | userAgent string 154 | httpClient httpClient 155 | insecure bool 156 | defaultOrganizationID *string 157 | defaultRegion *Region 158 | defaultZone *Zone 159 | defaultPageSize *int32 160 | } 161 | 162 | func newSettings() *settings { 163 | return &settings{} 164 | } 165 | 166 | func (s *settings) apply(opts []ClientOption) { 167 | for _, opt := range opts { 168 | opt(s) 169 | } 170 | } 171 | 172 | func (s *settings) validate() error { 173 | var err error 174 | if s.token == nil { 175 | return errors.New("no credential option provided") 176 | } 177 | 178 | _, err = url.Parse(s.apiURL) 179 | if err != nil { 180 | return errors.Wrap(err, "invalid url %s", s.apiURL) 181 | } 182 | 183 | // TODO: Check OrganizationID format 184 | if s.defaultOrganizationID != nil && *s.defaultOrganizationID == "" { 185 | return errors.New("default organization id cannot be empty") 186 | } 187 | 188 | // TODO: Check Region format 189 | if s.defaultRegion != nil && *s.defaultRegion == "" { 190 | return errors.New("default region cannot be empty") 191 | } 192 | 193 | // TODO: Check Zone format 194 | if s.defaultZone != nil && *s.defaultZone == "" { 195 | return errors.New("default zone cannot be empty") 196 | } 197 | 198 | if s.defaultPageSize != nil && *s.defaultPageSize <= 0 { 199 | return errors.New("default page size cannot be <= 0") 200 | } 201 | 202 | return nil 203 | } 204 | -------------------------------------------------------------------------------- /scw/client_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/scaleway/scaleway-sdk-go/internal/auth" 11 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 12 | "github.com/scaleway/scaleway-sdk-go/logger" 13 | ) 14 | 15 | const ( 16 | testAPIURL = "https://api.example.com/" 17 | defaultAPIURL = "https://api.scaleway.com" 18 | testAccessKey = "ACCESS_KEY" 19 | testSecretKey = "7363616c-6577-6573-6862-6f7579616161" // hint: | xxd -ps -r 20 | testDefaultOrganizationID = "6170692e-7363-616c-6577-61792e636f6d" // hint: | xxd -ps -r 21 | testDefaultRegion = RegionFrPar 22 | testDefaultZone = ZoneFrPar1 23 | testDefaultPageSize = int32(5) 24 | testInsecure = true 25 | ) 26 | 27 | func TestNewClientWithDefaults(t *testing.T) { 28 | 29 | options := []ClientOption{ 30 | WithoutAuth(), 31 | WithInsecure(), 32 | } 33 | 34 | client, err := NewClient(options...) 35 | testhelpers.AssertNoError(t, err) 36 | 37 | testhelpers.Equals(t, defaultAPIURL, client.apiURL) 38 | testhelpers.Equals(t, auth.NewNoAuth(), client.auth) 39 | 40 | } 41 | 42 | type mockConfig struct{} 43 | 44 | func (c *mockConfig) GetAccessKey() (string, bool) { 45 | return testAccessKey, true 46 | } 47 | func (c *mockConfig) GetSecretKey() (string, bool) { 48 | return testSecretKey, true 49 | } 50 | func (c *mockConfig) GetAPIURL() (string, bool) { 51 | return testAPIURL, true 52 | } 53 | func (c *mockConfig) GetInsecure() (bool, bool) { 54 | return testInsecure, true 55 | } 56 | func (c *mockConfig) GetDefaultOrganizationID() (string, bool) { 57 | return testDefaultOrganizationID, true 58 | } 59 | func (c *mockConfig) GetDefaultRegion() (Region, bool) { 60 | return testDefaultRegion, true 61 | } 62 | func (c *mockConfig) GetDefaultZone() (Zone, bool) { 63 | return testDefaultZone, true 64 | } 65 | 66 | func TestNewClientWithOptions(t *testing.T) { 67 | 68 | t.Run("Basic", func(t *testing.T) { 69 | someHTTPClient := &http.Client{} 70 | 71 | options := []ClientOption{ 72 | WithAPIURL(testAPIURL), 73 | WithAuth(testAccessKey, testSecretKey), 74 | WithHTTPClient(someHTTPClient), 75 | WithDefaultOrganizationID(testDefaultOrganizationID), 76 | WithDefaultRegion(testDefaultRegion), 77 | WithDefaultZone(testDefaultZone), 78 | WithDefaultPageSize(testDefaultPageSize), 79 | } 80 | 81 | client, err := NewClient(options...) 82 | testhelpers.AssertNoError(t, err) 83 | 84 | testhelpers.Equals(t, testAPIURL, client.apiURL) 85 | testhelpers.Equals(t, auth.NewToken(testAccessKey, testSecretKey), client.auth) 86 | 87 | testhelpers.Equals(t, someHTTPClient, client.httpClient) 88 | 89 | defaultOrganizationID, exist := client.GetDefaultOrganizationID() 90 | testhelpers.Equals(t, testDefaultOrganizationID, defaultOrganizationID) 91 | testhelpers.Assert(t, exist, "defaultOrganizationID must exist") 92 | 93 | defaultRegion, exist := client.GetDefaultRegion() 94 | testhelpers.Equals(t, testDefaultRegion, defaultRegion) 95 | testhelpers.Assert(t, exist, "defaultRegion must exist") 96 | 97 | defaultZone, exist := client.GetDefaultZone() 98 | testhelpers.Equals(t, testDefaultZone, defaultZone) 99 | testhelpers.Assert(t, exist, "defaultZone must exist") 100 | 101 | defaultPageSize, exist := client.GetDefaultPageSize() 102 | testhelpers.Equals(t, testDefaultPageSize, defaultPageSize) 103 | testhelpers.Assert(t, exist, "defaultPageSize must exist") 104 | }) 105 | 106 | t.Run("With custom profile", func(t *testing.T) { 107 | profile := &Profile{ 108 | s(testAccessKey), 109 | s(testSecretKey), 110 | s(testAPIURL), 111 | b(testInsecure), 112 | s(testDefaultOrganizationID), 113 | s(string(testDefaultRegion)), 114 | s(string(testDefaultZone)), 115 | } 116 | 117 | client, err := NewClient(WithProfile(profile)) 118 | testhelpers.AssertNoError(t, err) 119 | 120 | testhelpers.Equals(t, auth.NewToken(testAccessKey, testSecretKey), client.auth) 121 | testhelpers.Equals(t, testAPIURL, client.apiURL) 122 | 123 | clientTransport, ok := client.httpClient.(*http.Client).Transport.(*http.Transport) 124 | testhelpers.Assert(t, ok, "clientTransport must be not nil") 125 | testhelpers.Assert(t, clientTransport.TLSClientConfig != nil, "TLSClientConfig must be not nil") 126 | testhelpers.Equals(t, testInsecure, clientTransport.TLSClientConfig.InsecureSkipVerify) 127 | 128 | defaultOrganizationID, exist := client.GetDefaultOrganizationID() 129 | testhelpers.Equals(t, testDefaultOrganizationID, defaultOrganizationID) 130 | testhelpers.Assert(t, exist, "defaultOrganizationID must exist") 131 | 132 | defaultRegion, exist := client.GetDefaultRegion() 133 | testhelpers.Equals(t, testDefaultRegion, defaultRegion) 134 | testhelpers.Assert(t, exist, "defaultRegion must exist") 135 | 136 | defaultZone, exist := client.GetDefaultZone() 137 | testhelpers.Equals(t, testDefaultZone, defaultZone) 138 | testhelpers.Assert(t, exist, "defaultZone must exist") 139 | 140 | _, exist = client.GetDefaultPageSize() 141 | testhelpers.Assert(t, !exist, "defaultPageSize must not exist") 142 | }) 143 | 144 | } 145 | 146 | type fakeHTTPClient struct{} 147 | 148 | func (fakeHTTPClient) Do(*http.Request) (*http.Response, error) { 149 | return nil, nil 150 | } 151 | 152 | func (fakeHTTPClient) RoundTrip(*http.Request) (*http.Response, error) { 153 | return nil, nil 154 | } 155 | 156 | // TestSetInsecureMode test if setInsecureMode panic when given custom HTTP client 157 | func TestSetInsecureMode(t *testing.T) { 158 | var buf bytes.Buffer 159 | logger.DefaultLogger.Init(&buf, logger.LogLevelWarning) 160 | 161 | // custom Transport client 162 | clientWithFakeTransport := newHTTPClient() 163 | clientWithFakeTransport.Transport = fakeHTTPClient{} 164 | setInsecureMode(clientWithFakeTransport) 165 | 166 | // custom HTTP client 167 | setInsecureMode(fakeHTTPClient{}) 168 | 169 | // check log messages 170 | lines := strings.Split(buf.String(), "\n") 171 | getLogMessage := func(s string) string { 172 | return strings.Join(strings.Split(s, " ")[3:], " ") 173 | } 174 | testhelpers.Equals(t, "client: cannot use insecure mode with Transport client of type scw.fakeHTTPClient", getLogMessage(lines[0])) 175 | testhelpers.Equals(t, "client: cannot use insecure mode with HTTP client of type scw.fakeHTTPClient", getLogMessage(lines[1])) 176 | 177 | logger.DefaultLogger.Init(os.Stderr, logger.LogLevelWarning) 178 | } 179 | 180 | func TestNewVariableFromType(t *testing.T) { 181 | type fakeType struct { 182 | plop int 183 | } 184 | 185 | testhelpers.Equals(t, &fakeType{}, newVariableFromType(&fakeType{3})) 186 | } 187 | -------------------------------------------------------------------------------- /scw/convert_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 8 | ) 9 | 10 | var ( 11 | testString = "some string" 12 | testBytes = []byte{0, 1, 2} 13 | testBool = true 14 | testInt32 int32 = 42 15 | testInt64 int64 = 43 16 | testUInt32 uint32 = 44 17 | testUInt64 uint64 = 45 18 | testFloat32 float32 = 46 19 | testFloat64 float64 = 47 20 | testDuration time.Duration = 48 21 | testSize Size = 3 * GB 22 | ) 23 | 24 | func TestStringPtr(t *testing.T) { 25 | 26 | pointer := StringPtr(testString) 27 | slice := []string{testString} 28 | sliceOfPointers := StringSlicePtr(slice) 29 | pointerToSlice := StringsPtr(slice) 30 | 31 | // value to pointer value 32 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 33 | testhelpers.Equals(t, testString, *pointer) 34 | 35 | // slice of values to slice of pointers to value 36 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 37 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 38 | testhelpers.Equals(t, testString, *sliceOfPointers[0]) 39 | 40 | // slice of value to pointer to slice of values 41 | testhelpers.Assert(t, pointerToSlice != nil, "Pointer should have value") 42 | testhelpers.Equals(t, slice, *pointerToSlice) 43 | 44 | } 45 | 46 | func TestBytesPtr(t *testing.T) { 47 | 48 | pointer := BytesPtr(testBytes) 49 | slice := [][]byte{testBytes} 50 | sliceOfPointers := BytesSlicePtr(slice) 51 | 52 | // value to pointer value 53 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 54 | testhelpers.Equals(t, testBytes, *pointer) 55 | 56 | // slice of values to slice of pointers to value 57 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 58 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 59 | testhelpers.Equals(t, testBytes, *sliceOfPointers[0]) 60 | 61 | } 62 | 63 | func TestBoolPtr(t *testing.T) { 64 | 65 | pointer := BoolPtr(testBool) 66 | slice := []bool{testBool} 67 | sliceOfPointers := BoolSlicePtr(slice) 68 | 69 | // value to pointer value 70 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 71 | testhelpers.Equals(t, testBool, *pointer) 72 | 73 | // slice of values to slice of pointers to value 74 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 75 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 76 | testhelpers.Equals(t, testBool, *sliceOfPointers[0]) 77 | 78 | } 79 | 80 | func TestInt32Ptr(t *testing.T) { 81 | 82 | pointer := Int32Ptr(testInt32) 83 | slice := []int32{testInt32} 84 | sliceOfPointers := Int32SlicePtr(slice) 85 | 86 | // value to pointer value 87 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 88 | testhelpers.Equals(t, testInt32, *pointer) 89 | 90 | // slice of values to slice of pointers to value 91 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 92 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 93 | testhelpers.Equals(t, testInt32, *sliceOfPointers[0]) 94 | 95 | } 96 | 97 | func TestInt64Ptr(t *testing.T) { 98 | 99 | pointer := Int64Ptr(testInt64) 100 | slice := []int64{testInt64} 101 | sliceOfPointers := Int64SlicePtr(slice) 102 | 103 | // value to pointer value 104 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 105 | testhelpers.Equals(t, testInt64, *pointer) 106 | 107 | // slice of values to slice of pointers to value 108 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 109 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 110 | testhelpers.Equals(t, testInt64, *sliceOfPointers[0]) 111 | 112 | } 113 | 114 | func TestUint32Ptr(t *testing.T) { 115 | 116 | pointer := Uint32Ptr(testUInt32) 117 | slice := []uint32{testUInt32} 118 | sliceOfPointers := Uint32SlicePtr(slice) 119 | 120 | // value to pointer value 121 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 122 | testhelpers.Equals(t, testUInt32, *pointer) 123 | 124 | // slice of values to slice of pointers to value 125 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 126 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 127 | testhelpers.Equals(t, testUInt32, *sliceOfPointers[0]) 128 | 129 | } 130 | 131 | func TestUint64Ptr(t *testing.T) { 132 | 133 | pointer := Uint64Ptr(testUInt64) 134 | slice := []uint64{testUInt64} 135 | sliceOfPointers := Uint64SlicePtr(slice) 136 | 137 | // value to pointer value 138 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 139 | testhelpers.Equals(t, testUInt64, *pointer) 140 | 141 | // slice of values to slice of pointers to value 142 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 143 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 144 | testhelpers.Equals(t, testUInt64, *sliceOfPointers[0]) 145 | 146 | } 147 | 148 | func TestFloat32Ptr(t *testing.T) { 149 | 150 | pointer := Float32Ptr(testFloat32) 151 | slice := []float32{testFloat32} 152 | sliceOfPointers := Float32SlicePtr(slice) 153 | 154 | // value to pointer value 155 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 156 | testhelpers.Equals(t, testFloat32, *pointer) 157 | 158 | // slice of values to slice of pointers to value 159 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 160 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 161 | testhelpers.Equals(t, testFloat32, *sliceOfPointers[0]) 162 | 163 | } 164 | 165 | func TestFloat64Ptr(t *testing.T) { 166 | 167 | pointer := Float64Ptr(testFloat64) 168 | slice := []float64{testFloat64} 169 | sliceOfPointers := Float64SlicePtr(slice) 170 | 171 | // value to pointer value 172 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 173 | testhelpers.Equals(t, testFloat64, *pointer) 174 | 175 | // slice of values to slice of pointers to value 176 | testhelpers.Equals(t, 1, len(sliceOfPointers)) 177 | testhelpers.Assert(t, sliceOfPointers[0] != nil, "Pointer should have value") 178 | testhelpers.Equals(t, testFloat64, *sliceOfPointers[0]) 179 | 180 | } 181 | 182 | func TestDurationPtr(t *testing.T) { 183 | 184 | pointer := DurationPtr(testDuration) 185 | 186 | // value to pointer value 187 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 188 | testhelpers.Equals(t, testDuration, *pointer) 189 | 190 | } 191 | 192 | func TestSizePtr(t *testing.T) { 193 | 194 | pointer := SizePtr(testSize) 195 | 196 | // value to pointer value 197 | testhelpers.Assert(t, pointer != nil, "Pointer should have value") 198 | testhelpers.Equals(t, testSize, *pointer) 199 | 200 | } 201 | -------------------------------------------------------------------------------- /scw/client_option_test.go: -------------------------------------------------------------------------------- 1 | package scw 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/scaleway/scaleway-sdk-go/internal/auth" 9 | "github.com/scaleway/scaleway-sdk-go/internal/testhelpers" 10 | ) 11 | 12 | const ( 13 | apiURL = "https://example.com/" 14 | ) 15 | 16 | var ( 17 | defaultOrganizationID = "6170692e-7363-616c-6577-61792e636f6d" // hint: | xxd -ps -r 18 | defaultRegion = RegionNlAms 19 | defaultZone = ZoneNlAms1 20 | ) 21 | 22 | func TestClientOptions(t *testing.T) { 23 | 24 | testCases := []struct { 25 | name string 26 | clientOption ClientOption 27 | errStr string 28 | }{ 29 | { 30 | name: "Create a valid client option", 31 | clientOption: func(s *settings) { 32 | s.token = auth.NewToken(testAccessKey, testSecretKey) 33 | s.apiURL = apiURL 34 | s.defaultOrganizationID = &defaultOrganizationID 35 | s.defaultRegion = &defaultRegion 36 | s.defaultZone = &defaultZone 37 | }, 38 | }, 39 | { 40 | name: "Should throw an credential error", 41 | clientOption: func(s *settings) { 42 | s.apiURL = apiURL 43 | }, 44 | errStr: "scaleway-sdk-go: no credential option provided", 45 | }, 46 | { 47 | name: "Should throw an url error", 48 | clientOption: func(s *settings) { 49 | s.apiURL = ":test" 50 | s.token = auth.NewToken(testAccessKey, testSecretKey) 51 | }, 52 | errStr: "scaleway-sdk-go: invalid url :test: parse :test: missing protocol scheme", 53 | }, 54 | { 55 | name: "Should throw a organization id error", 56 | clientOption: func(s *settings) { 57 | v := "" 58 | s.token = auth.NewToken(testAccessKey, testSecretKey) 59 | s.defaultOrganizationID = &v 60 | }, 61 | errStr: "scaleway-sdk-go: default organization id cannot be empty", 62 | }, 63 | { 64 | name: "Should throw a region error", 65 | clientOption: func(s *settings) { 66 | v := Region("") 67 | s.token = auth.NewToken(testAccessKey, testSecretKey) 68 | s.defaultRegion = &v 69 | }, 70 | errStr: "scaleway-sdk-go: default region cannot be empty", 71 | }, 72 | { 73 | name: "Should throw a zone error", 74 | clientOption: func(s *settings) { 75 | v := Zone("") 76 | s.token = auth.NewToken(testAccessKey, testSecretKey) 77 | s.defaultZone = &v 78 | }, 79 | errStr: "scaleway-sdk-go: default zone cannot be empty", 80 | }, 81 | } 82 | 83 | for _, c := range testCases { 84 | t.Run(c.name, func(t *testing.T) { 85 | // New 86 | s := newSettings() 87 | 88 | // Apply 89 | s.apply([]ClientOption{c.clientOption}) 90 | 91 | // Validate 92 | err := s.validate() 93 | 94 | if c.errStr != "" { 95 | testhelpers.Assert(t, err != nil, "Should have error") 96 | testhelpers.Equals(t, c.errStr, err.Error()) 97 | } else { 98 | testhelpers.AssertNoError(t, err) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestCombinedClientOptions(t *testing.T) { 105 | tests := []struct { 106 | name string 107 | env map[string]string 108 | files map[string]string 109 | 110 | expectedError string 111 | expectedAccessKey string 112 | expectedSecretKey string 113 | expectedAPIURL string 114 | expectedDefaultOrganizationID *string 115 | expectedDefaultRegion *Region 116 | expectedDefaultZone *Zone 117 | }{ 118 | { 119 | name: "Complete config file with env variables", 120 | env: map[string]string{ 121 | "HOME": "{HOME}", 122 | scwAccessKeyEnv: v2ValidAccessKey2, 123 | scwSecretKeyEnv: v2ValidSecretKey2, 124 | scwAPIURLEnv: v2ValidAPIURL2, 125 | scwDefaultOrganizationIDEnv: v2ValidDefaultOrganizationID2, 126 | scwDefaultRegionEnv: v2ValidDefaultRegion2, 127 | scwDefaultZoneEnv: v2ValidDefaultZone2, 128 | }, 129 | files: map[string]string{ 130 | ".config/scw/config.yaml": v2CompleteValidConfigFile, 131 | }, 132 | expectedAccessKey: v2ValidAccessKey2, 133 | expectedSecretKey: v2ValidSecretKey2, 134 | expectedAPIURL: v2ValidAPIURL2, 135 | expectedDefaultOrganizationID: s(v2ValidDefaultOrganizationID2), 136 | expectedDefaultRegion: r(Region(v2ValidDefaultRegion2)), 137 | expectedDefaultZone: z(Zone(v2ValidDefaultZone2)), 138 | }, 139 | { 140 | name: "Complete config with active profile env variable and all env variables", 141 | env: map[string]string{ 142 | "HOME": "{HOME}", 143 | scwActiveProfileEnv: v2ValidProfile, 144 | scwAccessKeyEnv: v2ValidAccessKey, 145 | scwSecretKeyEnv: v2ValidSecretKey, 146 | scwAPIURLEnv: v2ValidAPIURL, 147 | scwDefaultOrganizationIDEnv: v2ValidDefaultOrganizationID, 148 | scwDefaultRegionEnv: v2ValidDefaultRegion, 149 | scwDefaultZoneEnv: v2ValidDefaultZone, 150 | }, 151 | files: map[string]string{ 152 | ".config/scw/config.yaml": v2CompleteValidConfigFile, 153 | }, 154 | expectedAccessKey: v2ValidAccessKey, 155 | expectedSecretKey: v2ValidSecretKey, 156 | expectedAPIURL: v2ValidAPIURL, 157 | expectedDefaultOrganizationID: s(v2ValidDefaultOrganizationID), 158 | expectedDefaultRegion: r(Region(v2ValidDefaultRegion)), 159 | expectedDefaultZone: z(Zone(v2ValidDefaultZone)), 160 | }, 161 | } 162 | 163 | // create home dir 164 | dir := initEnv(t) 165 | 166 | // delete home dir and reset env variables 167 | defer resetEnv(t, os.Environ(), dir) 168 | for _, test := range tests { 169 | t.Run(test.name, func(t *testing.T) { 170 | // set up env and config file(s) 171 | setEnv(t, test.env, test.files, dir) 172 | test.expectedError = strings.Replace(test.expectedError, "{HOME}", dir, -1) 173 | 174 | // remove config file(s) 175 | defer cleanEnv(t, test.files, dir) 176 | 177 | config, err := LoadConfig() 178 | testhelpers.AssertNoError(t, err) 179 | 180 | p, err := config.GetActiveProfile() 181 | testhelpers.AssertNoError(t, err) 182 | 183 | client, err := NewClient(WithProfile(p), WithEnv()) 184 | if test.expectedError == "" { 185 | testhelpers.AssertNoError(t, err) 186 | 187 | // assert getters 188 | testhelpers.Equals(t, test.expectedAccessKey, client.auth.(*auth.Token).AccessKey) 189 | testhelpers.Equals(t, test.expectedSecretKey, client.auth.(*auth.Token).SecretKey) 190 | testhelpers.Equals(t, test.expectedAPIURL, client.apiURL) 191 | testhelpers.Equals(t, test.expectedDefaultOrganizationID, client.defaultOrganizationID) 192 | testhelpers.Equals(t, test.expectedDefaultRegion, client.defaultRegion) 193 | testhelpers.Equals(t, test.expectedDefaultZone, client.defaultZone) 194 | // skip insecure tests 195 | } else { 196 | testhelpers.Equals(t, test.expectedError, err.Error()) 197 | } 198 | 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /vendor/github.com/dnaeon/go-vcr/cassette/cassette.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Marin Atanasov Nikolov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions 6 | // are met: 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer 9 | // in this position and unchanged. 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 15 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 21 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | package cassette 26 | 27 | import ( 28 | "errors" 29 | "fmt" 30 | "io/ioutil" 31 | "net/http" 32 | "net/url" 33 | "os" 34 | "path/filepath" 35 | "sync" 36 | 37 | "gopkg.in/yaml.v2" 38 | ) 39 | 40 | // Cassette format versions 41 | const ( 42 | cassetteFormatV1 = 1 43 | ) 44 | 45 | var ( 46 | // ErrInteractionNotFound indicates that a requested 47 | // interaction was not found in the cassette file 48 | ErrInteractionNotFound = errors.New("Requested interaction not found") 49 | ) 50 | 51 | // Request represents a client request as recorded in the 52 | // cassette file 53 | type Request struct { 54 | // Body of request 55 | Body string `yaml:"body"` 56 | 57 | // Form values 58 | Form url.Values `yaml:"form"` 59 | 60 | // Request headers 61 | Headers http.Header `yaml:"headers"` 62 | 63 | // Request URL 64 | URL string `yaml:"url"` 65 | 66 | // Request method 67 | Method string `yaml:"method"` 68 | } 69 | 70 | // Response represents a server response as recorded in the 71 | // cassette file 72 | type Response struct { 73 | // Body of response 74 | Body string `yaml:"body"` 75 | 76 | // Response headers 77 | Headers http.Header `yaml:"headers"` 78 | 79 | // Response status message 80 | Status string `yaml:"status"` 81 | 82 | // Response status code 83 | Code int `yaml:"code"` 84 | 85 | // Response duration (something like "100ms" or "10s") 86 | Duration string `yaml:"duration"` 87 | 88 | replayed bool 89 | } 90 | 91 | // Interaction type contains a pair of request/response for a 92 | // single HTTP interaction between a client and a server 93 | type Interaction struct { 94 | Request `yaml:"request"` 95 | Response `yaml:"response"` 96 | } 97 | 98 | // Matcher function returns true when the actual request matches 99 | // a single HTTP interaction's request according to the function's 100 | // own criteria. 101 | type Matcher func(*http.Request, Request) bool 102 | 103 | // DefaultMatcher is used when a custom matcher is not defined 104 | // and compares only the method and URL. 105 | func DefaultMatcher(r *http.Request, i Request) bool { 106 | return r.Method == i.Method && r.URL.String() == i.URL 107 | } 108 | 109 | // Filter function allows modification of an interaction before saving. 110 | type Filter func(*Interaction) error 111 | 112 | // Cassette type 113 | type Cassette struct { 114 | // Name of the cassette 115 | Name string `yaml:"-"` 116 | 117 | // File name of the cassette as written on disk 118 | File string `yaml:"-"` 119 | 120 | // Cassette format version 121 | Version int `yaml:"version"` 122 | 123 | // Mutex to lock accessing Interactions. omitempty is set 124 | // to prevent the mutex appearing in the recorded YAML. 125 | Mu sync.RWMutex `yaml:"mu,omitempty"` 126 | // Interactions between client and server 127 | Interactions []*Interaction `yaml:"interactions"` 128 | 129 | // Matches actual request with interaction requests. 130 | Matcher Matcher `yaml:"-"` 131 | 132 | // Filters interactions before being saved. 133 | Filters []Filter `yaml:"-"` 134 | } 135 | 136 | // New creates a new empty cassette 137 | func New(name string) *Cassette { 138 | c := &Cassette{ 139 | Name: name, 140 | File: fmt.Sprintf("%s.yaml", name), 141 | Version: cassetteFormatV1, 142 | Interactions: make([]*Interaction, 0), 143 | Matcher: DefaultMatcher, 144 | Filters: make([]Filter, 0), 145 | } 146 | 147 | return c 148 | } 149 | 150 | // Load reads a cassette file from disk 151 | func Load(name string) (*Cassette, error) { 152 | c := New(name) 153 | data, err := ioutil.ReadFile(c.File) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | err = yaml.Unmarshal(data, &c) 159 | 160 | return c, err 161 | } 162 | 163 | // AddInteraction appends a new interaction to the cassette 164 | func (c *Cassette) AddInteraction(i *Interaction) { 165 | c.Mu.Lock() 166 | c.Interactions = append(c.Interactions, i) 167 | c.Mu.Unlock() 168 | } 169 | 170 | // GetInteraction retrieves a recorded request/response interaction 171 | func (c *Cassette) GetInteraction(r *http.Request) (*Interaction, error) { 172 | c.Mu.Lock() 173 | defer c.Mu.Unlock() 174 | for _, i := range c.Interactions { 175 | if !i.replayed && c.Matcher(r, i.Request) { 176 | i.replayed = true 177 | return i, nil 178 | } 179 | } 180 | 181 | return nil, ErrInteractionNotFound 182 | } 183 | 184 | // Save writes the cassette data on disk for future re-use 185 | func (c *Cassette) Save() error { 186 | c.Mu.RLock() 187 | defer c.Mu.RUnlock() 188 | // Save cassette file only if there were any interactions made 189 | if len(c.Interactions) == 0 { 190 | return nil 191 | } 192 | 193 | // Create directory for cassette if missing 194 | cassetteDir := filepath.Dir(c.File) 195 | if _, err := os.Stat(cassetteDir); os.IsNotExist(err) { 196 | if err = os.MkdirAll(cassetteDir, 0755); err != nil { 197 | return err 198 | } 199 | } 200 | 201 | // Marshal to YAML and save interactions 202 | data, err := yaml.Marshal(c) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | f, err := os.Create(c.File) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | defer f.Close() 213 | 214 | // Honor the YAML structure specification 215 | // http://www.yaml.org/spec/1.2/spec.html#id2760395 216 | _, err = f.Write([]byte("---\n")) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | _, err = f.Write(data) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | return nil 227 | } 228 | --------------------------------------------------------------------------------