github.com/sacloud/iaas-api-go@v1.12.0/client.go (about) 1 // Copyright 2022-2023 The sacloud/iaas-api-go Authors 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 15 package iaas 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "fmt" 22 "io" 23 "net/http" 24 "runtime" 25 26 client "github.com/sacloud/api-client-go" 27 sacloudhttp "github.com/sacloud/go-http" 28 "github.com/sacloud/iaas-api-go/types" 29 ) 30 31 var ( 32 // SakuraCloudAPIRoot APIリクエスト送信先ルートURL(末尾にスラッシュを含まない) 33 SakuraCloudAPIRoot = "https://secure.sakura.ad.jp/cloud/zone" 34 35 // SakuraCloudZones 利用可能なゾーンのデフォルト値 36 SakuraCloudZones = types.ZoneNames 37 ) 38 39 var ( 40 // APIDefaultZone デフォルトゾーン、グローバルリソースなどで利用される 41 APIDefaultZone = "is1a" 42 // DefaultUserAgent デフォルトのユーザーエージェント 43 DefaultUserAgent = fmt.Sprintf( 44 "sacloud/iaas-api-go/v%s (%s/%s; +https://github.com/sacloud/iaas-api-go) %s", 45 Version, 46 runtime.GOOS, 47 runtime.GOARCH, 48 sacloudhttp.DefaultUserAgent, 49 ) 50 51 defaultCheckRetryStatusCodes = []int{ 52 http.StatusServiceUnavailable, 53 http.StatusLocked, 54 } 55 ) 56 57 const ( 58 // APIAccessTokenEnvKey APIアクセストークンの環境変数名 59 APIAccessTokenEnvKey = "SAKURACLOUD_ACCESS_TOKEN" //nolint:gosec 60 // APIAccessSecretEnvKey APIアクセスシークレットの環境変数名 61 APIAccessSecretEnvKey = "SAKURACLOUD_ACCESS_TOKEN_SECRET" //nolint:gosec 62 ) 63 64 // APICaller API呼び出し時に利用するトランスポートのインターフェース iaas.Clientなどで実装される 65 type APICaller interface { 66 Do(ctx context.Context, method, uri string, body interface{}) ([]byte, error) 67 } 68 69 // Client APIクライアント、APICallerインターフェースを実装する 70 // 71 // レスポンスステータスコード423、または503を受け取った場合、RetryMax回リトライする 72 // リトライ間隔はRetryMinからRetryMaxまで指数的に増加する(Exponential Backoff) 73 // 74 // リトライ時にcontext.Canceled、またはcontext.DeadlineExceededの場合はリトライしない 75 type Client struct { 76 factory *client.Factory 77 } 78 79 // NewClient APIクライアント作成 80 func NewClient(token, secret string) *Client { 81 opts := &client.Options{ 82 AccessToken: token, 83 AccessTokenSecret: secret, 84 } 85 return NewClientWithOptions(opts) 86 } 87 88 // NewClientFromEnv 環境変数からAPIキーを取得してAPIクライアントを作成する 89 func NewClientFromEnv() *Client { 90 return NewClientWithOptions(client.OptionsFromEnv()) 91 } 92 93 // NewClientWithOptions 指定のオプションでAPIクライアントを作成する 94 func NewClientWithOptions(opts *client.Options) *Client { 95 if len(opts.CheckRetryStatusCodes) == 0 { 96 opts.CheckRetryStatusCodes = defaultCheckRetryStatusCodes 97 } 98 factory := client.NewFactory(opts) 99 return &Client{factory: factory} 100 } 101 102 // Do APIコール実施 103 func (c *Client) Do(ctx context.Context, method, uri string, body interface{}) ([]byte, error) { 104 req, err := c.newRequest(ctx, method, uri, body) 105 if err != nil { 106 return nil, err 107 } 108 109 // API call 110 resp, err := c.factory.NewHttpRequestDoer().Do(req) 111 if err != nil { 112 return nil, err 113 } 114 defer resp.Body.Close() 115 116 data, err := io.ReadAll(resp.Body) 117 if err != nil { 118 return nil, err 119 } 120 121 if !c.isOkStatus(resp.StatusCode) { 122 errResponse := &APIErrorResponse{} 123 err := json.Unmarshal(data, errResponse) 124 if err != nil { 125 return nil, fmt.Errorf("error in response: %s", string(data)) 126 } 127 return nil, NewAPIError(req.Method, req.URL, resp.StatusCode, errResponse) 128 } 129 130 return data, nil 131 } 132 133 func (c *Client) newRequest(ctx context.Context, method, uri string, body interface{}) (*http.Request, error) { 134 // setup url and body 135 var url = uri 136 var bodyReader io.ReadSeeker 137 if body != nil { 138 var bodyJSON []byte 139 bodyJSON, err := json.Marshal(body) 140 if err != nil { 141 return nil, err 142 } 143 if method == "GET" { 144 url = fmt.Sprintf("%s?%s", url, bytes.NewBuffer(bodyJSON)) 145 } else { 146 bodyReader = bytes.NewReader(bodyJSON) 147 } 148 } 149 return http.NewRequestWithContext(ctx, method, url, bodyReader) 150 } 151 152 func (c *Client) isOkStatus(code int) bool { 153 codes := map[int]bool{ 154 http.StatusOK: true, 155 http.StatusCreated: true, 156 http.StatusAccepted: true, 157 http.StatusNoContent: true, 158 } 159 _, ok := codes[code] 160 return ok 161 }