client.go raw
1 // Copyright 2022-2025 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() //nolint:errcheck
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 }
162