github.com/sacloud/libsacloud/v2@v2.32.3/helper/api/options.go (about)

     1  // Copyright 2016-2022 The Libsacloud 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 api
    16  
    17  import (
    18  	"net/http"
    19  	"os"
    20  	"strconv"
    21  	"strings"
    22  
    23  	sacloudhttp "github.com/sacloud/go-http"
    24  	"github.com/sacloud/libsacloud/v2/sacloud"
    25  	"github.com/sacloud/libsacloud/v2/sacloud/profile"
    26  	"github.com/sacloud/libsacloud/v2/sacloud/trace/otel"
    27  )
    28  
    29  // CallerOptions sacloud.APICallerを作成する際のオプション
    30  type CallerOptions struct {
    31  	AccessToken       string
    32  	AccessTokenSecret string
    33  
    34  	APIRootURL     string
    35  	DefaultZone    string
    36  	Zones          []string
    37  	AcceptLanguage string
    38  
    39  	HTTPClient *http.Client
    40  
    41  	HTTPRequestTimeout   int
    42  	HTTPRequestRateLimit int
    43  
    44  	RetryMax     int
    45  	RetryWaitMax int
    46  	RetryWaitMin int
    47  
    48  	UserAgent string
    49  
    50  	TraceAPI             bool
    51  	TraceHTTP            bool
    52  	OpenTelemetry        bool
    53  	OpenTelemetryOptions []otel.Option
    54  
    55  	FakeMode      bool
    56  	FakeStorePath string
    57  }
    58  
    59  // DefaultOption 環境変数、プロファイルからCallerOptionsを組み立てて返す
    60  //
    61  // プロファイルは環境変数`SAKURACLOUD_PROFILE`または`USACLOUD_PROFILE`でプロファイル名が指定されていればそちらを優先し、
    62  // 未指定の場合は通常のプロファイル処理(~/.usacloud/currentファイルから読み込み)される。
    63  // 同じ項目を複数箇所で指定していた場合、環境変数->プロファイルの順で上書きされたものが返される
    64  func DefaultOption() (*CallerOptions, error) {
    65  	return DefaultOptionWithProfile("")
    66  }
    67  
    68  // DefaultOptionWithProfile 環境変数、プロファイルからCallerOptionsを組み立てて返す
    69  //
    70  // プロファイルは引数を優先し、空の場合は環境変数`SAKURACLOUD_PROFILE`または`USACLOUD_PROFILE`が利用され、
    71  // それも空の場合は通常のプロファイル処理(~/.usacloud/currentファイルから読み込み)される。
    72  // 同じ項目を複数箇所で指定していた場合、環境変数->プロファイルの順で上書きされたものが返される
    73  func DefaultOptionWithProfile(profileName string) (*CallerOptions, error) {
    74  	if profileName == "" {
    75  		profileName = stringFromEnvMulti([]string{"SAKURACLOUD_PROFILE", "USACLOUD_PROFILE"}, "")
    76  	}
    77  	fromProfile, err := OptionsFromProfile(profileName)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	return MergeOptions(OptionsFromEnv(), fromProfile, defaultOption), nil
    82  }
    83  
    84  var defaultOption = &CallerOptions{
    85  	APIRootURL:           sacloud.SakuraCloudAPIRoot,
    86  	DefaultZone:          sacloud.APIDefaultZone,
    87  	HTTPRequestTimeout:   300,
    88  	HTTPRequestRateLimit: 5,
    89  	RetryMax:             sacloudhttp.DefaultRetryMax,
    90  	RetryWaitMax:         int(sacloudhttp.DefaultRetryWaitMax.Seconds()),
    91  	RetryWaitMin:         int(sacloudhttp.DefaultRetryWaitMin.Seconds()),
    92  }
    93  
    94  // MergeOptions 指定のCallerOptionsの非ゼロ値フィールドをoのコピーにマージして返す
    95  func MergeOptions(opts ...*CallerOptions) *CallerOptions {
    96  	merged := &CallerOptions{}
    97  	for _, opt := range opts {
    98  		if opt.AccessToken != "" {
    99  			merged.AccessToken = opt.AccessToken
   100  		}
   101  		if opt.AccessTokenSecret != "" {
   102  			merged.AccessTokenSecret = opt.AccessTokenSecret
   103  		}
   104  		if opt.APIRootURL != "" {
   105  			merged.APIRootURL = opt.APIRootURL
   106  		}
   107  		if opt.DefaultZone != "" {
   108  			merged.DefaultZone = opt.DefaultZone
   109  		}
   110  		if len(opt.Zones) > 0 {
   111  			merged.Zones = opt.Zones
   112  		}
   113  		if opt.AcceptLanguage != "" {
   114  			merged.AcceptLanguage = opt.AcceptLanguage
   115  		}
   116  		if opt.HTTPClient != nil {
   117  			merged.HTTPClient = opt.HTTPClient
   118  		}
   119  		if opt.HTTPRequestTimeout > 0 {
   120  			merged.HTTPRequestTimeout = opt.HTTPRequestTimeout
   121  		}
   122  		if opt.HTTPRequestRateLimit > 0 {
   123  			merged.HTTPRequestRateLimit = opt.HTTPRequestRateLimit
   124  		}
   125  		if opt.RetryMax > 0 {
   126  			merged.RetryMax = opt.RetryMax
   127  		}
   128  		if opt.RetryWaitMax > 0 {
   129  			merged.RetryWaitMax = opt.RetryWaitMax
   130  		}
   131  		if opt.RetryWaitMin > 0 {
   132  			merged.RetryWaitMin = opt.RetryWaitMin
   133  		}
   134  		if opt.UserAgent != "" {
   135  			merged.UserAgent = opt.UserAgent
   136  		}
   137  
   138  		// Note: bool値は一度trueにしたらMergeでfalseになることがない
   139  		if opt.TraceAPI {
   140  			merged.TraceAPI = true
   141  		}
   142  		if opt.TraceHTTP {
   143  			merged.TraceHTTP = true
   144  		}
   145  		if opt.OpenTelemetry {
   146  			merged.OpenTelemetry = true
   147  		}
   148  		if len(opt.OpenTelemetryOptions) > 0 {
   149  			merged.OpenTelemetryOptions = opt.OpenTelemetryOptions
   150  		}
   151  		if opt.FakeMode {
   152  			merged.FakeMode = true
   153  		}
   154  		if opt.FakeStorePath != "" {
   155  			merged.FakeStorePath = opt.FakeStorePath
   156  		}
   157  	}
   158  	return merged
   159  }
   160  
   161  // OptionsFromEnv 環境変数からCallerOptionsを組み立てて返す
   162  func OptionsFromEnv() *CallerOptions {
   163  	return &CallerOptions{
   164  		AccessToken:       stringFromEnv("SAKURACLOUD_ACCESS_TOKEN", ""),
   165  		AccessTokenSecret: stringFromEnv("SAKURACLOUD_ACCESS_TOKEN_SECRET", ""),
   166  
   167  		APIRootURL:     stringFromEnv("SAKURACLOUD_API_ROOT_URL", ""),
   168  		DefaultZone:    stringFromEnv("SAKURACLOUD_DEFAULT_ZONE", ""),
   169  		Zones:          stringSliceFromEnv("SAKURACLOUD_ZONES", []string{}),
   170  		AcceptLanguage: stringFromEnv("SAKURACLOUD_ACCEPT_LANGUAGE", ""),
   171  
   172  		HTTPRequestTimeout:   intFromEnv("SAKURACLOUD_API_REQUEST_TIMEOUT", 0),
   173  		HTTPRequestRateLimit: intFromEnv("SAKURACLOUD_API_REQUEST_RATE_LIMIT", 0),
   174  
   175  		RetryMax:     intFromEnv("SAKURACLOUD_RETRY_MAX", 0),
   176  		RetryWaitMax: intFromEnv("SAKURACLOUD_RETRY_WAIT_MAX", 0),
   177  		RetryWaitMin: intFromEnv("SAKURACLOUD_RETRY_WAIT_MIN", 0),
   178  
   179  		TraceAPI:  profile.EnableAPITrace(stringFromEnv("SAKURACLOUD_TRACE", "")),
   180  		TraceHTTP: profile.EnableHTTPTrace(stringFromEnv("SAKURACLOUD_TRACE", "")),
   181  
   182  		FakeMode:      os.Getenv("SAKURACLOUD_FAKE_MODE") != "",
   183  		FakeStorePath: stringFromEnv("SAKURACLOUD_FAKE_STORE_PATH", ""),
   184  	}
   185  }
   186  
   187  // OptionsFromProfile 指定のプロファイルからCallerOptionsを組み立てて返す
   188  // プロファイル名に空文字が指定された場合はカレントプロファイルが利用される
   189  func OptionsFromProfile(profileName string) (*CallerOptions, error) {
   190  	if profileName == "" {
   191  		current, err := profile.CurrentName()
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  		profileName = current
   196  	}
   197  
   198  	config := profile.ConfigValue{}
   199  	if err := profile.Load(profileName, &config); err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	return &CallerOptions{
   204  		AccessToken:          config.AccessToken,
   205  		AccessTokenSecret:    config.AccessTokenSecret,
   206  		APIRootURL:           config.APIRootURL,
   207  		DefaultZone:          config.DefaultZone,
   208  		Zones:                config.Zones,
   209  		AcceptLanguage:       config.AcceptLanguage,
   210  		HTTPRequestTimeout:   config.HTTPRequestTimeout,
   211  		HTTPRequestRateLimit: config.HTTPRequestRateLimit,
   212  		RetryMax:             config.RetryMax,
   213  		RetryWaitMax:         config.RetryWaitMax,
   214  		RetryWaitMin:         config.RetryWaitMin,
   215  		TraceAPI:             config.EnableAPITrace(),
   216  		TraceHTTP:            config.EnableHTTPTrace(),
   217  		FakeMode:             config.FakeMode,
   218  		FakeStorePath:        config.FakeStorePath,
   219  	}, nil
   220  }
   221  
   222  func stringFromEnv(key, defaultValue string) string {
   223  	v := os.Getenv(key)
   224  	if v == "" {
   225  		return defaultValue
   226  	}
   227  	return v
   228  }
   229  
   230  func stringFromEnvMulti(keys []string, defaultValue string) string {
   231  	for _, key := range keys {
   232  		v := os.Getenv(key)
   233  		if v != "" {
   234  			return v
   235  		}
   236  	}
   237  	return defaultValue
   238  }
   239  
   240  func stringSliceFromEnv(key string, defaultValue []string) []string {
   241  	v := os.Getenv(key)
   242  	if v == "" {
   243  		return defaultValue
   244  	}
   245  	values := strings.Split(v, ",")
   246  	for i := range values {
   247  		values[i] = strings.Trim(values[i], " ")
   248  	}
   249  	return values
   250  }
   251  
   252  func intFromEnv(key string, defaultValue int) int {
   253  	v := os.Getenv(key)
   254  	if v == "" {
   255  		return defaultValue
   256  	}
   257  	i, err := strconv.ParseInt(v, 10, 64)
   258  	if err != nil {
   259  		return defaultValue
   260  	}
   261  	return int(i)
   262  }