options.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 api
  16  
  17  import (
  18  	"fmt"
  19  	"os"
  20  	"runtime"
  21  
  22  	client "github.com/sacloud/api-client-go"
  23  	"github.com/sacloud/api-client-go/profile"
  24  	"github.com/sacloud/iaas-api-go"
  25  	"github.com/sacloud/packages-go/envvar"
  26  )
  27  
  28  var UserAgent = fmt.Sprintf(
  29  	"sacloud/iaas-api-go/v%s (%s/%s; +https://github.com/sacloud/iaas-api-go) %s",
  30  	iaas.Version,
  31  	runtime.GOOS,
  32  	runtime.GOARCH,
  33  	client.DefaultUserAgent,
  34  )
  35  
  36  // CallerOptions iaas.APICallerを作成する際のオプション
  37  type CallerOptions struct {
  38  	*client.Options
  39  
  40  	APIRootURL  string
  41  	DefaultZone string
  42  	Zones       []string
  43  
  44  	TraceAPI      bool
  45  	FakeMode      bool
  46  	FakeStorePath string
  47  }
  48  
  49  // DefaultOption 環境変数、プロファイルからCallerOptionsを組み立てて返す
  50  //
  51  // プロファイルは環境変数`SAKURACLOUD_PROFILE`または`USACLOUD_PROFILE`でプロファイル名が指定されていればそちらを優先し、
  52  // 未指定の場合は通常のプロファイル処理(~/.usacloud/currentファイルから読み込み)される。
  53  // 同じ項目を複数箇所で指定していた場合、環境変数->プロファイルの順で上書きされたものが返される
  54  func DefaultOption() (*CallerOptions, error) {
  55  	return DefaultOptionWithProfile("")
  56  }
  57  
  58  // DefaultOptionWithProfile 環境変数、プロファイルからCallerOptionsを組み立てて返す
  59  //
  60  // プロファイルは引数を優先し、空の場合は環境変数`SAKURACLOUD_PROFILE`または`USACLOUD_PROFILE`が利用され、
  61  // それも空の場合は通常のプロファイル処理(~/.usacloud/currentファイルから読み込み)される。
  62  // 同じ項目を複数箇所で指定していた場合、環境変数->プロファイルの順で上書きされたものが返される
  63  func DefaultOptionWithProfile(profileName string) (*CallerOptions, error) {
  64  	options, err := client.DefaultOptionWithProfile(profileName)
  65  	if err != nil {
  66  		return nil, err
  67  	}
  68  
  69  	fromEnv := OptionsFromEnv()
  70  	fromEnv.Options = options
  71  
  72  	fromProfile, err := OptionsFromProfile(profileName)
  73  	if err != nil {
  74  		return nil, err
  75  	}
  76  	fromProfile.Options = options
  77  
  78  	defaults := &CallerOptions{
  79  		APIRootURL:  iaas.SakuraCloudAPIRoot,
  80  		DefaultZone: iaas.APIDefaultZone,
  81  		Zones:       iaas.SakuraCloudZones,
  82  		Options: &client.Options{
  83  			UserAgent: UserAgent,
  84  		},
  85  	}
  86  
  87  	return MergeOptions(defaults, fromEnv, fromProfile), nil
  88  }
  89  
  90  // OptionsFromEnv 環境変数からCallerOptionsを組み立てて返す
  91  func OptionsFromEnv() *CallerOptions {
  92  	return &CallerOptions{
  93  		Options:     client.OptionsFromEnv(),
  94  		APIRootURL:  envvar.StringFromEnv("SAKURACLOUD_API_ROOT_URL", ""),
  95  		DefaultZone: envvar.StringFromEnv("SAKURACLOUD_DEFAULT_ZONE", ""),
  96  		Zones:       envvar.StringSliceFromEnv("SAKURACLOUD_ZONES", []string{}),
  97  
  98  		TraceAPI: profile.EnableAPITrace(envvar.StringFromEnv("SAKURACLOUD_TRACE", "")),
  99  
 100  		FakeMode:      os.Getenv("SAKURACLOUD_FAKE_MODE") != "",
 101  		FakeStorePath: envvar.StringFromEnv("SAKURACLOUD_FAKE_STORE_PATH", ""),
 102  	}
 103  }
 104  
 105  // OptionsFromProfile 指定のプロファイルからCallerOptionsを組み立てて返す
 106  // プロファイル名に空文字が指定された場合はカレントプロファイルが利用される
 107  func OptionsFromProfile(profileName string) (*CallerOptions, error) {
 108  	options, err := client.OptionsFromProfile(profileName)
 109  	if err != nil {
 110  		return nil, err
 111  	}
 112  	config := options.ProfileConfigValue()
 113  	return &CallerOptions{
 114  		Options:       options,
 115  		APIRootURL:    config.APIRootURL,
 116  		DefaultZone:   config.DefaultZone,
 117  		Zones:         config.Zones,
 118  		TraceAPI:      config.EnableAPITrace(),
 119  		FakeMode:      config.FakeMode,
 120  		FakeStorePath: config.FakeStorePath,
 121  	}, nil
 122  }
 123  
 124  // MergeOptions 指定のCallerOptionsの非ゼロ値フィールドをoのコピーにマージして返す
 125  func MergeOptions(opts ...*CallerOptions) *CallerOptions {
 126  	merged := &CallerOptions{}
 127  	for _, opt := range opts {
 128  		if opt.Options != nil {
 129  			var opts []*client.Options
 130  			if merged.Options != nil {
 131  				opts = append(opts, merged.Options)
 132  			}
 133  			opts = append(opts, opt.Options)
 134  			merged.Options = client.MergeOptions(opts...)
 135  		}
 136  		if opt.APIRootURL != "" {
 137  			merged.APIRootURL = opt.APIRootURL
 138  		}
 139  		if opt.DefaultZone != "" {
 140  			merged.DefaultZone = opt.DefaultZone
 141  		}
 142  		if len(opt.Zones) > 0 {
 143  			merged.Zones = opt.Zones
 144  		}
 145  
 146  		// Note: bool値は一度trueにしたらMergeでfalseになることがない
 147  		if opt.TraceAPI {
 148  			merged.TraceAPI = true
 149  		}
 150  		if opt.FakeMode {
 151  			merged.FakeMode = true
 152  		}
 153  		if opt.FakeStorePath != "" {
 154  			merged.FakeStorePath = opt.FakeStorePath
 155  		}
 156  	}
 157  	return merged
 158  }
 159