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