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  }