options.go raw

   1  // Copyright 2022-2025 The sacloud/api-client-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 client
  16  
  17  import (
  18  	"context"
  19  	"net/http"
  20  	"strings"
  21  
  22  	"github.com/sacloud/api-client-go/profile"
  23  	sacloudhttp "github.com/sacloud/go-http"
  24  	"github.com/sacloud/packages-go/envvar"
  25  )
  26  
  27  // Options sacloudhttp.Clientを作成する際のオプション
  28  type Options struct {
  29  	// AccessToken APIキー:トークン
  30  	AccessToken string
  31  	// AccessTokenSecret APIキー:シークレット
  32  	AccessTokenSecret string
  33  
  34  	// AcceptLanguage APIリクエスト時のAccept-Languageヘッダーの値
  35  	AcceptLanguage string
  36  
  37  	// Gzip APIリクエストでgzipを有効にするかのフラグ
  38  	Gzip bool
  39  
  40  	// HttpClient APIリクエストで使用されるHTTPクライアント
  41  	//
  42  	// 省略した場合はhttp.DefaultClientが使用される
  43  	HttpClient *http.Client
  44  
  45  	// HttpRequestTimeout HTTPリクエストのタイムアウト秒数
  46  	HttpRequestTimeout int
  47  	// HttpRequestRateLimit 1秒あたりの上限リクエスト数
  48  	HttpRequestRateLimit int
  49  
  50  	// RetryMax リトライ上限回数
  51  	RetryMax int
  52  
  53  	// RetryWaitMax リトライ待ち秒数(最大)
  54  	RetryWaitMax int
  55  	// RetryWaitMin リトライ待ち秒数(最小)
  56  	RetryWaitMin int
  57  
  58  	// UserAgent ユーザーエージェント
  59  	UserAgent string
  60  
  61  	// Trace HTTPリクエスト/レスポンスのトレースログ(ダンプ)出力
  62  	Trace bool
  63  	// TraceOnlyError HTTPリクエスト/レスポンスのトレースログ(ダンプ)出力で非200番台のレスポンス時のみ出力する
  64  	TraceOnlyError bool
  65  
  66  	// RequestCustomizers リクエスト前に*http.Requestのカスタマイズを行うためのfunc
  67  	RequestCustomizers []sacloudhttp.RequestCustomizer
  68  
  69  	// CheckRetryFunc リトライすべきか判定するためのfunc
  70  	//
  71  	// CheckRetryStatusCodesより優先される
  72  	CheckRetryFunc func(ctx context.Context, resp *http.Response, err error) (bool, error)
  73  
  74  	// CheckRetryStatusCodes リトライすべきステータスコード
  75  	//
  76  	// CheckRetryFuncが指定されていない、かつこの値が指定されている場合、指定のステータスコードを持つレスポンスを受け取ったらリトライする
  77  	CheckRetryStatusCodes []int
  78  
  79  	// profileConfigValue プロファイルから読み込んだ値を保持する
  80  	profileConfigValue *profile.ConfigValue
  81  }
  82  
  83  // ProfileConfigValue プロファイルから読み込んだprofile.ConfigValueを返す
  84  func (o *Options) ProfileConfigValue() *profile.ConfigValue {
  85  	return o.profileConfigValue
  86  }
  87  
  88  // DefaultOption 環境変数、プロファイルからCallerOptionsを組み立てて返す
  89  //
  90  // プロファイルは環境変数`SAKURACLOUD_PROFILE`または`USACLOUD_PROFILE`でプロファイル名が指定されていればそちらを優先し、
  91  // 未指定の場合は通常のプロファイル処理(~/.usacloud/currentファイルから読み込み)される。
  92  // 同じ項目を複数箇所で指定していた場合、環境変数->プロファイルの順で上書きされたものが返される
  93  func DefaultOption() (*Options, error) {
  94  	return DefaultOptionWithProfile("")
  95  }
  96  
  97  // DefaultOptionWithProfile 環境変数、プロファイルからCallerOptionsを組み立てて返す
  98  //
  99  // プロファイルは引数を優先し、空の場合は環境変数`SAKURACLOUD_PROFILE`または`USACLOUD_PROFILE`が利用され、
 100  // それも空の場合は通常のプロファイル処理(~/.usacloud/currentファイルから読み込み)される。
 101  // 同じ項目を複数箇所で指定していた場合、環境変数->プロファイルの順で上書きされたものが返される
 102  func DefaultOptionWithProfile(profileName string) (*Options, error) {
 103  	fromProfile, err := OptionsFromProfile(profileName)
 104  	if err != nil {
 105  		return nil, err
 106  	}
 107  	return MergeOptions(defaultOption, OptionsFromEnv(), fromProfile), nil
 108  }
 109  
 110  var defaultOption = &Options{
 111  	HttpRequestTimeout:   300,
 112  	HttpRequestRateLimit: 5,
 113  	RetryMax:             sacloudhttp.DefaultRetryMax,
 114  	RetryWaitMax:         int(sacloudhttp.DefaultRetryWaitMax.Seconds()),
 115  	RetryWaitMin:         int(sacloudhttp.DefaultRetryWaitMin.Seconds()),
 116  }
 117  
 118  // MergeOptions 指定のCallerOptionsの非ゼロ値フィールドをoのコピーにマージして返す
 119  func MergeOptions(opts ...*Options) *Options {
 120  	merged := &Options{}
 121  	for _, opt := range opts {
 122  		if opt.AccessToken != "" {
 123  			merged.AccessToken = opt.AccessToken
 124  		}
 125  		if opt.AccessTokenSecret != "" {
 126  			merged.AccessTokenSecret = opt.AccessTokenSecret
 127  		}
 128  		if opt.AcceptLanguage != "" {
 129  			merged.AcceptLanguage = opt.AcceptLanguage
 130  		}
 131  		if opt.HttpClient != nil {
 132  			merged.HttpClient = opt.HttpClient
 133  		}
 134  		if opt.HttpRequestTimeout > 0 {
 135  			merged.HttpRequestTimeout = opt.HttpRequestTimeout
 136  		}
 137  		if opt.HttpRequestRateLimit > 0 {
 138  			merged.HttpRequestRateLimit = opt.HttpRequestRateLimit
 139  		}
 140  		if opt.RetryMax > 0 {
 141  			merged.RetryMax = opt.RetryMax
 142  		}
 143  		if opt.RetryWaitMax > 0 {
 144  			merged.RetryWaitMax = opt.RetryWaitMax
 145  		}
 146  		if opt.RetryWaitMin > 0 {
 147  			merged.RetryWaitMin = opt.RetryWaitMin
 148  		}
 149  		if opt.UserAgent != "" {
 150  			merged.UserAgent = opt.UserAgent
 151  		}
 152  
 153  		if opt.profileConfigValue != nil {
 154  			merged.profileConfigValue = opt.profileConfigValue
 155  		}
 156  
 157  		// Note: bool値は一度trueにしたらMergeでfalseになることがない
 158  		if opt.Gzip {
 159  			merged.Gzip = true
 160  		}
 161  		if opt.Trace {
 162  			merged.Trace = true
 163  		}
 164  		if opt.TraceOnlyError {
 165  			merged.TraceOnlyError = true
 166  		}
 167  		if len(opt.RequestCustomizers) > 0 {
 168  			merged.RequestCustomizers = opt.RequestCustomizers
 169  		}
 170  		if opt.CheckRetryFunc != nil {
 171  			merged.CheckRetryFunc = opt.CheckRetryFunc
 172  		}
 173  		if len(opt.CheckRetryStatusCodes) > 0 {
 174  			merged.CheckRetryStatusCodes = opt.CheckRetryStatusCodes
 175  		}
 176  	}
 177  	return merged
 178  }
 179  
 180  // OptionsFromEnv 環境変数からCallerOptionsを組み立てて返す
 181  func OptionsFromEnv() *Options {
 182  	return &Options{
 183  		AccessToken:       envvar.StringFromEnv("SAKURACLOUD_ACCESS_TOKEN", ""),
 184  		AccessTokenSecret: envvar.StringFromEnv("SAKURACLOUD_ACCESS_TOKEN_SECRET", ""),
 185  
 186  		AcceptLanguage: envvar.StringFromEnv("SAKURACLOUD_ACCEPT_LANGUAGE", ""),
 187  		Gzip:           envvar.StringFromEnv("SAKURACLOUD_GZIP", "") != "",
 188  
 189  		HttpRequestTimeout:   envvar.IntFromEnv("SAKURACLOUD_API_REQUEST_TIMEOUT", 0),
 190  		HttpRequestRateLimit: envvar.IntFromEnv("SAKURACLOUD_API_REQUEST_RATE_LIMIT", 0),
 191  
 192  		RetryMax:     envvar.IntFromEnv("SAKURACLOUD_RETRY_MAX", 0),
 193  		RetryWaitMax: envvar.IntFromEnv("SAKURACLOUD_RETRY_WAIT_MAX", 0),
 194  		RetryWaitMin: envvar.IntFromEnv("SAKURACLOUD_RETRY_WAIT_MIN", 0),
 195  
 196  		Trace:          envvar.StringFromEnv("SAKURACLOUD_TRACE", "") != "",
 197  		TraceOnlyError: strings.ToLower(envvar.StringFromEnv("SAKURACLOUD_TRACE", "")) == "error",
 198  	}
 199  }
 200  
 201  // OptionsFromProfile 指定のプロファイルからCallerOptionsを組み立てて返す
 202  //
 203  // プロファイルは引数を優先し、空の場合は環境変数`SAKURACLOUD_PROFILE`または`USACLOUD_PROFILE`が利用され、
 204  // それも空の場合は通常のプロファイル処理(~/.usacloud/currentファイルから読み込み)される。
 205  func OptionsFromProfile(profileName string) (*Options, error) {
 206  	// 引数がからの場合はまず環境変数から
 207  	if profileName == "" {
 208  		profileName = envvar.StringFromEnvMulti([]string{"SAKURACLOUD_PROFILE", "USACLOUD_PROFILE"}, "")
 209  	}
 210  	// それも空ならプロファイルのcurrentファイルから
 211  	if profileName == "" {
 212  		current, err := profile.CurrentName()
 213  		if err != nil {
 214  			return nil, err
 215  		}
 216  		profileName = current
 217  	}
 218  
 219  	config := profile.ConfigValue{}
 220  	if err := profile.Load(profileName, &config); err != nil {
 221  		return nil, err
 222  	}
 223  
 224  	return &Options{
 225  		AccessToken:          config.AccessToken,
 226  		AccessTokenSecret:    config.AccessTokenSecret,
 227  		AcceptLanguage:       config.AcceptLanguage,
 228  		Gzip:                 config.Gzip,
 229  		HttpRequestTimeout:   config.HTTPRequestTimeout,
 230  		HttpRequestRateLimit: config.HTTPRequestRateLimit,
 231  		RetryMax:             config.RetryMax,
 232  		RetryWaitMax:         config.RetryWaitMax,
 233  		RetryWaitMin:         config.RetryWaitMin,
 234  		Trace:                config.EnableHTTPTrace(),
 235  		TraceOnlyError:       strings.ToLower(config.TraceMode) == "error",
 236  		profileConfigValue:   &config,
 237  	}, nil
 238  }
 239