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